<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>goodjob79 님의 블로그</title>
    <link>https://goodjop79.tistory.com/</link>
    <description>Step By Step!  초보 인공지능 응용시스템 개발자 교육수강 스토리.  오류를 고치며 배운다.(개인학습, 응용S/W, AI, Python, C#,C++)</description>
    <language>ko</language>
    <pubDate>Sun, 19 Jul 2026 10:12:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>goodjob^^</managingEditor>
    <image>
      <title>goodjob79 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8630992/attach/4f32a95d940f4898b4bed7afd93daee4</url>
      <link>https://goodjop79.tistory.com</link>
    </image>
    <item>
      <title>D+68[Project] UI 화면구현(모달,팝업) &amp;amp; 데이터가져오기 에러 조치는?</title>
      <link>https://goodjop79.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;7월&amp;nbsp;14일(화)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 주 제 : UI 화면 구현시 모달과 팝업의 차이점 &amp;amp; 데이터 가져오기 에러 조치 하기&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. 화면 구현시 모달과 팝업 : &lt;/b&gt;웹 디자인/개발 분야에서 &lt;b&gt;'팝업(Popup)'과 '모달(Modal)'의 핵심적인 차이점&lt;/b&gt;&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;
 &lt;b&gt;1.1. 팝업 vs 모달 핵심 차이점&lt;/b&gt;
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;
 &lt;b&gt;&lt;/b&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 100%; height: 110px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt; 
  &lt;tbody&gt; 
   &lt;tr style=&quot;height: 22px;&quot;&gt; 
    &lt;td style=&quot;width: 20.5426%; text-align: center; height: 22px;&quot;&gt;&lt;b&gt;차 이 점&lt;/b&gt;&lt;/td&gt; 
    &lt;td style=&quot;width: 42.0542%; text-align: center; height: 22px;&quot;&gt;&lt;b&gt;팝&amp;nbsp; 업&lt;/b&gt;&lt;/td&gt; 
    &lt;td style=&quot;width: 37.4031%; text-align: center; height: 22px;&quot;&gt;&lt;b&gt;모&amp;nbsp; 달&lt;/b&gt;&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 22px;&quot;&gt; 
    &lt;td style=&quot;width: 20.5426%; text-align: center; height: 22px;&quot;&gt;창이 열리는 방식&lt;/td&gt; 
    &lt;td style=&quot;width: 42.0542%; text-align: center; height: 22px;&quot;&gt;브라우저의 새로운 독립된 창(Window)을 &lt;br&gt;생성하여 띄운다.&lt;/td&gt; 
    &lt;td style=&quot;width: 37.4031%; text-align: center; height: 22px;&quot;&gt;현재 보고 있는 웹 페이지(Layer) 위에 어두운 &lt;br&gt;배경과 함께 덧씌운다.&amp;nbsp;&amp;nbsp;&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 22px;&quot;&gt; 
    &lt;td style=&quot;width: 20.5426%; text-align: center; height: 22px;&quot;&gt;기존 화면 조작 가능 여부&lt;/td&gt; 
    &lt;td style=&quot;width: 42.0542%; text-align: center; height: 22px;&quot;&gt;창이 떠 있어도 뒤의 메인 페이지를 자유롭게 &lt;br&gt;조작할 수 있다.&lt;/td&gt; 
    &lt;td style=&quot;width: 37.4031%; text-align: center; height: 22px;&quot;&gt;모달창을 닫기 전까지는 뒤의 메인 페이지를 절대 &lt;br&gt;조작할 수 없다&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 22px;&quot;&gt; 
    &lt;td style=&quot;width: 20.5426%; text-align: center; height: 22px;&quot;&gt;사용목적 및 용도&lt;/td&gt; 
    &lt;td style=&quot;width: 42.0542%; text-align: center; height: 22px;&quot;&gt;단순 공지, 이벤트 광고 등 메인 기능 외 부가 &lt;br&gt;정보 노출에 사용&lt;/td&gt; 
    &lt;td style=&quot;width: 37.4031%; text-align: center; height: 22px;&quot;&gt;로그인, 회원가입, 경고, 필수 동의 등 사용자의 &lt;br&gt;특정 행동을 강제해야 할 때 쓰인다&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr style=&quot;height: 22px;&quot;&gt; 
    &lt;td style=&quot;width: 20.5426%; text-align: center; height: 22px;&quot;&gt;기술적 특징&lt;/td&gt; 
    &lt;td style=&quot;width: 42.0542%; text-align: center; height: 22px;&quot;&gt;사용자 브라우저의 팝업 차단 기능에 걸려 &lt;br&gt;안 보일 수 있다&lt;/td&gt; 
    &lt;td style=&quot;width: 37.4031%; text-align: center; height: 22px;&quot;&gt;&lt;span&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;웹 페이지 내부 코드(HTML/CSS)로 &lt;br&gt;구현되므로 차단되지 않는다&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;※ '모달' 방식으로 구현하는 화면 :&amp;nbsp; 배경창은 흐려지고, 마우스 이동시 창이 두개가 함께 움직인다. 좀더 깔끔하게 실행된다.&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wLW1q/dJMcaccKy2d/4KGFOfve044YhIkO3XUgJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wLW1q/dJMcaccKy2d/4KGFOfve044YhIkO3XUgJK/img.png&quot; data-alt=&quot;모달 방식으로 구현하는 화면 : 배경창은 흐려지고, 마우스 이동시 창이 두개가 함께 움직인다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wLW1q/dJMcaccKy2d/4KGFOfve044YhIkO3XUgJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwLW1q%2FdJMcaccKy2d%2F4KGFOfve044YhIkO3XUgJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;475&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모달 방식으로 구현하는 화면 : 배경창은 흐려지고, 마우스 이동시 창이 두개가 함께 움직인다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. 2. 팝업 (Popup) 구현하기&lt;/b&gt;&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
 팝업은 브라우저의 &lt;b&gt;새 창을 여는 방식&lt;/b&gt;이므로 &lt;b&gt;CSS 없이 JavaScript의 window.open() 함수&lt;/b&gt;만으로 구현한다.
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Aokoqd_19,Aokoqd_18&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
    &lt;pre class=&quot;bash&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// html
&amp;lt;!-- 메인 페이지 (index.html) --&amp;gt;
&amp;lt;button onclick=&quot;openPopup()&quot;&amp;gt;팝업창 열기&amp;lt;/button&amp;gt;

&amp;lt;script&amp;gt;
function openPopup() {
    // window.open('주소', '창이름', '옵션')
    window.open(
        'popup.html', 
        'samplePopup', 
        'width=400, height=500, top=100, left=100, resizable=no'
    );
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Aokoqd_1h,Aokoqd_1g&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
    &lt;pre class=&quot;bash&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;//html
&amp;lt;!-- 팝업으로 열릴 페이지 (popup.html) --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;팝업창&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h3&amp;gt;공지사항 팝업입니다.&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;이 창이 떠 있어도 뒤의 메인 페이지를 조작할 수 있습니다.&amp;lt;/p&amp;gt;
    &amp;lt;button onclick=&quot;window.close()&quot;&amp;gt;창 닫기&amp;lt;/button&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 3px 0px 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-ved=&quot;2ahUKEwihruHA-dGVAxVUh68BHYXPATcQh9gSegYIAAgGEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
    &amp;nbsp;
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;
 &amp;nbsp;
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;
 &lt;b&gt;1.3. 모달 (Modal) 구현하기&lt;/b&gt;
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
 모달은 현재 페이지 위에 레이어를 얹고, 배경을 어둡게(Dimmed) 만들어 뒤쪽 화면 조작을 막아야 하므로 &lt;b&gt;CSS 구조와 JS를 통한 클래스 제어&lt;/b&gt;가 핵임이다.
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;※ JS를 사용해 CSS의 클래스를 조작(추가/삭제)함으로써 동적인 화면을 구현하고, &lt;/span&gt;&lt;b&gt;JS에서 스타일 직접 변경 지양하는 이유는&amp;nbsp; &amp;nbsp; &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;예를 들어 element.style.color = &quot;red&quot; 처럼 JS로 CSS 속성을 직접 변경하면 유지보수가 어려워진다.&amp;nbsp;&amp;nbsp;&lt;/span&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;★웹 개발에서 &lt;/span&gt;&lt;u&gt;&lt;b&gt;CSS&lt;/b&gt;&lt;/u&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;u&gt;는 웹페이지의 레이아웃과 디자인&lt;/u&gt;을 담당하고, &lt;u&gt;&lt;b&gt;JS(자바스크립트)&lt;/b&gt;는 사용자와 상호작용하는 동적인 기능&lt;/u&gt;을 담당&lt;/span&gt;&lt;/p&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Aokoqd_27,Aokoqd_26&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
    &lt;pre class=&quot;bash&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;//html
&amp;lt;!-- 모달을 열기 위한 버튼 --&amp;gt;
&amp;lt;button id=&quot;openModalBtn&quot;&amp;gt;모달창 열기&amp;lt;/button&amp;gt;

&amp;lt;!-- 모달 전체를 감싸는 컨테이너 (어두운 배경 역할) --&amp;gt;
&amp;lt;div id=&quot;modalContainer&quot; class=&quot;modal-hidden&quot;&amp;gt;
    &amp;lt;!-- 실제 모달 콘텐츠 박스 --&amp;gt;
    &amp;lt;div class=&quot;modal-content&quot;&amp;gt;
        &amp;lt;h3&amp;gt;필수 동의 안내&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;모달창을 닫기 전까지는 뒤의 메인 페이지를 클릭할 수 없습니다.&amp;lt;/p&amp;gt;
        &amp;lt;button id=&quot;closeModalBtn&quot;&amp;gt;확인 및 닫기&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDBAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Aokoqd_2m,Aokoqd_2l&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
    &lt;pre class=&quot;bash&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;//CSS
/* 어두운 배경 처리 및 화면 중앙 정렬 */
#modalContainer {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.6); /* 뒷 배경을 어둡게 */
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9999; /* 항상 최상단에 위치 */
}

/* 흰색 모달 박스 스타일 */
.modal-content {
    background-color: white;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    max-width: 400px;
    width: 100%;
    text-align: center;
}

/* 모달을 숨기는 클래스 */
.modal-hidden {
    display: none !important;
}&lt;/code&gt;&lt;/pre&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Aokoqd_31,Aokoqd_30&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt; 
  &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt; 
   &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
    &lt;pre class=&quot;bash&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// JavaScript
const openModalBtn = document.getElementById('openModalBtn');
const closeModalBtn = document.getElementById('closeModalBtn');
const modalContainer = document.getElementById('modalContainer');

// 모달 열기: 숨김 클래스 제거
openModalBtn.addEventListener('click', () =&amp;gt; {
    modalContainer.classList.remove('modal-hidden');
});

// 모달 닫기: 숨김 클래스 추가
closeModalBtn.addEventListener('click', () =&amp;gt; {
    modalContainer.add('modal-hidden');
});

// (선택 사항) 모달 바깥 어두운 영역을 클릭해도 닫히게 설정
modalContainer.addEventListener('click', (e) =&amp;gt; {
    if (e.target === modalContainer) {
        modalContainer.classList.add('modal-hidden');
    }
});&lt;/code&gt;&lt;/pre&gt; 
   &lt;/div&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;figure data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;021&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/021.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/021.gif&quot; width=&quot;150&quot;&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. DB 연동하는 과정에서 데이터 가져오기 실패 원인 확인 및 조치&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2agyK/dJMcaiYgpXY/gSsHqxmMRgMkM3OyWlXKtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2agyK/dJMcaiYgpXY/gSsHqxmMRgMkM3OyWlXKtK/img.png&quot; data-alt=&quot;데이터 연동시 애러 메세지 : CREAT DATABASE;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2agyK/dJMcaiYgpXY/gSsHqxmMRgMkM3OyWlXKtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2agyK%2FdJMcaiYgpXY%2FgSsHqxmMRgMkM3OyWlXKtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;461&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 연동시 애러 메세지 : CREAT DATABASE;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;# 1049 에러 주요 원인&lt;br&gt;- ​데이터베이스 이름 오타: 연결 설정 파일(application.properties, .env, config.js 등)에 적은 DB 이름에 오타가 있는 경우&lt;br&gt;- ​실제 DB를 생성하지 않음: 새 프로젝트를 시작하면서 코드 설정만 해두고, 정작 MySQL 서버에는 CREATE DATABASE...로 물리적인 공간을 만들어두지 않았을 때 발생&lt;br&gt;- ​백업 파일(Dump) 복원 시 오류: 데이터베이스를 백업받은 .sql 파일을 새 서버에 가져와 넣으려고(Import) 할 때, 새 서버에 동일한 이름의 빈 DB를 미리 만들어두지 않아서 자주 발생.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.1. 코드에서 해결 방법 step-by-step&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1.1. 접속 정보(ID/PW) 재확인&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - 코드나 설정 파일(.env, config.js, settings.py 등)에 적힌 데이터베이스 접속 정보를 다시 확인&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; - Username:&lt;/b&gt; 'user'가 맞는지 확인 (보통 기본 관리자는 root)&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; - Password:&lt;/b&gt; 비밀번호에 오타가 없는지, 공백이 들어가지 않았는지 확인&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.1.2. 권한(Grant) 및 호스트(Host) 확인&lt;/b&gt;&lt;br&gt;- MySQL은 &lt;b&gt;&lt;u&gt;사용자명&lt;/u&gt; &lt;/b&gt;뿐만 아니라 접속하는 &lt;b&gt;&lt;u&gt;위치(Host)도 정확히 일치&lt;/u&gt;&lt;/b&gt;해야 한다.&lt;br&gt;- 에러 메시지의 'user'@'localhost'는 localhost(현재 컴퓨터)에서 user라는 이름으로 접속을 시도했다&lt;br&gt;- 데이터베이스 관리자 계정(root 등)으로 접속하여 해당 사용자가 등록되어 있고 권한이 있는지 확인&lt;br&gt;&lt;b&gt;- 사용자 목록 및 접속 호스트 확인 : &lt;/b&gt;&amp;nbsp;user와 localhost가 쌍으로 존재해야 하고, 만약 외부에서 접속하는 것이라면 localhost 대신 %(모든 호스트) 또는 특정 IP가 지정되어 있어야 한다.&lt;/p&gt;&lt;div&gt; 
 &lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# SQL 
SELECT User, Host FROM mysql.user;&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;- 사용자 생성 및 권한 부여 (필요한 경우) :&lt;/b&gt; 만약 계정이 없거나 권한이 없다면, 관리자 계정으로 접속하여 아래 명령어로 계정을 생성하고 권한을 부여하기&lt;/p&gt;&lt;div&gt; 
 &lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// SQL
-- 계정 생성 (비밀번호 설정)
CREATE USER 'user'@'localhost' IDENTIFIED BY '실제비밀번호';

-- 특정 데이터베이스(예: mydb)에 대한 모든 권한 부여
GRANT ALL PRIVILEGES ON mydb.* TO 'user'@'localhost';

-- 설정 반영
FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3. 비밀번호 인증 방식 문제 &lt;span style=&quot;color: #8A3DB6;&quot;&gt;(MySQL 8.0 이상)&lt;/span&gt;&lt;/b&gt;&lt;br&gt;- MySQL 8.0 이상부터는 기본 비밀번호 암호화 방식(caching_sha2_password)이 변경되어, 기존 라이브러리나 툴에서 접속하지 못하는 경우가 있다. 이 경우 기존 방식 (mysql_native_password)으로 변경해 주면 해결된다.&lt;/p&gt;&lt;div data-ved=&quot;0CAAQhtANahcKEwjTpNLN-9GVAxUAAAAAHQAAAAAQLg&quot; data-hveid=&quot;0&quot;&gt; 
 &lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// SQL
ALTER USER 'user'@'localhost' IDENTIFIED WITH mysql_native_password BY '실제비밀번호';
FLUSH PRIVILEGES;&lt;/code&gt;&lt;/pre&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;※ 코드의 데이터베이스 설정 부분에서 &lt;b&gt;ID가 user가 맞는지&lt;/b&gt;, &lt;b&gt;비밀번호가 정확한지&lt;/b&gt; 먼저 확인후, 그래도 안 된다면 MySQL 서버에 root 계정으로 들어가 &lt;b&gt;user 계정의 생성 여부와 권한&lt;/b&gt;을 확인하자.&amp;nbsp;&lt;br&gt;웹페이지 구현은 실제로 해보면 결코 쉽지 않은 작업이다. 인공지능이 아무리 자동으로 쉽게 해준다고는 하지만 그 문법을 알지 못하면 도무지 이해할 수가 없다. 개발후 유지보수가 관건이다.&amp;nbsp; &quot;도전은 계속된다.&quot;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/92</guid>
      <comments>https://goodjop79.tistory.com/92#entry92comment</comments>
      <pubDate>Tue, 14 Jul 2026 20:29:02 +0900</pubDate>
    </item>
    <item>
      <title>D+67[C++] Claude Code 활용 MySQL 연동, 실습(shopping mall)</title>
      <link>https://goodjop79.tistory.com/90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 13일(월)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 내 용 : 구현하고 싶은 내용들을 인공지능을 응용하여 Claude Code로 작성하기 (개인학습)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 주요 내용&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - 주제 선정 및 브레인스토밍 토의, 설계 중점 토의, 개인 임무분담 (4H)&lt;br&gt;&amp;nbsp; &amp;nbsp; - 주요 구현할 사항 토의 등 설계를 하기 위한 기획서 작성하기 (1H)&lt;br&gt;&amp;nbsp; &amp;nbsp; - 작성한 기획서를 바탕으로 테이블 명세서 작성하기 (1H)&lt;br&gt;&amp;nbsp; &amp;nbsp; - 구현 요구사항을 포함하여 터미널창에서 Claude Code 실행하기 (응용사항 반영한 코드 작성 10분)&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp; - 작성한 프로그램 소스코드 개인 학습 리뷰 &lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. Claude Code 개인 학습&lt;/b&gt;&lt;br&gt;&lt;b&gt;1.1. 환경 : 우분투 파이참,&amp;nbsp; PySide6, MySQL, 로컬 &quot;가상서버&quot; 연동&lt;/b&gt;&lt;br&gt;&lt;b&gt;1.2. 진행 화면&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KYFs7/dJMcadJo4fL/pbQrWpnj5JVMG7lvGYfkb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KYFs7/dJMcadJo4fL/pbQrWpnj5JVMG7lvGYfkb0/img.png&quot; data-alt=&quot;데이터베이스 및 UI 구현 실습 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KYFs7/dJMcadJo4fL/pbQrWpnj5JVMG7lvGYfkb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKYFs7%2FdJMcadJo4fL%2FpbQrWpnj5JVMG7lvGYfkb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;966&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터베이스 및 UI 구현 실습 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGYaCY/dJMcajbM4ZX/lshACrRkKEdZAkl17CWlok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGYaCY/dJMcajbM4ZX/lshACrRkKEdZAkl17CWlok/img.png&quot; data-alt=&quot;실제 동작 검증 단계 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGYaCY/dJMcajbM4ZX/lshACrRkKEdZAkl17CWlok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGYaCY%2FdJMcajbM4ZX%2FlshACrRkKEdZAkl17CWlok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;966&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 동작 검증 단계 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sPwjH/dJMcafghQmZ/GmTdtLFKhYNKFFe0gSLRv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sPwjH/dJMcafghQmZ/GmTdtLFKhYNKFFe0gSLRv1/img.png&quot; data-alt=&quot;MySQL 실행 및 메인파일 실행 하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sPwjH/dJMcafghQmZ/GmTdtLFKhYNKFFe0gSLRv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsPwjH%2FdJMcafghQmZ%2FGmTdtLFKhYNKFFe0gSLRv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;966&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL 실행 및 메인파일 실행 하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z6d4i/dJMcajbM41u/TzkzFNGPIwQglWV8hDdLTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z6d4i/dJMcajbM41u/TzkzFNGPIwQglWV8hDdLTk/img.png&quot; data-alt=&quot;전체 파일 구문 검사 및 Test&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z6d4i/dJMcajbM41u/TzkzFNGPIwQglWV8hDdLTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz6d4i%2FdJMcajbM41u%2FTzkzFNGPIwQglWV8hDdLTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;966&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 파일 구문 검사 및 Test&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;771&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhA8Ne/dJMcag7f2lC/RkAMS03excLMdkbhcGEhJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhA8Ne/dJMcag7f2lC/RkAMS03excLMdkbhcGEhJK/img.png&quot; data-alt=&quot;자체 검증한 것과 안된것 공지 화면, 서버연동 비번입력후 별도 실행 하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhA8Ne/dJMcag7f2lC/RkAMS03excLMdkbhcGEhJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhA8Ne%2FdJMcag7f2lC%2FRkAMS03excLMdkbhcGEhJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1138&quot; height=&quot;771&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;771&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자체 검증한 것과 안된것 공지 화면, 서버연동 비번입력후 별도 실행 하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Sy45/dJMcai41GJB/BKXpTkAjYTYHGrT6bJJmR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Sy45/dJMcai41GJB/BKXpTkAjYTYHGrT6bJJmR0/img.png&quot; data-alt=&quot;지금 작업한 내용들을 다시 이어서 진행하고자 할때 알림 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Sy45/dJMcai41GJB/BKXpTkAjYTYHGrT6bJJmR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Sy45%2FdJMcai41GJB%2FBKXpTkAjYTYHGrT6bJJmR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1131&quot; height=&quot;403&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지금 작업한 내용들을 다시 이어서 진행하고자 할때 알림 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1.3.&amp;nbsp; Claude Code 응용을 통한 학습 &amp;amp; README.md&amp;nbsp; 사용설명서(메뉴얼)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# PySide6 학습- 쇼핑몰

PySide6(Qt for Python) + MySQL 기반 데스크톱 쇼핑몰 애플리케이션
`member, category, product, product_option, cart, orders, order_item, board_post, comment`
9개 테이블 각각에 대해 CRUD 화면을 제공

## 프로젝트 구조

```
shopping_mall_pyside6/
├── main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 진입점 (로그인 -&amp;gt; 메인 윈도우)
├── requirements.txt
├── .env.example&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# DB 접속 정보 템플릿
├── sql/
│&amp;nbsp;&amp;nbsp; └── schema.sql&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 9개 테이블 DDL
├── config/
│&amp;nbsp;&amp;nbsp; └── db_config.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# .env 로드 및 DB 설정값
├── models/&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 테이블별 CRUD (Repository 패턴)
│&amp;nbsp;&amp;nbsp; ├── database.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # PyMySQL 커넥션/쿼리 실행 공통 로직
│&amp;nbsp;&amp;nbsp; ├── member.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# MEMBER
│&amp;nbsp;&amp;nbsp; ├── category.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# CATEGORY
│&amp;nbsp;&amp;nbsp; ├── product.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# PRODUCT
│&amp;nbsp;&amp;nbsp; ├── product_option.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # PRODUCT_OPTION
│&amp;nbsp;&amp;nbsp; ├── cart.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# CART
│&amp;nbsp;&amp;nbsp; ├── orders.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # ORDERS
│&amp;nbsp;&amp;nbsp; ├── order_item.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ORDER_ITEM
│&amp;nbsp;&amp;nbsp; ├── board_post.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # BOARD_POST
│&amp;nbsp;&amp;nbsp; └── comment.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # COMMENT
├── ui/&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # PySide6 화면
│&amp;nbsp;&amp;nbsp; ├── login_dialog.py
│&amp;nbsp;&amp;nbsp; ├── signup_dialog.py
│&amp;nbsp;&amp;nbsp; ├── mypage_widget.py
│&amp;nbsp;&amp;nbsp; ├── category_widget.py
│&amp;nbsp;&amp;nbsp; ├── product_widget.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 상품 + 상품옵션 + 장바구니 담기
│&amp;nbsp;&amp;nbsp; ├── cart_widget.py
│&amp;nbsp;&amp;nbsp; ├── orders_widget.py
│&amp;nbsp;&amp;nbsp; ├── board_widget.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 게시글 + 댓글
│&amp;nbsp;&amp;nbsp; └── main_window.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 탭으로 위 화면들을 통합
└── utils/
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├── validators.py
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├── password_hash.py
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└── constants.py
```

## 1. MySQL 준비 (Unix)

```bash
sudo systemctl start mysql&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 또는 mysqld
mysql -u root -p &amp;lt; sql/schema.sql
```

`sql/schema.sql`은 `shopping_mall` 데이터베이스를 새로 만들고 9개 테이블을 생성

## 2. PyCharm에서 프로젝트 열기

1. PyCharm에서 `shopping_mall_pyside6` 폴더를 프로젝트로 열기
2. 터미널(PyCharm 하단 Terminal 탭)에서 가상환경을 만들기

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

3. PyCharm 우측 상단에서 인터프리터를 `.venv`로 지정
&amp;nbsp;&amp;nbsp; (Settings → Project → Python Interpreter)

## 3. 환경 변수 설정

`.env.example`을 복사해 `.env`를 만들고 본인 MySQL 접속 정보로 수정

```bash
cp .env.example .env
```

```
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=본인 비밀번호
DB_NAME=shopping_mall
```

## 4. 실행

```bash
python main.py
```

또는 PyCharm에서 `main.py`를 열고 우측 상단 ▶(Run) 버튼을 누른다.

실행하면 로그인 창이 뜹니다. &quot;회원가입&quot;으로 회원을 만든 뒤 로그인하면
상품 / 장바구니 / 주문 내역 / 게시판 / 카테고리 관리 / 마이페이지 탭이 있는
메인 화면으로 이동

## 5. 화면과 테이블 매핑

| 탭 | 테이블 | 주요 기능 |
|---|---|---|
| 로그인/회원가입 | member | 가입, 로그인 인증(bcrypt) |
| 마이페이지 | member | 본인 정보 조회/수정 |
| 카테고리 관리 | category | 등록/수정/삭제, 사용여부 토글 |
| 상품 | product, product_option | 검색/필터, 등록, 옵션 등록, 판매중지, 장바구니 담기 |
| 장바구니 | cart | 수량 변경, 삭제, 주문 전환 |
| 주문 내역 | orders, order_item | 주문 목록 및 상세(스냅샷) 조회 |
| 게시판 | board_post, comment | 글쓰기/수정/삭제, 댓글 작성/조회 |

## 클린 코드 원칙 (기획서 컨벤션 준수)

- 클래스명 PascalCase (`MemberRepository`), 함수/변수명 snake_case, 상수 UPPER_CASE
- DB 접근은 `models/` 계층으로만 수행하고, UI는 Repository를 통해서만 데이터를 다룬다.
- 비밀번호는 반드시 `utils/password_hash.py`(bcrypt)를 통해서만 저장/검증한다.
- 화면 입력값 검증은 `utils/validators.py`에 모아 재사용한다.
- 각 위젯은 담당 테이블/화면 하나의 책임만 가진다.

## 알려진 단순화 사항 (추후 개선)

- 최종 테이블 스펙(`member`)에는 관리자 구분 컬럼이 없어, 모든 로그인 회원이
&amp;nbsp;&amp;nbsp;카테고리/상품 관리 탭에 접근할 수 있도록 단순화했습니다. 역할 분리가 필요하면
&amp;nbsp;&amp;nbsp;`member`에 `role` 컬럼을 추가하고 `MainWindow`에서 탭 노출을 제어하면 된다.
- 주문 처리 시 재고 검증 후 차감하지만, 여러 테이블에 걸친 작업을 하나의
&amp;nbsp;&amp;nbsp;DB 트랜잭션으로 묶지는 않았습니다(학습용 단순화). 운영 환경에서는
&amp;nbsp;&amp;nbsp;`Database.cursor()`를 확장해 여러 쿼리를 한 트랜잭션으로 묶는 것을 권장&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;PySide6&amp;gt;=6.6.0&lt;br&gt;PyMySQL&amp;gt;=1.1.0&lt;br&gt;bcrypt&amp;gt;=4.0.1&lt;br&gt;python-dotenv&amp;gt;=1.0.0&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/90</guid>
      <comments>https://goodjop79.tistory.com/90#entry90comment</comments>
      <pubDate>Mon, 13 Jul 2026 21:53:36 +0900</pubDate>
    </item>
    <item>
      <title>D+66[C+MySQl] 회원가입/로그인을 MySQL서버와 연동하기 과제</title>
      <link>https://goodjop79.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 12일(일)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 주 제 : &amp;nbsp;회원가입 / 로그인을 MySQL서버와 연동하기 학습&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 구현할 내용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PySide6 GUI &lt;br /&gt;QTabWidget(회원가입 / 로그인) &lt;br /&gt;MySQL Server 연동(mysql-connector-python) &lt;br /&gt;회원가입 &lt;br /&gt;로그인 &lt;br /&gt;아이디 중복 확인 &lt;br /&gt;이메일 @ 검사 &lt;br /&gt;Parameter Binding(%s) &lt;br /&gt;try / except / finally &lt;br /&gt;conn=None 사용 &lt;br /&gt;문자열 직접 SQL 조합 금지&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1.1. 구현 조건&lt;/b&gt;&lt;br /&gt;회원가입과&amp;nbsp;로그인으로&amp;nbsp;구성된&amp;nbsp;PySide6&amp;nbsp;프로그램&amp;nbsp;작성 &lt;br /&gt;DB&amp;nbsp;연동해서&amp;nbsp;회원가입,&amp;nbsp;로그인&amp;nbsp;구현하기 &lt;br /&gt;로그인&amp;nbsp;탭은&amp;nbsp;아이디,&amp;nbsp;비밀번호&amp;nbsp;일치&amp;nbsp;여부에&amp;nbsp;따라&amp;nbsp;성공,&amp;nbsp;실패를&amp;nbsp;표시하기 &lt;br /&gt;server는&amp;nbsp;MySQL&amp;nbsp;서버&amp;nbsp;사용 &lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.1.1.필수 구현 조건&lt;/b&gt; &lt;br /&gt;QTabWidget으로&amp;nbsp;회원가입&amp;nbsp;탭과&amp;nbsp;로그인&amp;nbsp;탭을&amp;nbsp;분리&amp;nbsp;구현 &lt;br /&gt;회원가입시&amp;nbsp;아이디,&amp;nbsp;이메일,&amp;nbsp;비밀번호&amp;nbsp;모두&amp;nbsp;입력받기 &lt;br /&gt;SQL&amp;nbsp;값은&amp;nbsp;%s&amp;nbsp;파리미터&amp;nbsp;바인딩으로&amp;nbsp;전달 &lt;br /&gt;문자열&amp;nbsp;직접&amp;nbsp;조합은&amp;nbsp;금지 &lt;br /&gt;DB연결은&amp;nbsp;CONN=None&amp;nbsp;+&amp;nbsp;try/except/finally&amp;nbsp;구조&amp;nbsp;사용 &lt;br /&gt;아이디,&amp;nbsp;이메일,&amp;nbsp;비밀번호&amp;nbsp;중&amp;nbsp;하나라도&amp;nbsp;비어&amp;nbsp;있으면&amp;nbsp;처리후&amp;nbsp;저장&amp;nbsp;차단 &lt;br /&gt;이메일&amp;nbsp;형식은&amp;nbsp;최소한&amp;nbsp;&quot;@&quot;&amp;nbsp;포함&amp;nbsp;여부만&amp;nbsp;확인&amp;nbsp;(정구식&amp;nbsp;검증은&amp;nbsp;선택)&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 설치 라이브러리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;pip install PySide6
pip install mysql-connector-python&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.1.&amp;nbsp; 2방안 구현 내용 (화면, 사진)&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-57-34.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmkKtr/dJMcai41lsw/2WDuuyt3DD3Mu2y6y77KHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmkKtr/dJMcai41lsw/2WDuuyt3DD3Mu2y6y77KHK/img.png&quot; data-alt=&quot;로그인 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmkKtr/dJMcai41lsw/2WDuuyt3DD3Mu2y6y77KHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmkKtr%2FdJMcai41lsw%2F2WDuuyt3DD3Mu2y6y77KHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-57-34.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-57-50.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zFUkz/dJMcacDSCP6/HZWnL9KiRKIUyExZnuxoQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zFUkz/dJMcacDSCP6/HZWnL9KiRKIUyExZnuxoQ0/img.png&quot; data-alt=&quot;서버에서 사용가능한 아이디 확인 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zFUkz/dJMcacDSCP6/HZWnL9KiRKIUyExZnuxoQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzFUkz%2FdJMcacDSCP6%2FHZWnL9KiRKIUyExZnuxoQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-57-50.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버에서 사용가능한 아이디 확인 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-16.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0nxjs/dJMcaf1yQer/lsD5Y5bpWyILyxBlZwI9L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0nxjs/dJMcaf1yQer/lsD5Y5bpWyILyxBlZwI9L1/img.png&quot; data-alt=&quot;회원가입이 완료되었음을 알림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0nxjs/dJMcaf1yQer/lsD5Y5bpWyILyxBlZwI9L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0nxjs%2FdJMcaf1yQer%2FlsD5Y5bpWyILyxBlZwI9L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-16.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원가입이 완료되었음을 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-41.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl93b9/dJMcaixhR79/1Stv1eg8oIsEFJKY7NPONK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl93b9/dJMcaixhR79/1Stv1eg8oIsEFJKY7NPONK/img.png&quot; data-alt=&quot;비밀번호 찾기를 위한 보인 확인 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl93b9/dJMcaixhR79/1Stv1eg8oIsEFJKY7NPONK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl93b9%2FdJMcaixhR79%2F1Stv1eg8oIsEFJKY7NPONK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-41.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비밀번호 찾기를 위한 보인 확인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-55.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNDQ9C/dJMcabkE6mc/tT9anKBFMhnhzyqk3Yjo20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNDQ9C/dJMcabkE6mc/tT9anKBFMhnhzyqk3Yjo20/img.png&quot; data-alt=&quot;비밀번호 변경후 알림 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNDQ9C/dJMcabkE6mc/tT9anKBFMhnhzyqk3Yjo20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNDQ9C%2FdJMcabkE6mc%2FtT9anKBFMhnhzyqk3Yjo20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-58-55.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비밀번호 변경후 알림 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-59-11.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qZjQ/dJMcafghtPH/ld0neivOjkRMARhvnWw3wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qZjQ/dJMcafghtPH/ld0neivOjkRMARhvnWw3wk/img.png&quot; data-alt=&quot;회원정보 수정을 위한 인증 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qZjQ/dJMcafghtPH/ld0neivOjkRMARhvnWw3wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qZjQ%2FdJMcafghtPH%2Fld0neivOjkRMARhvnWw3wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-59-11.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원정보 수정을 위한 인증 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-13 13-59-19.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SLfr0/dJMcacRieFe/mhIrSYUdITKZQjvcy3e880/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SLfr0/dJMcacRieFe/mhIrSYUdITKZQjvcy3e880/img.png&quot; data-alt=&quot;회원정보 수정후 완료 알림 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SLfr0/dJMcacRieFe/mhIrSYUdITKZQjvcy3e880/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSLfr0%2FdJMcacRieFe%2FmhIrSYUdITKZQjvcy3e880%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1238&quot; height=&quot;892&quot; data-filename=&quot;스크린샷 2026-07-13 13-59-19.png&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회원정보 수정후 완료 알림 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Ⅰ방안&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.1. MySQL DB 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;//PyqtDB
// pyqtdb라는 이름의 데이터베이스를 생성하며, 이모지 저장 등이 가능한 utf8mb4 문자셋을 설정
CREATE DATABASE pyqtdb CHARACTER SET utf8mb4; 

// 이후 진행할 모든 작업(테이블 생성 등)을 pyqtdb 데이터베이스 안에서 수행하도록 지정
USE pyqtdb; 

// 사용자 정보를 저장할 users 테이블을 생성
CREATE TABLE users( 
&amp;nbsp;&amp;nbsp; // 각 행을 식별하는 고유 ID. 숫자가 자동으로 1씩 증가하며 기본키(PRIMARY KEY)로 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;id INT AUTO_INCREMENT PRIMARY KEY, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp; // 사용자 아이디를 저장, 최대 30자까지 입력 가능하며 중복될 수 없고(UNIQUE) 빈 값도 허용하지 않음(NOT NULL).
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;username VARCHAR(30) UNIQUE NOT NULL, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;// 이메일 주소를 저장. 최대 100자까지 입력 가능하며 빈 값을 허용하지 않음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;email VARCHAR(100) NOT NULL, 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;// 암호화된 비밀번호를 저장. 최대 100자까지 입력 가능하며 빈 값을 허용하지 않음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password VARCHAR(100) NOT NULL 
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3.2. mysql_membership.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import sys  # 시스템 관련 파라미터와 함수를 사용하기 위해 임포트
import mysql.connector as mc  # MySQL 데이터베이스 연결을 위한 라이브러리 임포트
# PySide6 GUI 위젯 및 레이아웃 클래스들을 임포트
from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QFormLayout,
    QMessageBox,
    QTabWidget
)


class MainWindow(QWidget):  # QWidget을 상속받는 메인 윈도우 클래스 정의

    def __init__(self):  # 클래스 생성자 정의
        super().__init__()  # 부모 클래스(QWidget)의 생성자 호출

        self.setWindowTitle(&quot;회원가입 / 로그인&quot;)  # 윈도우 창 제목 설정

        tabs = QTabWidget()  # 탭 전환을 위한 QTabWidget 객체 생성

        tabs.addTab(self.build_signup_tab(), &quot;회원가입&quot;)  # 회원가입 탭 추가
        tabs.addTab(self.build_login_tab(), &quot;로그인&quot;)  # 로그인 탭 추가

        layout = QVBoxLayout(self)  # 메인 윈도우용 수직 레이아웃 생성
        layout.addWidget(tabs)  # 레이아웃에 탭 위젯 추가


    # 회원가입 화면
    # ===============

    def build_signup_tab(self):  # 회원가입 탭 화면을 구성하는 메서드

        tab = QWidget()  # 탭으로 사용할 위젯 객체 생성

        # 입력 필드(텍스트 박스) 생성
        self.signup_username_edit = QLineEdit()
        self.signup_email_edit = QLineEdit()
        self.signup_password_edit = QLineEdit()

        self.signup_password_edit.setEchoMode(QLineEdit.Password)  # 비밀번호 입력 시 문자 숨김 처리

        form = QFormLayout()  # 라벨과 입력 필드를 나란히 배치하는 폼 레이아웃 생성

        # 폼 레이아웃에 행 추가
        form.addRow(&quot;아이디&quot;, self.signup_username_edit)
        form.addRow(&quot;이메일&quot;, self.signup_email_edit)
        form.addRow(&quot;비밀번호&quot;, self.signup_password_edit)

        btn_signup = QPushButton(&quot;가입하기&quot;)  # 회원가입 버튼 생성

        self.signup_result_label = QLabel(&quot;&quot;)  # 결과 메시지를 표시할 라벨 생성

        layout = QVBoxLayout(tab)  # 탭 위젯용 수직 레이아웃 생성

        # 레이아웃에 위젯들 배치
        layout.addLayout(form)
        layout.addWidget(btn_signup)
        layout.addWidget(self.signup_result_label)

        btn_signup.clicked.connect(self.signup)  # 버튼 클릭 시 signup 메서드 실행되도록 연결

        return tab  # 구성된 탭 위젯 반환


    # 로그인 화면
    # ===============

    def build_login_tab(self):  # 로그인 탭 화면을 구성하는 메서드

        tab = QWidget()  # 탭으로 사용할 위젯 객체 생성

        # 입력 필드 생성
        self.login_username_edit = QLineEdit()
        self.login_password_edit = QLineEdit()

        self.login_password_edit.setEchoMode(QLineEdit.Password)  # 비밀번호 입력 시 문자 숨김 처리

        form = QFormLayout()  # 폼 레이아웃 생성

        # 폼 레이아웃에 행 추가
        form.addRow(&quot;아이디&quot;, self.login_username_edit)
        form.addRow(&quot;비밀번호&quot;, self.login_password_edit)

        btn_login = QPushButton(&quot;로그인&quot;)  # 로그인 버튼 생성

        self.login_result_label = QLabel(&quot;&quot;)  # 결과 메시지를 표시할 라벨 생성

        layout = QVBoxLayout(tab)  # 탭 위젯용 수직 레이아웃 생성

        # 레이아웃에 위젯들 배치
        layout.addLayout(form)
        layout.addWidget(btn_login)
        layout.addWidget(self.login_result_label)

        btn_login.clicked.connect(self.login)  # 버튼 클릭 시 login 메서드 실행되도록 연결

        return tab  # 구성된 탭 위젯 반환


    # 회원가입 로직
    # ===========

    def signup(self):  # 회원가입 로직 처리 메서드

        # 입력 필드에서 텍스트 추출 및 앞뒤 공백 제거
        username = self.signup_username_edit.text().strip()
        email = self.signup_email_edit.text().strip()
        password = self.signup_password_edit.text()

        # 빈칸 검사
        if not username or not email or not password:
            QMessageBox.warning(
                self,
                &quot;입력 오류&quot;,
                &quot;아이디, 이메일, 비밀번호를 모두 입력하세요.&quot;
            )
            return

        # 이메일 형식 검사
        if &quot;@&quot; not in email:
            QMessageBox.warning(
                self,
                &quot;이메일 오류&quot;,
                &quot;올바른 이메일 형식이 아닙니다.&quot;
            )
            return

        conn = None

        try:
            # MySQL 데이터베이스 연결 시도
            conn = mc.connect(
                host=&quot;localhost&quot;,
                user=&quot;root&quot;,
                password=&quot;비밀번호&quot;,  # 실제 DB 비밀번호로 수정 필요
                database=&quot;pyqtdb&quot;
            )

            cursor = conn.cursor()

            # 1. 아이디 중복 확인
            sql_check = &quot;&quot;&quot;
            SELECT id FROM users WHERE username=%s
            &quot;&quot;&quot;
            cursor.execute(sql_check, (username,))

            if cursor.fetchone():
                self.signup_result_label.setText(&quot;이미 존재하는 아이디입니다.&quot;)
                return

            # 2. 회원 저장 (에러 발생 구문 수정 및 추가)
            sql_insert = &quot;&quot;&quot;
            INSERT INTO users (username, email, password) 
            VALUES (%s, %s, %s)
            &quot;&quot;&quot;
            cursor.execute(sql_insert, (username, email, password))
            conn.commit()  # 데이터베이스에 최종 반영

            self.signup_result_label.setText(&quot;회원가입이 완료되었습니다!&quot;)
            
            # 가입 성공 후 입력 필드 초기화
            self.signup_username_edit.clear()
            self.signup_email_edit.clear()
            self.signup_password_edit.clear()

        except mc.Error as e:
            # 데이터베이스 관련 오류 예외 처리
            QMessageBox.critical(self, &quot;DB 오류&quot;, f&quot;데이터베이스 에러 발생: {e}&quot;)
            self.signup_result_label.setText(&quot;회원가입 실패&quot;)
            
        finally:
            # 에러 발생 여부와 상관없이 연결 닫기
            if conn and conn.is_connected():
                cursor.close()
                conn.close()


    # 로그인 로직
    # ===========

    def login(self):  # 로그인 로직 처리 메서드
        username = self.login_username_edit.text().strip()
        password = self.login_password_edit.text()

        if not username or not password:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;아이디와 비밀번호를 모두 입력하세요.&quot;)
            return

        conn = None

        try:
            conn = mc.connect(
                host=&quot;localhost&quot;,
                user=&quot;root&quot;,
                password=&quot;비밀번호&quot;,  # 실제 DB 비밀번호로 수정 필요
                database=&quot;pyqtdb&quot;
            )
            cursor = conn.cursor()

            # 아이디와 비밀번호가 일치하는지 확인
            sql = &quot;&quot;&quot;
            SELECT id FROM users WHERE username=%s AND password=%s
            &quot;&quot;&quot;
            cursor.execute(sql, (username, password))
            user = cursor.fetchone()

            if user:
                self.login_result_label.setText(&quot;로그인 성공!&quot;)
                QMessageBox.information(self, &quot;성공&quot;, f&quot;{username}님, 환영합니다!&quot;)
            else:
                self.login_result_label.setText(&quot;아이디 또는 비밀번호가 틀렸습니다.&quot;)

        except mc.Error as e:
            QMessageBox.critical(self, &quot;DB 오류&quot;, f&quot;데이터베이스 에러 발생: {e}&quot;)
            self.login_result_label.setText(&quot;로그인 실패&quot;)
            
        finally:
            if conn and conn.is_connected():
                cursor.close()
                conn.close()


# 프로그램 실행부
# =============
if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Ⅱ 방안&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.1. &lt;/b&gt;&lt;b&gt;SQLProject&lt;/b&gt;&lt;b&gt; / requirements.txt&amp;nbsp; &amp;nbsp; &amp;nbsp;설명서(실행 메뉴얼)&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; * LLM응용&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# SQLProject &amp;mdash; PySide6 + MySQL 회원관리 프로그램

# 회원가입/로그인을 MySQL 서버와 연동하는 기본 GUI 프로그램

### 프로젝트 구조
```
SQLProject/
├── main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 실행 진입점 (PySide6 GUI)
├── db.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # MySQL 연동 함수 (회원가입/로그인/중복확인)
├── config.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# DB 접속 정보
├── requirements.txt
├── sql/
│&amp;nbsp;&amp;nbsp; └── create_table.sql&amp;nbsp;&amp;nbsp; # DB / 테이블 생성 스크립트
└── README.md
```
### 1. 준비 (MySQL)
1. MySQL 서버가 로컬(또는 원격)에 설치되어 실행 중
2. MySQL Workbench 또는 CLI에서 `sql/create_table.sql`을 실행하여
&amp;nbsp;&amp;nbsp; 데이터베이스(`sql_project_db`)와 `users` 테이블을 생성

터미널창:$&amp;nbsp;&amp;nbsp;mysql -u root -p &amp;lt; sql/create_table.sql

## 2. config.py 설정
`config.py`를 열어 본인 MySQL 환경에 맞게 수정

```python
DB_CONFIG = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;host&quot;: &quot;localhost&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;port&quot;: 3306,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;user&quot;: &quot;wolf&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;password&quot;: &quot;본인비번_1234&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;database&quot;: &quot;sql_project_db&quot;
}
```

## 3. PyCharm에서 실행하는 방법
1. PyCharm에서 `SQLProject` 폴더를 **Open** 하여 프로젝트로 열기
2. `File &amp;gt; Settings &amp;gt; Project: SQLProject &amp;gt; Python Interpreter`에서
&amp;nbsp;&amp;nbsp; 가상환경(venv)을 새로 만들거나 기존 인터프리터를 선택
3. 터미널(PyCharm 하단 Terminal 탭)에서 아래 명령으로 패키지를 설치
&amp;gt;&amp;gt;&amp;gt;&amp;nbsp;&amp;nbsp;pip install -r requirements.txt

4. `main.py`를 우클릭 &amp;rarr; Run 'main' 실행
&amp;nbsp;&amp;nbsp; (또는 main.py를 연 상태에서 Shift+F10 / 실행 버튼 클릭)

## 4. 기능 설명
회원가입: 아이디/비밀번호/비밀번호 확인/이메일/이름 입력 후 가입 
아이디 중복 확인 : 가입 전 `SELECT COUNT(*) ... WHERE user_id = %s` 로 확인, 통과해야 가입 버튼 동작 
이메일 형식 검사 : `@`가 정확히 1개 포함되고 도메인에 `.`이 있는지 검사 
로그인 : 아이디로 조회 후 SHA-256 해시된 비밀번호 비교 
비밀번호 저장 : 평문 저장 대신 SHA-256 해시로 저장 (`hashlib`) 
비밀번호 찾기 : 아이디+이메일 일치 여부로 본인 확인 후, 통과 시에만 새 비밀번호 입력란 활성화 
회원정보 수정 : 아이디+현재 비밀번호로 로그인 인증 후, 기존 이메일/이름을 불러와 수정 (비밀번호는 입력 시에만 변경) 

### 탭 구성 (총 4개)
1. 회원가입
2. 로그인
3. 비밀번호 찾기 &amp;mdash; 2단계(본인 확인 &amp;rarr; 재설정) 구조, 확인 전에는 새 비밀번호 입력란이 비활성화됨
4. 회원정보 수정 &amp;mdash; 2단계(로그인 인증 &amp;rarr; 수정) 구조, 인증 전에는 수정 입력란이 비활성화됨

## 5. 구현 규칙 (요구사항 반영)
- 모든 DB 함수는 `conn = None` 으로 시작한 뒤 `try / except / finally` 구조 사용
- `finally` 블록에서 `cursor.close()`, `conn.close()`를 반드시 수행하여 커넥션 누수를 방지
- 모든 SQL은 **Parameter Binding(`%s`)** 사용, f-string이나 `+` 연산으로 SQL을 직접 조합하지 않음
&amp;nbsp;&amp;nbsp;(SQL Injection 방지)
- GUI(`main.py`)와 DB 로직(`db.py`)을 분리하여 유지보수성 확보

## 6. 문제 해결 (Troubleshooting)
- `Access denied for user` 오류: `config.py`의 `user`/`password` 값 확인하기
- `Unknown database 'sql_project_db'` 오류: `sql/create_table.sql`을 먼저 실행했는지 확인하기
- `ModuleNotFoundError: No module named 'PySide6'`: `pip install -r requirements.txt`를 실행했는지 확인하기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.2. &lt;/b&gt;&lt;b&gt;SQLProject / main.py&amp;nbsp; &amp;nbsp;// 실행 진입점&lt;/b&gt;&lt;b&gt;&amp;nbsp;(PySide6&amp;nbsp;GUI)&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# main.py
# PySide6 기반 회원관리 프로그램 (회원가입 / 로그인)
# PyCharm에서 바로 실행 가능하도록 작성됨 (진입점: main.py)

import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QTabWidget, QMainWindow,
    QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,
    QFormLayout, QMessageBox
)
from PySide6.QtCore import Qt

import db


class SignupTab(QWidget):
    &quot;&quot;&quot;회원가입 탭&quot;&quot;&quot;

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_pw = QLineEdit()
        self.input_pw.setEchoMode(QLineEdit.Password)
        self.input_pw_confirm = QLineEdit()
        self.input_pw_confirm.setEchoMode(QLineEdit.Password)
        self.input_email = QLineEdit()
        self.input_name = QLineEdit()

        self.btn_check_id = QPushButton(&quot;아이디 중복 확인&quot;)
        self.btn_check_id.clicked.connect(self.check_duplicate_id)

        self.btn_signup = QPushButton(&quot;회원가입&quot;)
        self.btn_signup.clicked.connect(self.handle_signup)

        # 중복 확인을 통과했는지 여부를 추적하는 플래그
        self.id_checked = False

        id_row = QHBoxLayout()
        id_row.addWidget(self.input_id)
        id_row.addWidget(self.btn_check_id)

        form = QFormLayout()
        form.addRow(&quot;아이디&quot;, id_row)
        form.addRow(&quot;비밀번호&quot;, self.input_pw)
        form.addRow(&quot;비밀번호 확인&quot;, self.input_pw_confirm)
        form.addRow(&quot;이메일&quot;, self.input_email)
        form.addRow(&quot;이름&quot;, self.input_name)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_signup)
        layout.addStretch()
        self.setLayout(layout)

        # 아이디를 수정하면 다시 중복 확인을 받도록 초기화
        self.input_id.textChanged.connect(self.reset_id_check)

    def reset_id_check(self):
        self.id_checked = False

    def check_duplicate_id(self):
        user_id = self.input_id.text().strip()
        if not user_id:
            QMessageBox.warning(self, &quot;확인&quot;, &quot;아이디를 입력해 주세요.&quot;)
            return

        is_dup = db.is_id_duplicate(user_id)
        if is_dup:
            QMessageBox.warning(self, &quot;중복 확인&quot;, &quot;이미 사용 중인 아이디입니다.&quot;)
            self.id_checked = False
        else:
            QMessageBox.information(self, &quot;중복 확인&quot;, &quot;사용 가능한 아이디입니다.&quot;)
            self.id_checked = True

    def is_valid_email(self, email: str) -&amp;gt; bool:
        &quot;&quot;&quot;
        이메일 형식 검사 (요구사항: @ 포함 여부 검사)
        - '@'가 정확히 1개 포함되어 있어야 함
        - '@' 앞뒤로 문자열이 존재해야 함
        &quot;&quot;&quot;
        if email.count(&quot;@&quot;) != 1:
            return False
        local_part, domain_part = email.split(&quot;@&quot;)
        if not local_part or not domain_part:
            return False
        if &quot;.&quot; not in domain_part:
            return False
        return True

    def handle_signup(self):
        user_id = self.input_id.text().strip()
        password = self.input_pw.text()
        password_confirm = self.input_pw_confirm.text()
        email = self.input_email.text().strip()
        name = self.input_name.text().strip()

        # 1) 빈 값 검사
        if not (user_id and password and password_confirm and email and name):
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;모든 항목을 입력해 주세요.&quot;)
            return

        # 2) 아이디 중복 확인 여부
        if not self.id_checked:
            QMessageBox.warning(self, &quot;확인 필요&quot;, &quot;아이디 중복 확인을 먼저 진행해 주세요.&quot;)
            return

        # 3) 비밀번호 일치 확인
        if password != password_confirm:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;비밀번호가 일치하지 않습니다.&quot;)
            return

        # 4) 이메일 @ 형식 검사
        if not self.is_valid_email(email):
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;올바른 이메일 형식이 아닙니다. (예: user@example.com)&quot;)
            return

        # 5) DB에 회원가입 요청
        success, message = db.register_user(user_id, password, email, name)
        if success:
            QMessageBox.information(self, &quot;회원가입&quot;, message)
            self.clear_form()
        else:
            QMessageBox.critical(self, &quot;회원가입 실패&quot;, message)

    def clear_form(self):
        self.input_id.clear()
        self.input_pw.clear()
        self.input_pw_confirm.clear()
        self.input_email.clear()
        self.input_name.clear()
        self.id_checked = False


class LoginTab(QWidget):
    &quot;&quot;&quot;로그인 탭&quot;&quot;&quot;

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_pw = QLineEdit()
        self.input_pw.setEchoMode(QLineEdit.Password)

        self.btn_login = QPushButton(&quot;로그인&quot;)
        self.btn_login.clicked.connect(self.handle_login)

        # 엔터키로도 로그인되도록 설정
        self.input_pw.returnPressed.connect(self.handle_login)

        form = QFormLayout()
        form.addRow(&quot;아이디&quot;, self.input_id)
        form.addRow(&quot;비밀번호&quot;, self.input_pw)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_login)
        layout.addStretch()
        self.setLayout(layout)

    def handle_login(self):
        user_id = self.input_id.text().strip()
        password = self.input_pw.text()

        if not user_id or not password:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;아이디와 비밀번호를 입력해 주세요.&quot;)
            return

        success, message = db.login_user(user_id, password)
        if success:
            QMessageBox.information(self, &quot;로그인 성공&quot;, message)
        else:
            QMessageBox.critical(self, &quot;로그인 실패&quot;, message)


class ResetPasswordTab(QWidget):
    &quot;&quot;&quot;비밀번호 찾기(재설정) 탭
    1단계: 아이디 + 이메일로 본인 확인
    2단계: 확인 통과 시 새 비밀번호 입력란이 활성화되어 재설정 가능
    &quot;&quot;&quot;

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_email = QLineEdit()

        self.btn_verify = QPushButton(&quot;본인 확인&quot;)
        self.btn_verify.clicked.connect(self.handle_verify)

        self.input_new_pw = QLineEdit()
        self.input_new_pw.setEchoMode(QLineEdit.Password)
        self.input_new_pw_confirm = QLineEdit()
        self.input_new_pw_confirm.setEchoMode(QLineEdit.Password)

        self.btn_reset = QPushButton(&quot;비밀번호 재설정&quot;)
        self.btn_reset.clicked.connect(self.handle_reset)

        self.verified = False
        self.set_reset_enabled(False)

        form = QFormLayout()
        form.addRow(&quot;아이디&quot;, self.input_id)
        form.addRow(&quot;이메일&quot;, self.input_email)

        form2 = QFormLayout()
        form2.addRow(&quot;새 비밀번호&quot;, self.input_new_pw)
        form2.addRow(&quot;새 비밀번호 확인&quot;, self.input_new_pw_confirm)

        layout = QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(self.btn_verify)
        layout.addWidget(QLabel(&quot;― 본인 확인 후 아래에서 새 비밀번호를 설정하세요 ―&quot;))
        layout.addLayout(form2)
        layout.addWidget(self.btn_reset)
        layout.addStretch()
        self.setLayout(layout)

        self.input_id.textChanged.connect(self.reset_verification)
        self.input_email.textChanged.connect(self.reset_verification)

    def set_reset_enabled(self, enabled: bool):
        self.input_new_pw.setEnabled(enabled)
        self.input_new_pw_confirm.setEnabled(enabled)
        self.btn_reset.setEnabled(enabled)

    def reset_verification(self):
        self.verified = False
        self.set_reset_enabled(False)

    def handle_verify(self):
        user_id = self.input_id.text().strip()
        email = self.input_email.text().strip()

        if not user_id or not email:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;아이디와 이메일을 모두 입력해 주세요.&quot;)
            return

        ok = db.verify_id_email(user_id, email)
        if ok:
            QMessageBox.information(self, &quot;본인 확인&quot;, &quot;본인 확인이 완료되었습니다. 새 비밀번호를 입력해 주세요.&quot;)
            self.verified = True
            self.set_reset_enabled(True)
        else:
            QMessageBox.warning(self, &quot;본인 확인 실패&quot;, &quot;아이디와 이메일이 일치하지 않습니다.&quot;)
            self.verified = False
            self.set_reset_enabled(False)

    def handle_reset(self):
        if not self.verified:
            QMessageBox.warning(self, &quot;확인 필요&quot;, &quot;먼저 본인 확인을 진행해 주세요.&quot;)
            return

        user_id = self.input_id.text().strip()
        new_pw = self.input_new_pw.text()
        new_pw_confirm = self.input_new_pw_confirm.text()

        if not new_pw or not new_pw_confirm:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;새 비밀번호를 입력해 주세요.&quot;)
            return

        if new_pw != new_pw_confirm:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;새 비밀번호가 일치하지 않습니다.&quot;)
            return

        success, message = db.reset_password(user_id, new_pw)
        if success:
            QMessageBox.information(self, &quot;비밀번호 재설정&quot;, message)
            self.clear_form()
        else:
            QMessageBox.critical(self, &quot;재설정 실패&quot;, message)

    def clear_form(self):
        self.input_id.clear()
        self.input_email.clear()
        self.input_new_pw.clear()
        self.input_new_pw_confirm.clear()
        self.verified = False
        self.set_reset_enabled(False)


class EditProfileTab(QWidget):
    &quot;&quot;&quot;회원정보 수정 탭
    1단계: 아이디 + 현재 비밀번호로 로그인 인증
    2단계: 인증 통과 시 이메일/이름/비밀번호(선택) 수정 가능
    &quot;&quot;&quot;

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.input_id = QLineEdit()
        self.input_current_pw = QLineEdit()
        self.input_current_pw.setEchoMode(QLineEdit.Password)

        self.btn_auth = QPushButton(&quot;본인 인증&quot;)
        self.btn_auth.clicked.connect(self.handle_auth)

        self.input_email = QLineEdit()
        self.input_name = QLineEdit()
        self.input_new_pw = QLineEdit()
        self.input_new_pw.setEchoMode(QLineEdit.Password)
        self.input_new_pw.setPlaceholderText(&quot;변경하지 않으려면 비워두세요&quot;)

        self.btn_update = QPushButton(&quot;정보 수정&quot;)
        self.btn_update.clicked.connect(self.handle_update)

        self.authenticated = False
        self.current_user_id = None
        self.set_edit_enabled(False)

        auth_form = QFormLayout()
        auth_form.addRow(&quot;아이디&quot;, self.input_id)
        auth_form.addRow(&quot;현재 비밀번호&quot;, self.input_current_pw)

        edit_form = QFormLayout()
        edit_form.addRow(&quot;이메일&quot;, self.input_email)
        edit_form.addRow(&quot;이름&quot;, self.input_name)
        edit_form.addRow(&quot;새 비밀번호&quot;, self.input_new_pw)

        layout = QVBoxLayout()
        layout.addLayout(auth_form)
        layout.addWidget(self.btn_auth)
        layout.addWidget(QLabel(&quot;― 본인 인증 후 아래 정보를 수정하세요 ―&quot;))
        layout.addLayout(edit_form)
        layout.addWidget(self.btn_update)
        layout.addStretch()
        self.setLayout(layout)

        self.input_id.textChanged.connect(self.reset_auth)
        self.input_current_pw.textChanged.connect(self.reset_auth)

    def set_edit_enabled(self, enabled: bool):
        self.input_email.setEnabled(enabled)
        self.input_name.setEnabled(enabled)
        self.input_new_pw.setEnabled(enabled)
        self.btn_update.setEnabled(enabled)

    def reset_auth(self):
        self.authenticated = False
        self.current_user_id = None
        self.set_edit_enabled(False)
        self.input_email.clear()
        self.input_name.clear()

    def handle_auth(self):
        user_id = self.input_id.text().strip()
        password = self.input_current_pw.text()

        if not user_id or not password:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;아이디와 현재 비밀번호를 입력해 주세요.&quot;)
            return

        success, message = db.login_user(user_id, password)
        if not success:
            QMessageBox.critical(self, &quot;인증 실패&quot;, message)
            self.reset_auth()
            return

        ok, result = db.get_user_info(user_id)
        if not ok:
            QMessageBox.critical(self, &quot;조회 실패&quot;, result)
            return

        self.authenticated = True
        self.current_user_id = user_id
        self.input_email.setText(result[&quot;email&quot;])
        self.input_name.setText(result[&quot;name&quot;])
        self.set_edit_enabled(True)
        QMessageBox.information(self, &quot;본인 인증&quot;, &quot;인증되었습니다. 정보를 수정할 수 있습니다.&quot;)

    def is_valid_email(self, email: str) -&amp;gt; bool:
        if email.count(&quot;@&quot;) != 1:
            return False
        local_part, domain_part = email.split(&quot;@&quot;)
        if not local_part or not domain_part:
            return False
        if &quot;.&quot; not in domain_part:
            return False
        return True

    def handle_update(self):
        if not self.authenticated or not self.current_user_id:
            QMessageBox.warning(self, &quot;인증 필요&quot;, &quot;먼저 본인 인증을 진행해 주세요.&quot;)
            return

        email = self.input_email.text().strip()
        name = self.input_name.text().strip()
        new_pw = self.input_new_pw.text()  # 비워두면 비밀번호는 변경하지 않음

        if not email or not name:
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;이메일과 이름을 입력해 주세요.&quot;)
            return

        if not self.is_valid_email(email):
            QMessageBox.warning(self, &quot;입력 오류&quot;, &quot;올바른 이메일 형식이 아닙니다. (예: user@example.com)&quot;)
            return

        success, message = db.update_user_info(self.current_user_id, email, name, new_pw or None)
        if success:
            QMessageBox.information(self, &quot;회원정보 수정&quot;, message)
            self.input_new_pw.clear()
        else:
            QMessageBox.critical(self, &quot;수정 실패&quot;, message)


class MainWindow(QMainWindow):
    &quot;&quot;&quot;메인 윈도우: QTabWidget으로 회원가입/로그인/비밀번호 찾기/회원정보 수정 탭 구성&quot;&quot;&quot;

    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;SQLProject - 회원관리 시스템&quot;)
        self.setFixedSize(440, 420)

        tabs = QTabWidget()
        tabs.addTab(SignupTab(), &quot;회원가입&quot;)
        tabs.addTab(LoginTab(), &quot;로그인&quot;)
        tabs.addTab(ResetPasswordTab(), &quot;비밀번호 찾기&quot;)
        tabs.addTab(EditProfileTab(), &quot;회원정보 수정&quot;)

        self.setCentralWidget(tabs)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.3.&amp;nbsp;&lt;/b&gt;&lt;b&gt;SQLProject / db.py&amp;nbsp; &amp;nbsp; &amp;nbsp;// &lt;/b&gt;&lt;b&gt;MySQL&amp;nbsp;연동&amp;nbsp;함수&amp;nbsp;(회원가입/로그인/중복확인)&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# db.py
# MySQL 연동을 담당하는 모듈
# - mysql-connector-python 사용
# - 모든 SQL 실행 함수는 conn = None 으로 시작
# - try / except / finally 구조로 연결 예외 처리 및 반드시 close() 수행
# - SQL 인젝션 방지를 위해 문자열 직접 조합(f-string, + 연산 등) 금지
#&amp;nbsp;&amp;nbsp; -&amp;gt; 반드시 %s Parameter Binding 사용

import hashlib
import mysql.connector
from mysql.connector import Error
from config import DB_CONFIG


def get_connection():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MySQL 서버에 연결하고 connection 객체를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;실패 시 None을 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = mysql.connector.connect(**DB_CONFIG)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return conn
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 연결 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None


def hash_password(password: str) -&amp;gt; str:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;비밀번호를 SHA-256으로 해시하여 저장한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;평문 비밀번호를 DB에 그대로 저장하지 않기 위함이다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return hashlib.sha256(password.encode(&quot;utf-8&quot;)).hexdigest()


def is_id_duplicate(user_id: str) -&amp;gt; bool:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;아이디 중복 여부를 확인한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;이미 존재하면 True, 존재하지 않으면 False를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;오류 발생 시 안전하게 True를 반환하여 가입을 막는다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True&amp;nbsp;&amp;nbsp;# 연결 실패 시 안전하게 중복으로 처리

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT COUNT(*) FROM users WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (user_id,))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(count,) = cursor.fetchone()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return count &amp;gt; 0

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[중복 확인 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def register_user(user_id: str, password: str, email: str, name: str) -&amp;gt; tuple[bool, str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;회원가입을 수행한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;성공 시 (True, &quot;성공 메시지&quot;), 실패 시 (False, &quot;실패 사유&quot;)를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INSERT INTO users (user_id, password, email, name)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VALUES (%s, %s, %s, %s)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hashed_pw = hash_password(password)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (user_id, hashed_pw, email, name))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, &quot;회원가입이 완료되었습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UNIQUE 제약 위반 등 DB 오류 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[회원가입 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;회원가입 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def find_id_by_email(email: str) -&amp;gt; tuple[bool, str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;이메일로 아이디를 찾는다. (아이디 찾기용 - 필요 시 사용)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;성공 시 (True, &quot;user_id&quot;), 실패 시 (False, &quot;실패 사유&quot;)를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT user_id FROM users WHERE email = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (email,))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;row = cursor.fetchone()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if row is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;해당 이메일로 가입된 아이디가 없습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, row[0]

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[아이디 찾기 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;조회 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def verify_id_email(user_id: str, email: str) -&amp;gt; bool:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;비밀번호 찾기(재설정) 전 본인 확인 단계.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;아이디와 이메일이 DB에 등록된 정보와 일치하면 True를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT COUNT(*) FROM users WHERE user_id = %s AND email = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (user_id, email))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(count,) = cursor.fetchone()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return count &amp;gt; 0

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[본인 확인 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def reset_password(user_id: str, new_password: str) -&amp;gt; tuple[bool, str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;본인 확인(verify_id_email)을 통과한 사용자의 비밀번호를 재설정한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;UPDATE users SET password = %s WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hashed_pw = hash_password(new_password)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (hashed_pw, user_id))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor.rowcount == 0:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;비밀번호 변경에 실패했습니다. 아이디를 확인해 주세요.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, &quot;비밀번호가 성공적으로 변경되었습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[비밀번호 재설정 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;비밀번호 재설정 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def get_user_info(user_id: str) -&amp;gt; tuple[bool, dict | str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;회원정보 수정 화면에 기존 정보를 채워주기 위해 사용자 정보를 조회한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;성공 시 (True, {&quot;email&quot;:..., &quot;name&quot;:...}), 실패 시 (False, &quot;실패 사유&quot;)를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT email, name FROM users WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (user_id,))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;row = cursor.fetchone()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if row is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;존재하지 않는 아이디입니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;email, name = row
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, {&quot;email&quot;: email, &quot;name&quot;: name}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[회원정보 조회 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;조회 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def update_user_info(user_id: str, email: str, name: str, new_password: str | None = None) -&amp;gt; tuple[bool, str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;회원정보(이메일, 이름, [선택]비밀번호)를 수정한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;new_password가 None이거나 빈 문자열이면 비밀번호는 변경하지 않는다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if new_password:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;UPDATE users SET email = %s, name = %s, password = %s WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hashed_pw = hash_password(new_password)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;params = (email, name, hashed_pw, user_id)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;UPDATE users SET email = %s, name = %s WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;params = (email, name, user_id)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, params)&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, &quot;회원정보가 수정되었습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[회원정보 수정 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;회원정보 수정 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()


def login_user(user_id: str, password: str) -&amp;gt; tuple[bool, str]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;로그인을 수행한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;아이디/비밀번호가 일치하면 (True, &quot;환영 메시지&quot;),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;일치하지 않으면 (False, &quot;실패 사유&quot;)를 반환한다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;데이터베이스 서버에 연결할 수 없습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor = conn.cursor()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT password, name FROM users WHERE user_id = %s&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (user_id,))&amp;nbsp;&amp;nbsp;# Parameter Binding
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;row = cursor.fetchone()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if row is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;존재하지 않는 아이디입니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stored_password, name = row
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hashed_input_pw = hash_password(password)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if stored_password == hashed_input_pw:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True, f&quot;{name}님, 환영합니다!&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, &quot;비밀번호가 일치하지 않습니다.&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Error as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[로그인 오류] {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False, f&quot;로그인 중 오류가 발생했습니다: {e}&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if cursor is not None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn is not None and conn.is_connected():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.4.&amp;nbsp;&lt;/b&gt;&lt;b&gt;SQLProject / config.py&amp;nbsp; // &lt;/b&gt;&lt;b&gt;&amp;nbsp;#&amp;nbsp;DB&amp;nbsp;접속&amp;nbsp;정보&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# config.py
# MySQL 서버 접속 정보
# 본인 환경에 맞게 값을 수정해서 사용
&quot;&quot;&quot;
DB_CONFIG = {
    &quot;host&quot;: &quot;localhost&quot;,       # MySQL 서버 주소
    &quot;port&quot;: 3306,               # MySQL 포트 (기본 3306)
    &quot;user&quot;: &quot;root&quot;,             # MySQL 사용자명
    &quot;password&quot;: &quot;1234&quot;,         # MySQL 비밀번호 (본인 환경에 맞게 수정)
    &quot;database&quot;: &quot;sql_project_db&quot;  # 사용할 데이터베이스명
}
&quot;&quot;&quot;

DB_CONFIG = {
    &quot;host&quot;: &quot;127.0.0.1&quot;,       # MySQL 서버 주소
    &quot;port&quot;: 3306,               # MySQL 포트 (기본 3306)
    &quot;user&quot;: &quot;root&quot;,             # MySQL 사용자명
    &quot;password&quot;: &quot;1234&quot;,         # MySQL 비밀번호 (본인 환경에 맞게 수정)
    &quot;database&quot;: &quot;sql_project_db&quot;  # 사용할 데이터베이스명
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.5.&amp;nbsp;&lt;/b&gt;&lt;b&gt;SQLProject / sql / create_table.sql&amp;nbsp; &amp;nbsp; &lt;/b&gt;&lt;b&gt;&amp;nbsp;// DB / 테이블 생성 스크립트&lt;br /&gt;&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// create_table.sql
// SQLProject 회원관리용 데이터베이스 및 테이블 생성 스크립트

// sql_project_db 데이터베이스가 존재하지 않을 때만 새로 생성
// 데이터베이스 기본 문자셋은 이모지 저장이 가능한 utf8mb4로, 정렬 규칙은 utf8mb4_general_ci로 설정
CREATE DATABASE IF NOT EXISTS sql_project_db
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;DEFAULT CHARACTER SET utf8mb4
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;COLLATE utf8mb4_general_ci;

// 이후 진행할 작업(테이블 생성 등)을 위해 sql_project_db 데이터베이스를 선택
USE sql_project_db;

// users 테이블이 존재하지 않을 때만 회원 정보를 저장할 테이블을 생성
CREATE TABLE IF NOT EXISTS users (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;id&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INT AUTO_INCREMENT PRIMARY KEY, // 고유 식별자 복수 일련번호 (자동 증가, 기본키)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_id&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; VARCHAR(50)&amp;nbsp;&amp;nbsp;NOT NULL UNIQUE,&amp;nbsp;&amp;nbsp;// 로그인 아이디 (중복 불가)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VARCHAR(255) NOT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 비밀번호 (해시 저장)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;email&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; VARCHAR(100) NOT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 이메일
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VARCHAR(50)&amp;nbsp;&amp;nbsp;NOT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 이름
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;created_at&amp;nbsp;&amp;nbsp;TIMESTAMP DEFAULT CURRENT_TIMESTAMP // 계정 생성 일시 (데이터 삽입 시 현재 시간 자동 입력)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;997&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MgPQD/dJMcahkSCP3/TPXvkhwQmfibi93lcnAGDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MgPQD/dJMcahkSCP3/TPXvkhwQmfibi93lcnAGDK/img.png&quot; data-alt=&quot;MySQL creat 테이블 생성 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MgPQD/dJMcahkSCP3/TPXvkhwQmfibi93lcnAGDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMgPQD%2FdJMcahkSCP3%2FTPXvkhwQmfibi93lcnAGDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;997&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;997&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MySQL creat 테이블 생성 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/oWSG7/dJMcaf1yl2P/Yuuuwyim9bmS4ZtT0IqJv1/SQLProject.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;SQLProject.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/c# c++</category>
      <category>C #mysql #SQL #예제 #실습 #개인학습 #과제 #DB # membership</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/89</guid>
      <comments>https://goodjop79.tistory.com/89#entry89comment</comments>
      <pubDate>Sun, 12 Jul 2026 23:36:30 +0900</pubDate>
    </item>
    <item>
      <title>D+66[AI] Claude Code 환경설정 및 이해, 예제 실습</title>
      <link>https://goodjop79.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;7월&amp;nbsp;10일(금)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ&amp;nbsp;주&amp;nbsp;제&amp;nbsp;:&amp;nbsp;Claude&amp;nbsp;Code&amp;nbsp;환경설정&amp;nbsp;및&amp;nbsp;실습을&amp;nbsp;통한&amp;nbsp;활용&amp;nbsp;이해&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.&amp;nbsp;Claude&amp;nbsp;Code&amp;nbsp;설치&amp;nbsp;및&amp;nbsp;환경&amp;nbsp;설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. Claude Code 설치하기 (터미널:$프롬프트 입력창)&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783657913971&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; :$  curl -fsSL https://claude.ai/install.sh | bash     // 설치 하기
 :$  ~/.local/bin/claude    //설치 위치 확인 
 :$  claude --version      //설치 버젼 확인하기
 :$  claude               //실행하기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 10-10-02.png&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y9Hfp/dJMcabSmLwT/MEirwDJOKiDAbMYlfS8xb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y9Hfp/dJMcabSmLwT/MEirwDJOKiDAbMYlfS8xb0/img.png&quot; data-alt=&quot;Claude Code 설치 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y9Hfp/dJMcabSmLwT/MEirwDJOKiDAbMYlfS8xb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy9Hfp%2FdJMcabSmLwT%2FMEirwDJOKiDAbMYlfS8xb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;335&quot; data-filename=&quot;스크린샷 2026-07-09 10-10-02.png&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Claude Code 설치 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b87sSv/dJMcacRgh1d/txcKmwZpaDGIPTqqgB670k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b87sSv/dJMcacRgh1d/txcKmwZpaDGIPTqqgB670k/img.png&quot; data-alt=&quot;Claude Code 실행시 첫 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b87sSv/dJMcacRgh1d/txcKmwZpaDGIPTqqgB670k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb87sSv%2FdJMcacRgh1d%2FtxcKmwZpaDGIPTqqgB670k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;659&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Claude Code 실행시 첫 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 10-23-38.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjqbGt/dJMcaixfGYZ/ON9M1ckxU6ZkvC16eVXKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjqbGt/dJMcaixfGYZ/ON9M1ckxU6ZkvC16eVXKi1/img.png&quot; data-alt=&quot;예제 실습 화면 (디렉토리 이동 : ~/claude-practice)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjqbGt/dJMcaixfGYZ/ON9M1ckxU6ZkvC16eVXKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjqbGt%2FdJMcaixfGYZ%2FON9M1ckxU6ZkvC16eVXKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;842&quot; height=&quot;654&quot; data-filename=&quot;스크린샷 2026-07-09 10-23-38.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예제 실습 화면 (디렉토리 이동 : ~/claude-practice)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 10-27-15.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;895&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnjkbN/dJMcaa65GDV/W8AHfVuNdbAPHJ2pehcbk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnjkbN/dJMcaa65GDV/W8AHfVuNdbAPHJ2pehcbk0/img.png&quot; data-alt=&quot;클로드와 채팅화면 (개발자가 핵심적으료 요청사항에 포함할 사항)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnjkbN/dJMcaa65GDV/W8AHfVuNdbAPHJ2pehcbk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnjkbN%2FdJMcaa65GDV%2FW8AHfVuNdbAPHJ2pehcbk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;832&quot; height=&quot;911&quot; data-filename=&quot;스크린샷 2026-07-09 10-27-15.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;895&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클로드와 채팅화면 (개발자가 핵심적으료 요청사항에 포함할 사항)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 10-27-33.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;895&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6SFUQ/dJMcagzosUB/uQ4S3fAS1lejMy4EwReZLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6SFUQ/dJMcagzosUB/uQ4S3fAS1lejMy4EwReZLk/img.png&quot; data-alt=&quot;클로드와 채팅화면 (개발자가 핵심적으료 요청사항에 포함할 사항)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6SFUQ/dJMcagzosUB/uQ4S3fAS1lejMy4EwReZLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6SFUQ%2FdJMcagzosUB%2FuQ4S3fAS1lejMy4EwReZLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;825&quot; height=&quot;904&quot; data-filename=&quot;스크린샷 2026-07-09 10-27-33.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;895&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클로드와 채팅화면 (개발자가 핵심적으료 요청사항에 포함할 사항)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 10-56-34.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;825&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLneaU/dJMcafUQCC2/TDkO6INkdkW7rQHqvgp3Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLneaU/dJMcafUQCC2/TDkO6INkdkW7rQHqvgp3Yk/img.png&quot; data-alt=&quot;클로드와 채팅화면 -- &amp;quot;대한민국 3대 메가 프로젝트- 출처 포함&amp;quot;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLneaU/dJMcafUQCC2/TDkO6INkdkW7rQHqvgp3Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLneaU%2FdJMcafUQCC2%2FTDkO6INkdkW7rQHqvgp3Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;819&quot; height=&quot;881&quot; data-filename=&quot;스크린샷 2026-07-09 10-56-34.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;825&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클로드와 채팅화면 -- &quot;대한민국 3대 메가 프로젝트- 출처 포함&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 12-22-51.png&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxuh2l/dJMcagM09iL/ZStwGaFcBNkEkPVzKCucd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxuh2l/dJMcagM09iL/ZStwGaFcBNkEkPVzKCucd1/img.png&quot; data-alt=&quot;클로드 채팅 화면 : 굳이 파일을 열지 않더라도 내용을 표시해 준다. &amp;quot;대상 파일 위치를 찾은후, 대상 파일안에 내용들을 출력해 달라는 내용&amp;quot;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxuh2l/dJMcagM09iL/ZStwGaFcBNkEkPVzKCucd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxuh2l%2FdJMcagM09iL%2FZStwGaFcBNkEkPVzKCucd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;825&quot; height=&quot;832&quot; data-filename=&quot;스크린샷 2026-07-09 12-22-51.png&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클로드 채팅 화면 : 굳이 파일을 열지 않더라도 내용을 표시해 준다. &quot;대상 파일 위치를 찾은후, 대상 파일안에 내용들을 출력해 달라는 내용&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;2. Claud Code 활용하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 터미널창 프롬프트창에서 &quot;자연어&quot;로 명령하면 .py 파일을 생성하고 소스코드까지 작성하여 .py로 저장까지 한다.&lt;br /&gt;- AI가 인간처럼 생각을 행동으로 옮기는 것은 다소 제한적, 스킬이나, 환경설정에서 지침 등 사전에 설정하여 두고 통제가 필요하다.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 임베디드란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 개발은 &lt;b&gt;가전제품, 자동차, 의료기기 등 특정 하드웨어 기기 내에 내장된 마이크로프로세서가 특정 목적의 기능만 수행&lt;/b&gt;하도록 &lt;b&gt;제어하는 소프트웨어 개발&lt;/b&gt;이다. 하드웨어와 밀접하게 결합되며, &lt;b&gt;제한된 자원 안에서 최적화 및 안정성을 갖추는 것이 핵심&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 기타 생각해 볼 사항들&lt;/b&gt;&lt;br /&gt;- Claude s/w 개발 방법론이 아직은 명확한게 없다.&lt;br /&gt;-&amp;nbsp;파이참&amp;nbsp;환경에서&amp;nbsp;개발을&amp;nbsp;하되&amp;nbsp;클로드에서&amp;nbsp;바로&amp;nbsp;활용&amp;nbsp;실습한다.&lt;br /&gt;- 교육과정이 종료 후 개인 프로젝트를 계속 할수 있는가? 개인프로젝트 어떻게? 무엇을?&amp;nbsp;&lt;br /&gt;-&amp;nbsp;Claude.ai&amp;nbsp;를&amp;nbsp;사용할&amp;nbsp;때는&amp;nbsp;복붙을&amp;nbsp;해서&amp;nbsp;활용했지만,&amp;nbsp;하네스엔지니어링에서는&amp;nbsp;Claude&amp;nbsp;Code는&amp;nbsp;터미널에서&amp;nbsp;작업을&amp;nbsp;맡긴다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;* 컨텍스트(=메뉴얼) 하네스는 어떻게 프로그램 개발 행동 작업을 할 것인가?&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;4.&amp;nbsp;에이전트&amp;nbsp;MCP&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;MCP(Model&amp;nbsp;Context&amp;nbsp;Protocol)는&amp;nbsp;대형언어모델(LLM)이&amp;nbsp;외부&amp;nbsp;데이터나&amp;nbsp;앱과&amp;nbsp;표준화된&amp;nbsp;방식으로&amp;nbsp;소통할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;엔트로픽(Anthropic)이&amp;nbsp;발표한&amp;nbsp;오픈&amp;nbsp;소스&amp;nbsp;연결&amp;nbsp;표준&amp;nbsp;프로토콜,&amp;nbsp;AI&amp;nbsp;시대의&amp;nbsp;'USB-C&amp;nbsp;포트'이다.&lt;br /&gt;가.&amp;nbsp; 핵심 요약 표준화된 연동 :&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;과거에는 서비스마다 API를 따로 개발해야 했으나, 지금은 &lt;/span&gt;&lt;b&gt;MCP를 통해 하나의 통일된 규격으로 &lt;/b&gt;&lt;b&gt;&amp;nbsp; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;모든 외부 앱&lt;/b&gt;&lt;b&gt;&amp;middot;DB를 LLM에 연결&lt;/b&gt;&lt;br /&gt;나. 실시간 데이터 접근: AI가 슬랙, 구글 캘린더, 기업 내부 DB 등의 최신 컨텍스트를 실시간으로 가져와 활용&lt;br /&gt;다. AI 에이전트 자동화: AI가 단순히 답변하는 것을 넘어, 외부 도구를 직접 실행하여 복잡한 업무를 스스로 처리하도록 돕는다&lt;br /&gt;라. 3대 구성 요소&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;MCP 호스트 : 클로드, 챗GPT 등 사용자가 쓰는 AI 서비스&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;MCP 클라이언트: 호스트 내부에서 서버와의 연결을 중재&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;MCP 서버: 외부 앱의 기능을 AI가 쓸 수 있게 제공하는 인터페이스&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;5. Claude Code 예제 실습&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; 파일폴더 / 파일명 : inventory-project / inventory.py&lt;/p&gt;
&lt;pre id=&quot;code_1783659510650&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import json

LOW_STOCK_THRESHOLD = 5
DEFAULT_INVENTORY_FILE = &quot;inventory.json&quot;

inventory = {}


def add_item(name, quantity):
    inventory[name] = inventory.get(name, 0) + quantity
    check_low_stock(name)


def remove_item(name, quantity):
    if name not in inventory:
        print(f&quot;'{name}' 항목이 존재하지 않습니다.&quot;)
        return

    inventory[name] = max(0, inventory[name] - quantity)
    check_low_stock(name)


def update_quantity(name, quantity):
    inventory[name] = quantity
    check_low_stock(name)


def check_low_stock(name):
    if inventory.get(name, 0) &amp;lt; LOW_STOCK_THRESHOLD:
        print(f&quot;[재고부족 경고] '{name}' 재고가 {inventory[name]}개로 부족합니다.&quot;)


def display_inventory():
    for name, quantity in inventory.items():
        print(f&quot;{name}: {quantity}개&quot;)


def save_inventory(filepath=DEFAULT_INVENTORY_FILE):
    with open(filepath, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        json.dump(inventory, f, ensure_ascii=False, indent=2)
    print(f&quot;재고 목록을 '{filepath}' 파일에 저장했습니다.&quot;)


def load_inventory(filepath=DEFAULT_INVENTORY_FILE):
    global inventory
    try:
        with open(filepath, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
            inventory = json.load(f)
        print(f&quot;'{filepath}' 파일에서 재고 목록을 불러왔습니다.&quot;)
    except FileNotFoundError:
        print(f&quot;'{filepath}' 파일이 존재하지 않습니다.&quot;)


if __name__ == &quot;__main__&quot;:
    add_item(&quot;사과&quot;, 10)
    remove_item(&quot;사과&quot;, 7)
    display_inventory()
    save_inventory()
    load_inventory()
    display_inventory()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 10-48-09.png&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;877&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF6xZv/dJMcabLI6lR/Exke1fEiGVjz8P6zMZQV50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF6xZv/dJMcabLI6lR/Exke1fEiGVjz8P6zMZQV50/img.png&quot; data-alt=&quot;.py 파일을 새로 생성해서 요구사항 수정 반영 요청하기 (1)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF6xZv/dJMcabLI6lR/Exke1fEiGVjz8P6zMZQV50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF6xZv%2FdJMcabLI6lR%2FExke1fEiGVjz8P6zMZQV50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;814&quot; height=&quot;877&quot; data-filename=&quot;스크린샷 2026-07-10 10-48-09.png&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;877&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.py 파일을 새로 생성해서 요구사항 수정 반영 요청하기 (1)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 10-49-36.png&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqjzp/dJMcaccHnLd/uOpDfvwkIK685oTJn6KFlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqjzp/dJMcaccHnLd/uOpDfvwkIK685oTJn6KFlK/img.png&quot; data-alt=&quot;채팅 화면 (파일을 새로 생성후 추가 요청사항을 반영한다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqjzp/dJMcaccHnLd/uOpDfvwkIK685oTJn6KFlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnqjzp%2FdJMcaccHnLd%2FuOpDfvwkIK685oTJn6KFlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;815&quot; height=&quot;919&quot; data-filename=&quot;스크린샷 2026-07-10 10-49-36.png&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅 화면 (파일을 새로 생성후 추가 요청사항을 반영한다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;6. 컨텍스트 엔지니어링의 전환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6.1. 컨텍스트란?&amp;nbsp;&lt;/b&gt;&lt;br /&gt;Claude Code에서는 대화가 아닌 프로젝트 폴더 전체가 컨텍스트의 기본 단위, 그 폴더의 배경과 규칙을 적어두는 문서다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** 파일명 : &lt;b&gt;CLAUDE.md&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 진행 방향에 대한 산출물 가이드(CLAUDE.md / md=markdown)이며, 규칙 덧붙이기, 일종의 메뉴얼&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;6.2.&amp;nbsp; /init md 초안 만들기 실습&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; ※ 실습 도중에도 토큰은 사용된다. 핵심만 출력하도록 활용하기&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;- Claude Code 터미널창 &amp;gt; &lt;b&gt;init&amp;nbsp; 입력&lt;/b&gt;&amp;nbsp; &amp;nbsp;// 파일 CLAUDE.md가 생성된다.&amp;nbsp;&lt;br /&gt;&amp;nbsp; - cat CLAUDE.md&amp;nbsp; &amp;nbsp;// md&amp;nbsp; 파일 생성 결과 확인, 기본 가이드임을 알려준다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 11-21-10.png&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv9AKT/dJMcaalBvmz/bkBDCBLekPpYSAbrWVga3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv9AKT/dJMcaalBvmz/bkBDCBLekPpYSAbrWVga3k/img.png&quot; data-alt=&quot;skill init &amp;quot;추가 요구사항&amp;quot; 반영(초록색 부분) 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv9AKT/dJMcaalBvmz/bkBDCBLekPpYSAbrWVga3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv9AKT%2FdJMcaalBvmz%2FbkBDCBLekPpYSAbrWVga3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;882&quot; data-filename=&quot;스크린샷 2026-07-10 11-21-10.png&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;882&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;skill init &quot;추가 요구사항&quot; 반영(초록색 부분) 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 11-24-43.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLV1Ra/dJMcahZl2qw/FAyizrdv0UyrLBPpRPED2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLV1Ra/dJMcahZl2qw/FAyizrdv0UyrLBPpRPED2K/img.png&quot; data-alt=&quot;skill init &amp;quot;추가 요구사항&amp;quot; 반영(초록색 부분) 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLV1Ra/dJMcahZl2qw/FAyizrdv0UyrLBPpRPED2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLV1Ra%2FdJMcahZl2qw%2FFAyizrdv0UyrLBPpRPED2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;791&quot; data-filename=&quot;스크린샷 2026-07-10 11-24-43.png&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;skill init &quot;추가 요구사항&quot; 반영(초록색 부분) 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 11-40-12.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgEeE7/dJMcab5WA7Q/XCdP9zb2K8ru4n8sU6ih70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgEeE7/dJMcab5WA7Q/XCdP9zb2K8ru4n8sU6ih70/img.png&quot; data-alt=&quot;파일저장 및 불러오기 기능을 JSON 형식으로 추가하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgEeE7/dJMcab5WA7Q/XCdP9zb2K8ru4n8sU6ih70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgEeE7%2FdJMcab5WA7Q%2FXCdP9zb2K8ru4n8sU6ih70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;695&quot; height=&quot;731&quot; data-filename=&quot;스크린샷 2026-07-10 11-40-12.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일저장 및 불러오기 기능을 JSON 형식으로 추가하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 11-40-27.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;907&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmYLj0/dJMcacqiiLU/M829WNqTKbkfN10GgVtzx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmYLj0/dJMcacqiiLU/M829WNqTKbkfN10GgVtzx1/img.png&quot; data-alt=&quot;inventory.py 에서 추가 요구사항이 반영된 결과 출력 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmYLj0/dJMcacqiiLU/M829WNqTKbkfN10GgVtzx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmYLj0%2FdJMcacqiiLU%2FM829WNqTKbkfN10GgVtzx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;915&quot; data-filename=&quot;스크린샷 2026-07-10 11-40-27.png&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;907&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;inventory.py 에서 추가 요구사항이 반영된 결과 출력 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;8. Skill의 전환 + 종합 실습&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8.1. claude/skills / 폴더 안에 스킬 폴더 / SKILL.md 생성 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cat.claude / skills / * / SKILL.md&lt;/p&gt;
&lt;pre id=&quot;code_1783654578652&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project overview

A minimal single-file Python inventory tracker. There is no build system, package manager, test suite, or linter configured &amp;mdash; the entire project is `inventory.py`.

## Commands

Run the script directly:

```bash
python3 inventory.py
```

There are no tests, lint, or build steps in this repository.

## Architecture

`inventory.py` keeps all state in a single module-level dict, `inventory` (name -&amp;gt; quantity), and exposes plain functions that mutate it:

- `add_item(name, quantity)` / `remove_item(name, quantity)` / `update_quantity(name, quantity)` &amp;mdash; mutate `inventory`, then call `check_low_stock(name)`.
- `check_low_stock(name)` &amp;mdash; prints a `[재고부족 경고]` warning whenever an item's quantity drops below `LOW_STOCK_THRESHOLD` (currently 5). Any new function that changes an item's quantity should call this afterward to keep the low-stock warning consistent.
- `display_inventory()` &amp;mdash; prints the current state of `inventory`.

The `if __name__ == &quot;__main__&quot;:` block at the bottom is a manual smoke test, not an automated test suite.

## Coding rules

- The main file is `inventory.py`.
- Function names start with a verb (e.g., `add_item`, `remove_item`).&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 11-57-15.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;974&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vfj1K/dJMcacDQuug/2VmtYdojXou3kY8PkiYDg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vfj1K/dJMcacDQuug/2VmtYdojXou3kY8PkiYDg1/img.png&quot; data-alt=&quot;skill.md 생성된 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vfj1K/dJMcacDQuug/2VmtYdojXou3kY8PkiYDg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvfj1K%2FdJMcacDQuug%2F2VmtYdojXou3kY8PkiYDg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;974&quot; data-filename=&quot;스크린샷 2026-07-10 11-57-15.png&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;974&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;skill.md 생성된 파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 12-06-53.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eIjw8V/dJMcacDQuxR/U2s2uwIb11UbcDRC5AbYCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eIjw8V/dJMcacDQuxR/U2s2uwIb11UbcDRC5AbYCK/img.png&quot; data-alt=&quot;CLAUDE.md 스킬 생성 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eIjw8V/dJMcacDQuxR/U2s2uwIb11UbcDRC5AbYCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeIjw8V%2FdJMcacDQuxR%2FU2s2uwIb11UbcDRC5AbYCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;798&quot; height=&quot;718&quot; data-filename=&quot;스크린샷 2026-07-10 12-06-53.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CLAUDE.md 스킬 생성 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-10 12-10-32.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTfbPP/dJMcabLI54P/kL9z9hiSaSD0M21CyVc9Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTfbPP/dJMcabLI54P/kL9z9hiSaSD0M21CyVc9Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTfbPP/dJMcabLI54P/kL9z9hiSaSD0M21CyVc9Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTfbPP%2FdJMcabLI54P%2FkL9z9hiSaSD0M21CyVc9Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;801&quot; height=&quot;711&quot; data-filename=&quot;스크린샷 2026-07-10 12-10-32.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/88</guid>
      <comments>https://goodjop79.tistory.com/88#entry88comment</comments>
      <pubDate>Fri, 10 Jul 2026 15:11:35 +0900</pubDate>
    </item>
    <item>
      <title>D+65[C+MySQL] SQL 예제 문제 실습</title>
      <link>https://goodjop79.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 9일(목)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 내 용 : MySQL 예제 문제 실습&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 회원 테이블 생성하기&amp;nbsp; CREATE TABLE member;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-34-12.png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckqN3c/dJMcabEPGaa/qXC5lStkOjrZtBQe21fzak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckqN3c/dJMcabEPGaa/qXC5lStkOjrZtBQe21fzak/img.png&quot; data-alt=&quot;CREATE TABLE member;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckqN3c/dJMcabEPGaa/qXC5lStkOjrZtBQe21fzak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckqN3c%2FdJMcabEPGaa%2FqXC5lStkOjrZtBQe21fzak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;767&quot; height=&quot;825&quot; data-filename=&quot;스크린샷 2026-07-09 15-34-12.png&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CREATE TABLE member;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 구매 테이블 생성하기&amp;nbsp; CREATE TABLE buy;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-34-23.png&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfogSE/dJMcacw01qC/a5EXdBbxjAi1lwfsyC2QH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfogSE/dJMcacw01qC/a5EXdBbxjAi1lwfsyC2QH0/img.png&quot; data-alt=&quot;CREATE TABLE buy;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfogSE/dJMcacw01qC/a5EXdBbxjAi1lwfsyC2QH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfogSE%2FdJMcacw01qC%2Fa5EXdBbxjAi1lwfsyC2QH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;806&quot; data-filename=&quot;스크린샷 2026-07-09 15-34-23.png&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CREATE TABLE buy;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□&amp;nbsp; 테이블 데이터 조회하기&amp;nbsp; &amp;nbsp; naver_db.buy;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; naver_db. member;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;☞ 데이터 베이스가 선택되지 않아 오류 발생시 다시 USE 로 데이터베이스 사용전환을 한 후 다시 실행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-35-40.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;955&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm157Y/dJMcajiBaWD/YOlFlCYhMjVcb3RFrCMPm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm157Y/dJMcajiBaWD/YOlFlCYhMjVcb3RFrCMPm0/img.png&quot; data-alt=&quot;각각의 다른위치에서 자료 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm157Y/dJMcajiBaWD/YOlFlCYhMjVcb3RFrCMPm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm157Y%2FdJMcajiBaWD%2FYOlFlCYhMjVcb3RFrCMPm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;806&quot; data-filename=&quot;스크린샷 2026-07-09 15-35-40.png&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;955&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;각각의 다른위치에서 자료 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 멤버 데이터 조회하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biXqwU/dJMcagGeRja/cw5bMTdc43jKiFRgztEeIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biXqwU/dJMcagGeRja/cw5bMTdc43jKiFRgztEeIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biXqwU/dJMcagGeRja/cw5bMTdc43jKiFRgztEeIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiXqwU%2FdJMcagGeRja%2Fcw5bMTdc43jKiFRgztEeIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;757&quot; height=&quot;259&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 멤버 데이터 중에서 id, name, addr 만 선택 조회하기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-36-28.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p6LBv/dJMcaaMJqRh/XDlkX5RWh7Th2BjLWKmsdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p6LBv/dJMcaaMJqRh/XDlkX5RWh7Th2BjLWKmsdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p6LBv/dJMcaaMJqRh/XDlkX5RWh7Th2BjLWKmsdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp6LBv%2FdJMcaaMJqRh%2FXDlkX5RWh7Th2BjLWKmsdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;571&quot; data-filename=&quot;스크린샷 2026-07-09 15-36-28.png&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□&amp;nbsp; 생성된 뷰 모든 데이터 조회하기&lt;/b&gt;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwiui5yBm8WVAxWibPUHHSCUCFsQi4wTeggIAggACAQQAA&quot; data-bfc=&quot;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgEEAE&quot; data-ved=&quot;2ahUKEwiui5yBm8WVAxWibPUHHSCUCFsQ-7AUeggIAggACAQQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgEEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;member 테이블에서 회원 아이디(mem_id), 이름(mem_name), 주소(addr)만 뽑아내어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;v_member 라는 이름의 뷰를 생성하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;b&gt;뷰는 실제 데이터를 저장하지 않고,&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;원본 테이블의 데이터를 보여주는 '창문' 역할을 한다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783587033569&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE VIEW v_member AS SELECT mem_id, mem_name, addr FROM member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성된 뷰(v_member)의 모든 데이터를 조회합니다. 즉, 앞서 지정한 회원 아이디, 이름, 주소 전체를 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783587085754&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM v_member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-ved=&quot;2ahUKEwiui5yBm8WVAxWibPUHHSCUCFsQ-7AUeggIAggACAQQDQ&quot; data-hveid=&quot;CAIIAAgEEA0&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgEEA4&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;뷰(v_member)에서 주소(addr)가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'서울' 또는 '경기'인 회원의 이름과 주소만 조회&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1783587179835&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT mem_name, addr FROM v_member WHERE addr IN ('서울', '경기');&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-36-52.png&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uj3to/dJMcaa0e69U/j7ibYcy7qZPU2KIcSIbFQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uj3to/dJMcaa0e69U/j7ibYcy7qZPU2KIcSIbFQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uj3to/dJMcaa0e69U/j7ibYcy7qZPU2KIcSIbFQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUj3to%2FdJMcaa0e69U%2Fj7ibYcy7qZPU2KIcSIbFQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;769&quot; height=&quot;686&quot; data-filename=&quot;스크린샷 2026-07-09 15-36-52.png&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- JOIN 구문을 활용하여 데이터 통합 하기&lt;/p&gt;
&lt;pre id=&quot;code_1783586025930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT B.mem_id, M.mem_name, B.prod_name, M.addr, 
        CONCAT(M.phone1, M.phone2) '연락처' 
   FROM buy B
     INNER JOIN member M
     ON B.mem_id = M.mem_id;

== &amp;lt;구분 설명 &amp;gt; ==
     SELECT B.mem_id,  // buy 테이블(B)에서 회원 ID를 가져오기
       M.mem_name,     // member 테이블(M)에서 회원 이름을 가져오기
       B.prod_name,    // buy 테이블(B)에서 구매한 상품명을 가져오기
       M.addr,         // member 테이블(M)에서 주소를 가져오기
       CONCAT(M.phone1, M.phone2) '연락처' 
                      // member 테이블(M)의 연락처 국번과 번호를 합쳐 '연락처'라는 별칭으로 가져오기      
  FROM buy B          // [기준 테이블] 중심이 되는 구매(buy) 테이블을 가져오고, 별칭을 'B'로 지정

       INNER JOIN member M // [조인 대상] 연결할 회원(member) 테이블을 가져오고, 별칭을 'M'으로 지정
                           // (INNER JOIN이므로 두 테이블 모두에 데이터가 존재하는 경우만 매칭된다)
                              
       ON B.mem_id = M.mem_id; //[조인 조건] buy 테이블의 회원 ID(B.mem_id)와 
                               // member 테이블의 회원 ID(M.mem_id)일치 데이터만 연결하는 조건 지정&lt;/code&gt;&lt;/pre&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACAsQAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;※ SQL문은 구매 테이블(buy)과 회원 테이블(member)을 회원 ID 기준으로 결합,&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; 상품을 구매한 회원의 &lt;/span&gt;&lt;b&gt;구매 기록, 주소, &lt;/b&gt;&lt;b&gt;&amp;nbsp;합쳐진 전화번호를 조회&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;하는 내부 조인(INNER JOIN) 구문이다.&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACAsQAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;INNER JOIN member M&lt;/b&gt;: buy 테이블에 있는 데이터와 member 테이블에 있는 데이터를 서로 결합하겠다는 의미&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;ON B.mem_id = M.mem_id&lt;/b&gt;: 결합할 때의 기준 키(Key). 구매한 사람의 ID(B.mem_id)가 회원 가입된 ID(M.mem_id)와 &lt;b data-index-in-node=&quot;88&quot; data-path-to-node=&quot;6,1,0&quot;&gt;정확히 일치하는 데이터만 합쳐서&lt;/b&gt; 결과를 출력한다&lt;/li&gt;
&lt;li&gt;회원 정보가 없는 유령 회원의 구매 기록이나, 구매 기록이 없는 회원의 정보는 출력되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;775&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cV6JUC/dJMcaf8gPqG/fou2kDmpQk7h6ptXGqJKnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cV6JUC/dJMcaf8gPqG/fou2kDmpQk7h6ptXGqJKnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV6JUC/dJMcaf8gPqG/fou2kDmpQk7h6ptXGqJKnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcV6JUC%2FdJMcaf8gPqG%2Ffou2kDmpQk7h6ptXGqJKnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;704&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;775&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□&amp;nbsp; name 조회하기&amp;nbsp;&lt;/b&gt; &amp;nbsp; SELECT&amp;nbsp;*&amp;nbsp;FROM&amp;nbsp;v_memberbuy&amp;nbsp;WHERE&amp;nbsp;mem_name&amp;nbsp;=&amp;nbsp;'블랙핑크';&lt;/p&gt;
&lt;pre id=&quot;code_1783588312177&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT B.mem_id 'Member ID', M.mem_name AS 'Member Name', B.prod_name &quot;Product Name&quot;, 
       CONCAT(M.phone1, M.phone2) AS &quot;Office Phone&quot; 
FROM buy B 
INNER JOIN member M ON B.mem_id = M.mem_id;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIBBAA&quot; data-ved=&quot;2ahUKEwj6xff2n8WVAxULkVYBHUesL3wQ-7AUegYIAAgEEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAIBBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;테이블 합치기 &lt;/b&gt;: 구매 테이블(buy)과 회원 테이블(member)을 회원 아이디(mem_id) 기준으로 합쳐서 정보 가져오기&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAIBBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;연락처 합치기 &lt;/b&gt;: CONCAT 함수를 사용해 나눠져 있던 전화번호(phone1, phone2)를 하나의 연락처 문자열로 연결&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;별칭 지정 &lt;/b&gt;: 결과 화면에 보일 열 이름을 영어(Member ID, Member Name 등)로 알아보기 쉽게 바꾸기&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-37-35.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;809&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A7Wz6/dJMcabEPGf6/I8H7buafjKTkkwFLX4anYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A7Wz6/dJMcabEPGf6/I8H7buafjKTkkwFLX4anYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A7Wz6/dJMcabEPGf6/I8H7buafjKTkkwFLX4anYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA7Wz6%2FdJMcabEPGf6%2FI8H7buafjKTkkwFLX4anYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;837&quot; height=&quot;752&quot; data-filename=&quot;스크린샷 2026-07-09 15-37-35.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;809&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 기존 가상 테이블 뷰 조회 하기&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783588469305&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT DISTINCT `Member ID`, `Member Name` FROM v_viewtest1;  // 백틱 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;* DISTINCT를 사용했기 때문에, 한 사람이 물건을 여러 개 샀더라도 중복 없이 &lt;b&gt;이름을 딱 한 번씩만 출력&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 가상 테이블 수정, 뷰의 구조 변경하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1783588491698&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER VIEW v_viewtest1 AS 
SELECT B.mem_id '회원 아이디', M.mem_name AS '회원 이름', B.prod_name &quot;제품 이름&quot;, 
       CONCAT(M.phone1, M.phone2) AS &quot;연락처&quot; 
FROM buy B 
INNER JOIN member M ON B.mem_id = M.mem_id;&lt;/code&gt;&lt;/pre&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;012&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/012.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/012.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&amp;nbsp;`&lt;span style=&quot;color: #8a3db6;&quot;&gt;백틱`&lt;/span&gt;이&amp;nbsp; 뭔데요?&amp;nbsp;&lt;/b&gt;&lt;br /&gt;백틱&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;(``)&lt;/b&gt;&lt;/span&gt;은 프로그래밍에서 &lt;b&gt;문자열을 감싸거나 코드 블록을 표시할 때 사용하는 특수 기호&lt;/b&gt;.&lt;br /&gt;키보드의 1번 왼쪽(물결표 아래 &lt;b&gt;``&lt;/b&gt;)에 위치하며, 변수와 문자열을 쉽게 결합하고 &lt;br /&gt;여러 줄을 표현할 수 있어 JavaScript, SQL, Markdown 등에서 자주 쓰인다.&lt;br /&gt;- 변수삽입(보간법) : &lt;u&gt;문자열 내에 $\{변수명\}을 넣어 + 기호 없이도 변수나 표현식을 문자열에 쉽게 포함&lt;/u&gt;&lt;br /&gt;- 여러 줄 문자열 : 따옴표와 달리 &lt;u&gt;줄 바꿈을 하더라도 에러가 나지 않아, 긴 문장이나 HTML 코드&lt;/u&gt;를 작성&lt;br /&gt;- 코드블록 : 마크다운(Markdown), GitHub 등에서 &lt;u&gt;백틱 세 개(```)를 사용해 코드를 강조하는 블록 생성&amp;nbsp;&lt;/u&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 15-37-46.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD9FfM/dJMcadP88Sy/p5gow0yoexmBr6pADLeWO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD9FfM/dJMcadP88Sy/p5gow0yoexmBr6pADLeWO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD9FfM/dJMcadP88Sy/p5gow0yoexmBr6pADLeWO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD9FfM%2FdJMcadP88Sy%2Fp5gow0yoexmBr6pADLeWO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;709&quot; data-filename=&quot;스크린샷 2026-07-09 15-37-46.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;785&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□ 파일 위치 지정하여 조회하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 16-06-18.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VpMyD/dJMcadP88ST/kykvNGFk1RaGEQigpOj8hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VpMyD/dJMcadP88ST/kykvNGFk1RaGEQigpOj8hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VpMyD/dJMcadP88ST/kykvNGFk1RaGEQigpOj8hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVpMyD%2FdJMcadP88ST%2FkykvNGFk1RaGEQigpOj8hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;778&quot; height=&quot;202&quot; data-filename=&quot;스크린샷 2026-07-09 16-06-18.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;□&amp;nbsp; 테이블 수정하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;b&gt;ALTER 문은&lt;/b&gt; 이미 생성된 데이터베이스 &lt;b&gt;객체(테이블 등)의 구조를 수정할 때&lt;/b&gt; 사용하는 DDL 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;b&gt;기존 테이블을 삭제하고 다시 만들 필요 없이 컬럼 추가, 삭제, 자료형 변경, 제약조건 설정&lt;/b&gt; 등을 수행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-09 17-13-09.png&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4ncL8/dJMcacDPZwg/noD1kwNrOzvUJ68i2YTc51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4ncL8/dJMcacDPZwg/noD1kwNrOzvUJ68i2YTc51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4ncL8/dJMcacDPZwg/noD1kwNrOzvUJ68i2YTc51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4ncL8%2FdJMcacDPZwg%2FnoD1kwNrOzvUJ68i2YTc51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;760&quot; height=&quot;724&quot; data-filename=&quot;스크린샷 2026-07-09 17-13-09.png&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/c# c++</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/87</guid>
      <comments>https://goodjop79.tistory.com/87#entry87comment</comments>
      <pubDate>Thu, 9 Jul 2026 18:32:27 +0900</pubDate>
    </item>
    <item>
      <title>D+65[AI] Claude 활용을 통한 하네스 엔지니어링</title>
      <link>https://goodjop79.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 9일(목)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ&amp;nbsp;주&amp;nbsp;제&amp;nbsp;:&amp;nbsp;Claude&amp;nbsp;Code&amp;nbsp;-&amp;nbsp;하네스(harness)&amp;nbsp;&lt;/b&gt;&lt;br&gt;ㅇ 주요 학습할 내용 : Claude.ai, Claude Code, MCP, VS Code, Git/GitHub, 나만의 MCP 서버 만들기 진행(예정)&lt;br&gt;&lt;br&gt;&lt;b&gt;1.&amp;nbsp;Claude&amp;nbsp;활용을&amp;nbsp;통한&amp;nbsp;하네스&amp;nbsp;엔지니어링&amp;nbsp;개념&amp;nbsp;이해하기&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 하네스 엔지니어링(Harness Engineering)&amp;nbsp;&lt;/b&gt;:&amp;nbsp;어떻게&amp;nbsp;일을&amp;nbsp;할지&amp;nbsp;설정해&amp;nbsp;놓은&amp;nbsp;것&amp;nbsp;&quot;하네스&quot;&lt;br&gt;AI&amp;nbsp;에이전트가&amp;nbsp;허용된&amp;nbsp;범위&amp;nbsp;내에서&amp;nbsp;안전하게&amp;nbsp;업무를&amp;nbsp;수행하도록&amp;nbsp;돕는&amp;nbsp;전체적인&amp;nbsp;제어&amp;nbsp;구조와&amp;nbsp;운영&amp;nbsp;체계,&amp;nbsp;강력하지만&amp;nbsp;통제하기&amp;nbsp;어려운&amp;nbsp;AI의&amp;nbsp;힘을&amp;nbsp;올바른&amp;nbsp;방향으로&amp;nbsp;이끌고&amp;nbsp;오류를&amp;nbsp;방지하는&amp;nbsp;역할&lt;br&gt;&lt;b&gt;프롬프트 엔지니어링이 &quot;어떻게 지시할 것인가&quot;에 집중&lt;/b&gt;한다면, &lt;b&gt;하네스 엔지니어링은 &quot;AI가 제대로 일할 수 있는 전체 업무 환경과 안전 규칙을 어떻게 구축할 것인가&quot;에 초점&lt;/b&gt;을 맞추고,&lt;u&gt; AI의 신뢰성과 자율성을 극대화하기 위해 지속적인 성능이 개선&lt;/u&gt;되고 있다.&amp;nbsp;&lt;br&gt;* LLM 활용 엔지니어링 : 프롬프트 엔지니어링, 컨텍스트 엔지니어링, 하네스 엔지니어링&amp;nbsp; &amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br&gt;&lt;b&gt;2.&amp;nbsp;Claude.ai&amp;nbsp;스킬&lt;/b&gt;&lt;b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1.&amp;nbsp;ERD&amp;nbsp;생성하기&amp;nbsp;-&amp;nbsp;생성한&amp;nbsp;ERD를&amp;nbsp;코드로&amp;nbsp;변환하기&lt;/b&gt;&lt;br&gt;&lt;b&gt;2.2. Skill 이란?&lt;/b&gt;&lt;br&gt;프롬프트에서 컨텍스트로 - 코드로 생성하면 하네스 , 스킬 형식을 지정해 놓으면 반복하는 답변형식을 스킬로 만들어서 재사용한다-&amp;nbsp;하네스를&amp;nbsp;자동으로&amp;nbsp;가져와서&amp;nbsp;사용할수&amp;nbsp;있게&amp;nbsp;설정을&amp;nbsp;할수&amp;nbsp;있어야&amp;nbsp;한다.&amp;nbsp;이것이&amp;nbsp;하네스&amp;nbsp;엔지니어링이다.&lt;br&gt;-&amp;nbsp;스킬이&amp;nbsp;켜지면&amp;nbsp;어떻게&amp;nbsp;사용하라고&amp;nbsp;설명해&amp;nbsp;주는&amp;nbsp;메뉴얼&amp;nbsp;같은&amp;nbsp;형태이다.&lt;br&gt;(예시사이트)&amp;nbsp;skillsmp.com&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;※ 예시) 이미지를 &quot;인식&quot;한 후 &quot;관계를 해석&quot;하고, &quot;소스코드로 변환&quot; 한다.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;2.3.&amp;nbsp;반복한&amp;nbsp;지시를&amp;nbsp;스킬로&amp;nbsp;만들어서&amp;nbsp;사용하기&amp;nbsp;실습&lt;/b&gt;&lt;br&gt;-&amp;nbsp;스킬을&amp;nbsp;체크해서&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;개발에서&amp;nbsp;사용하는&amp;nbsp;스킬은&amp;nbsp;공부를&amp;nbsp;해야&amp;nbsp;한다.&amp;nbsp;자료요약&amp;nbsp;및&amp;nbsp;정리&amp;nbsp;조건,&amp;nbsp;채팅도&amp;nbsp;업무와&amp;nbsp;일이다.&amp;nbsp;&lt;br&gt;- 리서치 한 채팅 내용에서 [스킬 - skill-creator- 프롬프트 창]에 스킬문구가 자동 추가되어 있고, 그 규칙을 만들어서 [저장 - Save Skill] 해 두면 다음번 채팅할때 반복 입력하지 않아도 된다. + 표시클릭하면 스킬에 새로 추가 만들어진 &quot;스킬 목록&quot;을 확인할 수 있다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;※ 나만의 채팅 스킬 작성&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;① erd-image-analysis&amp;nbsp;&lt;/p&gt;&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;&lt;div class=&quot;moreless-content&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를&amp;nbsp;첨부한다.&lt;br&gt;이미지를&amp;nbsp;분석한다&lt;br&gt;관계&amp;nbsp;분석을&amp;nbsp;한다&lt;br&gt;분석한&amp;nbsp;결과를&amp;nbsp;클린&amp;nbsp;코드로&amp;nbsp;변환한다.&lt;br&gt;작성한&amp;nbsp;클린&amp;nbsp;코드는&amp;nbsp;오류를&amp;nbsp;검증하고&amp;nbsp;수정하여&amp;nbsp;출력한다.&lt;br&gt;파일 내려받기 가능하도록 생성한다.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;②&amp;nbsp;daily-training&lt;/p&gt;&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;&lt;div class=&quot;moreless-content&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;최근&amp;nbsp;작업&amp;nbsp;내용을&amp;nbsp;참고하여&amp;nbsp;&quot;문제점,&amp;nbsp;개선방안,&amp;nbsp;추진&amp;nbsp;성과,&amp;nbsp;향후&amp;nbsp;추진사항&amp;nbsp;등을&amp;nbsp;포함&quot;하여&amp;nbsp;주간&amp;nbsp;보고서를&amp;nbsp;생성한다.&lt;br&gt;한글버젼으로&amp;nbsp;작성하고,&amp;nbsp;관련&amp;nbsp;핵심내용&amp;nbsp;요약시&amp;nbsp;&quot;키워드&quot;는&amp;nbsp;최신&amp;nbsp;이슈를&amp;nbsp;검색하여&amp;nbsp;반영한다&lt;br&gt;생성한&amp;nbsp;보고서는&amp;nbsp;환각에&amp;nbsp;유의하여&amp;nbsp;&quot;팩트&quot;를&amp;nbsp;기반으로&amp;nbsp;작성해야&amp;nbsp;한다.&lt;br&gt;요구사항&amp;nbsp;결과는&amp;nbsp;오탈자가&amp;nbsp;없도록&amp;nbsp;결과를&amp;nbsp;검토후&amp;nbsp;답변한다.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;③ code-create&lt;/p&gt;&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;&lt;div class=&quot;moreless-content&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;코드&amp;nbsp;작성&amp;nbsp;및&amp;nbsp;수정&amp;nbsp;:&amp;nbsp;&amp;nbsp;버그수정,&amp;nbsp;기능&amp;nbsp;추가,&amp;nbsp;리팩토링&lt;br&gt;코드&amp;nbsp;탐색&amp;nbsp;및&amp;nbsp;이해&amp;nbsp;:&amp;nbsp;&amp;nbsp;이&amp;nbsp;프로젝트&amp;nbsp;구조를&amp;nbsp;설명하고,&amp;nbsp;핵심&amp;nbsp;함수는&amp;nbsp;어디에서&amp;nbsp;호출되는지&amp;nbsp;찾아주고,&amp;nbsp;&quot;주석&quot;을&amp;nbsp;달아줘.&lt;br&gt;테스트 및 검증 : 실행하여 실패 원인을 분석한다&lt;br&gt;별도 요청할 때까지 git / pr 작성, 코드 리뷰는 실행하지 않는다&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br&gt;&lt;b&gt;2.4.&amp;nbsp;Claude&amp;nbsp;Code&amp;nbsp;실행&amp;nbsp;하기&amp;nbsp;&lt;/b&gt;&lt;br&gt;GUI&amp;nbsp;=&amp;nbsp;AI,&amp;nbsp;Cowork&amp;nbsp;&lt;br&gt;Terminal&amp;nbsp;=&amp;nbsp;Claude&amp;nbsp;Code&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
 &lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2026년, 왜 지금 하네스 엔지니어링인가&lt;/span&gt;&lt;/b&gt;
 &lt;br&gt; 
 &lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;009&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot;&gt;
  &lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot; width=&quot;150&quot;&gt;
 &lt;/figure&gt; 
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2025년은 에이전트의 해, 2026년은 하네스의 해인 이유는?&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2025년은&amp;nbsp;AI&amp;nbsp;에이전트&amp;nbsp;기술이&amp;nbsp;급속도로&amp;nbsp;발전하며&amp;nbsp;다양한&amp;nbsp;산업에&amp;nbsp;도입이&amp;nbsp;시작된&amp;nbsp;해였다.&amp;nbsp;&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;수많은 기업이 에이전트 구현 자체에 집중했고, 그 결과 '일단 만들어보고 운용하면서 고친다'는 방식이 주류를 이뤘다. 그러나&amp;nbsp;에이전트&amp;nbsp;수가&amp;nbsp;늘고&amp;nbsp;서비스&amp;nbsp;규모가&amp;nbsp;확장되면서&amp;nbsp;공통적인&amp;nbsp;과제가&amp;nbsp;드러났다.&amp;nbsp;&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동작&amp;nbsp;불안정,&amp;nbsp;출력&amp;nbsp;품질의&amp;nbsp;편차,&amp;nbsp;보안&amp;nbsp;사고&amp;nbsp;등이&amp;nbsp;현실적인&amp;nbsp;문제로&amp;nbsp;부각되어&amp;nbsp;&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2026년에는&amp;nbsp;단순히&amp;nbsp;'에이전트를&amp;nbsp;만드는&amp;nbsp;것'에서&amp;nbsp;'&lt;br&gt;&lt;b&gt;에이전트를 안전하고 안정적으로 운용하는 구조를&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;설계하는&amp;nbsp;것&lt;/b&gt;'으로&amp;nbsp;관심의&amp;nbsp;축이&amp;nbsp;이동하고&amp;nbsp;있다.&lt;/span&gt;
 &lt;br&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;AI 에이전트 활용 확산과 함께 커지는 섀도우 AI의 위험&quot;&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 에이전트를 업무에 활용하는 사례가 늘어나면서 조직이 인식하지 못하는 사이에 새로운 위험도 &lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;함께 커지고 있다. 섀도우 AI(Shadow AI)?&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;조직의 공식적인 승인이나 관리 체계 밖에서 직원들이 무단으로 AI 도구를 도입·사용하는 현상&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;업무&amp;nbsp;효율을&amp;nbsp;높이려는&amp;nbsp;선의에서&amp;nbsp;시작되는&amp;nbsp;경우가&amp;nbsp;많지만,&amp;nbsp;기업&amp;nbsp;입장에서는&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;심각한&amp;nbsp;위험을&amp;nbsp;초래&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;① 데이터 유출 : 승인되지 않은 AI 서비스에 사내 기밀 정보나 고객 데이터가 입력될 수 있다.&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;②&amp;nbsp; 품질 불균형 : 팀마다 다른 AI 도구를 사용하면 출력 품질과 판단 기준이 통일되지 않는다.&lt;/span&gt;
 &lt;br&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;③ 책임 소재 불명확: 문제가 발생했을 때 어떤 도구가 원인인지 추적하기 어려워 진다.&lt;/span&gt;
 &lt;br&gt;
 &lt;br&gt;
&lt;/blockquote&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 후&amp;nbsp; &amp;nbsp;기&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; 오늘로써 교육 65일째 이제는 Claude 활용을 통한 하네스 엔지니어링을 하게 된다. 다양한 컴퓨터 AI, IT 용어들 Database, 기존에 있던 프로그램에서 새로운 인공지능까지 포함해서 학습해야 해서 공부해야 할 분야가 많지만 재미있게 활용해 보고 있다.&lt;br&gt;모든 일이 처음 시작은 어렵지만 익숙해지면 여러분야에 활용할 수 있게 된다. 다양한 프로그램과 인공지능을 응용한 프로그램을 만들어 보자. &quot;도전은 계속된다.&quot;&amp;nbsp; 끝.&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/86</guid>
      <comments>https://goodjop79.tistory.com/86#entry86comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:04:50 +0900</pubDate>
    </item>
    <item>
      <title>D+64[C+MySQL]  Workbench (핵심 문법정리 &amp;amp; 예제 실습)</title>
      <link>https://goodjop79.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일&amp;nbsp; 자 : 2026년 7월 8일(수)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 주&amp;nbsp; 제 : MySQL 기본, 고급문법 정리 및 Workbench 예제 실습&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. &lt;/b&gt;&lt;b&gt;MySQL&amp;nbsp;핵&lt;/b&gt;&lt;b&gt;심 문법 정리&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100.002%; height: 1192px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;명령어&lt;br&gt;(구문)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;기본 문법 및 형식&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;주요&lt;/b&gt;&lt;b&gt; 설명&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 176px; text-align: center;&quot; rowspan=&quot;4&quot;&gt;&lt;b&gt;DML&lt;br&gt;&lt;br&gt;(데이터 조작)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;SELECT&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;조회&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;SELECT 컬럼1, 컬럼2 FROM 테이블이름 WHERE 조건;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;특정 테이블에서 원하는 조건의 데이터를 조회&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;INSERT&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;입력&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;INSERT INTO 테이블이름 (컬럼1, 컬럼2) VALUES (값1, 값2);&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;테이블에 새로운 행(데이터)을 삽입&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;UPDATE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;기본적 &lt;br&gt;미허용&lt;br&gt;&quot;수정&quot;&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;UPDATE 테이블이름 SET 컬럼1 = 값1 WHERE 조건;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;조건에 맞는 기존 데이터를 수정&lt;br&gt;(WHERE 생략 시 전체 변경 주의)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;DELETE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;기본적 &lt;br&gt;미허용&lt;br&gt;&quot;삭제&quot;&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;DELETE FROM 테이블이름 WHERE 조건;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;조건에 맞는 기존 데이터를 삭제&lt;br&gt;(행 단위 삭제, 복구 가능) . 특정 툴에서 실행한다&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 176px; text-align: center;&quot; rowspan=&quot;4&quot;&gt;&lt;b&gt;DDL&lt;br&gt;&lt;br&gt;(데이터 정의)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;CREATE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;생성&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;CREATE TABLE 테이블이름 (컬럼1 데이터타입, ...);&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;새로운 테이블이나 데이터베이스를 생성&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;ALTER&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;수정&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;ALTER TABLE 테이블이름 ADD 컬럼명 데이터타입;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;기존 테이블의 구조(컬럼 추가, 수정, 삭제 등)를 변경&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;DROP&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;삭제&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;DROP TABLE 테이블이름;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;테이블 자체를 완전히 삭제 (구조와 데이터 모두 삭제)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;TRUNCATE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;초기화&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;TRUNCATE TABLE 테이블이름;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;테이블의 모든 데이터를 최초 생성 상태로 초기화 &lt;br&gt;(구조는 남기고 데이터만 고속 삭제, 복구 불가)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 77px; text-align: center;&quot; rowspan=&quot;2&quot;&gt;&lt;b&gt;DCL&lt;br&gt;&lt;br&gt;(데이터 제어)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;GRANT&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;권한부여&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;GRANT 권한 ON 대상 TO 사용자;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;특정 사용자에게 데이터베이스 객체에 대한 권한부여 (예: SELECT, INSERT 등)&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 33px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 33px; text-align: center;&quot;&gt;&lt;b&gt;REVOKE&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 33px; text-align: center;&quot;&gt;권한회수&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 33px; text-align: justify;&quot;&gt;REVOKE 권한 ON 대상 FROM 사용자;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 33px; text-align: justify;&quot;&gt;특정 사용자에게 부여했던 *권한을 회수(취소)*&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 88px; text-align: center;&quot; rowspan=&quot;2&quot;&gt;&lt;b&gt;TCL&lt;br&gt;&lt;br&gt;(트랜잭션 제어)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;COMMIT&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;작업완료&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;COMMIT;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;트랜잭션의 변경 내용을 데이터베이스에 최종 반영&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 44px; text-align: center;&quot;&gt;&lt;b&gt;ROLLBACK&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 44px; text-align: center;&quot;&gt;작업취소&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 44px; text-align: justify;&quot;&gt;ROLLBACK;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 44px; text-align: justify;&quot;&gt;트랜잭션의 변경 내용을 취소, 이전 상태로 되돌리기&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 111px;&quot;&gt;&lt;td style=&quot;width: 8.02325%; height: 657px; text-align: center;&quot; rowspan=&quot;8&quot;&gt;&lt;b&gt;고급&lt;br&gt;문법&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 11.8604%; height: 111px; text-align: center;&quot;&gt;윈도우 함수&lt;br&gt;&lt;br&gt;(Window Functions)&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 111px; text-align: center;&quot;&gt;ROW_NUMBER(), RANK(), DENSE_RANK()&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 111px; text-align: justify;&quot;&gt;SELECT 행번_함수() OVER (PARTITION BY 그룹컬럼 ORDER BY 정렬컬럼) FROM 테이블명;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 111px; text-align: justify;&quot;&gt;행과 행 간의 관계를 정의하여 순위를 매기거나 집계를 수행합니다. PARTITION BY로 그룹을 나누고 ORDER BY로 순서대로 정렬하여 계산합니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 77px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 77px; text-align: center;&quot;&gt;CTE&lt;br&gt;&lt;br&gt;(공통&lt;br&gt;테이블 식)&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 77px; text-align: center;&quot;&gt;WITH&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 77px; text-align: justify;&quot;&gt;WITH 임시테이블명 AS (SELECT ... ) SELECT * FROM 임시테이블명;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 77px; text-align: justify;&quot;&gt;복잡한 쿼리를 단순화하기 위해 서브쿼리를 마치 하나의 임시 테이블처럼 정의해 사용하는 문법입니다. 재귀적(Recursive) 호출도 가능합니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 66px; text-align: center;&quot;&gt;&lt;b&gt;서브쿼리 &lt;br&gt;확장&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 66px; text-align: center;&quot;&gt;ANY, ALL&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 66px; text-align: justify;&quot;&gt;SELECT * FROM 테이블명 WHERE 컬럼 &amp;gt; ANY / ALL (서브쿼리);&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 66px; text-align: justify;&quot;&gt;ANY는 서브쿼리의 결과 중 하나라도 만족하면 참(OR 조건), ALL은 서브쿼리의 모든 결과를 만족해야 참(AND 조건)이 됩니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 66px; text-align: center;&quot;&gt;&lt;b&gt;인덱스 제어&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 66px; text-align: center;&quot;&gt;FORCE INDEX&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 66px; text-align: justify;&quot;&gt;SELECT * FROM 테이블명 FORCE INDEX (인덱스명) WHERE 조건;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 66px; text-align: justify;&quot;&gt;MySQL 옵티마이저가 다른 실행 계획을 세우더라도, 개발자가 지정한 특정 인덱스를 강제로 사용하도록 지정합니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 83px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 83px; text-align: center;&quot;&gt;피벗 변환&lt;br&gt;&lt;br&gt;(행을 열로)&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 83px; text-align: center;&quot;&gt;CASE WHEN&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 83px; text-align: justify;&quot;&gt;SELECT 행컬럼, MAX(CASE WHEN 열컬럼 = '값' THEN 데이터 END) FROM 테이블명 GROUP BY 행컬럼;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 83px; text-align: justify;&quot;&gt;MySQL은 공식 PIVOT 명령어가 없으므로, 집계 함수(MAX, SUM)와 CASE WHEN 문을 결합하여 행 데이터를 열 데이터로 전환합니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 66px; text-align: center;&quot;&gt;&lt;b&gt;동적 SQL&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 66px; text-align: center;&quot;&gt;PREPARE, EXECUTE&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 66px; text-align: justify;&quot;&gt;PREPARE 템플릿명 FROM '쿼리문';&lt;br&gt;&lt;br&gt;EXECUTE 템플릿명 USING @변수;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 66px; text-align: justify;&quot;&gt;쿼리 문장을 문자열 형태로 준비해 두고, 실행 시점에 변수를 바인딩하여 동적으로 쿼리를 실행할 수 있게 합니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 111px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 111px; text-align: center;&quot;&gt;&lt;b&gt;프로시저&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 111px; text-align: center;&quot;&gt;CREATE PROCEDURE&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 111px; text-align: justify;&quot;&gt;DELIMITER //&lt;br&gt;&lt;br&gt;CREATE PROCEDURE 이름(IN/OUT 파라미터) BEGIN 쿼리... END //&lt;br&gt;&lt;br&gt;DELIMITER ;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 111px; text-align: justify;&quot;&gt;여러 개의 SQL 문을 하나의 함수처럼 묶어서 서버에 저장하고, CALL 프로시저명()으로 호출하여 실행하는 객체입니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 77px;&quot;&gt;&lt;td style=&quot;width: 11.8604%; height: 77px; text-align: center;&quot;&gt;&lt;b&gt;트리거&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 14.8836%; height: 77px; text-align: center;&quot;&gt;CREATE TRIGGER&lt;/td&gt;&lt;td style=&quot;width: 29.8791%; height: 77px; text-align: justify;&quot;&gt;CREATE TRIGGER 트리거명 BEFORE/AFTER INSERT/UPDATE/DELETE ON 테이블명 FOR EACH ROW BEGIN ... END;&lt;/td&gt;&lt;td style=&quot;width: 35.1333%; height: 77px; text-align: justify;&quot;&gt;특정 테이블에 데이터가 추가, 수정, 삭제될 때 자동으로 연쇄 실행되는 로직을 정의합니다. (OLD, NEW 키워드로 변경 전후 데이터 접근 가능)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/bF9nh1/dJMcahSxh15/drMG0F7rvGU7mc4If7HLjK/%EC%BF%BC%EB%A6%AC%ED%95%B5%EC%8B%AC%EB%AC%B8%EB%B2%95_260708.ods?attach=1&amp;amp;knm=tfile.ods&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;쿼리핵심문법_260708.ods&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.03MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.&lt;/b&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;b&gt;Workbench 예제 실습&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;2. 1. 새로운 쿼리를 작성후 리프레시를 하여 좌측 메뉴에서 Schemas 목록을 확인 한다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yqTAH/dJMcahLO4td/4r32xDtw0aDTvi4aORx1Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yqTAH/dJMcahLO4td/4r32xDtw0aDTvi4aORx1Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yqTAH/dJMcahLO4td/4r32xDtw0aDTvi4aORx1Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyqTAH%2FdJMcahLO4td%2F4r32xDtw0aDTvi4aORx1Fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;645&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;2. 2. 실습 문제 : SELECT&amp;nbsp; DB 조회&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFa6ML/dJMcabSlmDT/kGGrDcwm0NrvvgRltjctoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFa6ML/dJMcabSlmDT/kGGrDcwm0NrvvgRltjctoK/img.png&quot; data-alt=&quot;buy DB 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFa6ML/dJMcabSlmDT/kGGrDcwm0NrvvgRltjctoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFa6ML%2FdJMcabSlmDT%2FkGGrDcwm0NrvvgRltjctoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;610&quot; data-origin-width=&quot;631&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;buy DB 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cttKHw/dJMcadipjmI/jka9HYKE5VKqZPrpF1pb71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cttKHw/dJMcadipjmI/jka9HYKE5VKqZPrpF1pb71/img.png&quot; data-alt=&quot;member DB 조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cttKHw/dJMcadipjmI/jka9HYKE5VKqZPrpF1pb71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcttKHw%2FdJMcadipjmI%2Fjka9HYKE5VKqZPrpF1pb71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;250&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;member DB 조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/c# c++</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/85</guid>
      <comments>https://goodjop79.tistory.com/85#entry85comment</comments>
      <pubDate>Wed, 8 Jul 2026 17:36:42 +0900</pubDate>
    </item>
    <item>
      <title>D+63[학습로그] 초빙 강연 (일,채용,기업과 인재상)</title>
      <link>https://goodjop79.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 7일(화)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ&amp;nbsp;주&amp;nbsp;제&amp;nbsp;:&amp;nbsp;일,&amp;nbsp;채용,&amp;nbsp;기업과&amp;nbsp;인재상&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 주요 내용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 참고할 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;NCS 직무역량 강조&lt;/b&gt; : 자격증만 있다고 되는 것이 아니다. 직무역량, 자격증 역량, 포트폴리오 구성, 7가지 직업기초 능력이 필요 +&lt;i&gt; 리더십, 공동체 윤리, 직업윤리&lt;/i&gt; 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자기소개서 작성 :&lt;/b&gt; 이직한 계기, 어떤 문제를 해결하기 위해 노력해서 해결을 했어요, 하는 경험치가 들어가야 한다.&amp;nbsp; 2개 이상 포함되어야 한다. 내가 일상에 해 왔던 것들을 얼마만큼 해 내는 것들을 기술한 것, LLM은 확률형 문자열 답변 제공 기능을 하므로 반드시 검증이 필요하다.&lt;br /&gt;*&amp;nbsp; (예) LG 입사시에 유닉스 사용할 줄 안다고 하니 그것으로 무엇을 할 수 있니? 라고 질문함&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;HRD(Human Resource Development): 인적자원개발 조직과 개인 역량을 향상시켜 성과를 극대화하는 교육 및 훈련 활동&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;과거에는 개발자들의 퇴근시간이 일정치가 않았다. 개발자들이 대기하는 시간이 길어서 버티는 개발자가 많지 않았다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;개발자는 처음부터 연봉이 높지 않고, 실력을 꾸준히 쌓아가야 연봉도 함께 높아진다. 그만두는 경우가 있더라도 업계 관례상 동종업계에서 자기관리를 잘 해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일의 두가지 차원&lt;/b&gt;&lt;br /&gt;-&amp;nbsp; 기술적 차원 : 육체, 정신, 기술적 측면&lt;br /&gt;-&amp;nbsp; 사회적 차원 : 대인관계, 집단내 역학관계, 집단(조직)문화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 자기소개서 작성 Tip&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sdXMt/dJMcagMYkBt/vPY5jNkfa0iMCTOkkZ8r7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sdXMt/dJMcagMYkBt/vPY5jNkfa0iMCTOkkZ8r7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sdXMt/dJMcagMYkBt/vPY5jNkfa0iMCTOkkZ8r7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsdXMt%2FdJMcagMYkBt%2FvPY5jNkfa0iMCTOkkZ8r7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;325&quot; height=&quot;234&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;[경험치에 들어가야 할 내용들]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;취미나 면접시에는 게임 NO, 정치 및 종교색이 묻어나지 않도록 작성&amp;nbsp;&lt;/li&gt;
&lt;li&gt;내가 얼마나 긍정적으로 바른마음으로 행동이나 실천할 수 있는가?&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대인관계 역량 중요, 타인에 대한 감수성, 이해와 배려를 하는가?&lt;/li&gt;
&lt;li&gt;집단이나 조직문화에 융합될 수 있는가? 팀워크를 발휘할 수 있는가?&amp;nbsp;&lt;/li&gt;
&lt;li&gt;리더십 안에 flow ship 있다. 즉 조력자 역할이 있다&lt;/li&gt;
&lt;li&gt;리더십은 얼마만큼 나의 영향력이 타인에게 영향을 주었는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;[성장배경, 가치관, 강&amp;middot;약점]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;솔직 담백하게 적는다. 선한영향력을 주는 가치관&lt;/li&gt;
&lt;li&gt;업체 현장 견학 : 복장, 바른자세&lt;/li&gt;
&lt;li&gt;긍정적인것&amp;nbsp;먼저&amp;nbsp;작성하고,&amp;nbsp;부정적인것&amp;nbsp;뒤로&amp;nbsp;작성한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[기업과 인재상]&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기업이 선발하고자 하는 인재 : 경험을 통해서 무엇을 배웠는가?&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이 교육 과정은 경험치에 들어갈 수 있다. (예시 : 신입과 이모님들과 동시에 식사를 함께 하고자 자동화 생산라인 일부 프로그램 코드를 수정해서 개선한 사례, 수작업 엑셀작업들을 프로그램화 해서 업무의 효율성을 향상 시킨 사례)&lt;/li&gt;
&lt;li&gt;팀플, 팀워크 작업에서 문제를 해결하기 위해서 어떠한 노력을 했는가?&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&quot;협업의 관계 구조&quot;를 만들어 가는 것도 중요하다. 도움을 요청하고 해결하기 위한 노력 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2020년대 이후 기업환경과 채용전략의 변화 (잡코리아 2020)&amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 98.2558%; height: 80px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 15.6921%; height: 22px; text-align: center;&quot;&gt;방 식&lt;/td&gt;
&lt;td style=&quot;width: 9.48688%; height: 22px; text-align: center;&quot;&gt;범 위&lt;/td&gt;
&lt;td style=&quot;width: 12.3509%; height: 22px; text-align: center;&quot;&gt;구 조&lt;/td&gt;
&lt;td style=&quot;width: 62.4701%; height: 22px; text-align: center;&quot;&gt;조직 리더십&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.6921%; height: 16px; text-align: center;&quot;&gt;4차&amp;nbsp;산업혁명&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 9.48688%; height: 16px; text-align: center;&quot;&gt;AI채용&lt;/td&gt;
&lt;td style=&quot;width: 12.3509%; height: 16px; text-align: center;&quot;&gt;수시채용&lt;/td&gt;
&lt;td style=&quot;width: 62.4701%; height: 16px; text-align: center;&quot;&gt;무경계화&amp;nbsp;&amp;nbsp;애자일 조직&amp;nbsp;&amp;nbsp;&amp;nbsp;감성리더십&amp;nbsp;&amp;nbsp;협력 중심&amp;nbsp;&amp;nbsp;&amp;nbsp;융복합적 능력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ** (예) 광주글로벌모터스&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; -&lt;span&gt; &lt;/span&gt;&lt;/span&gt;지역에서 AI를 가장 먼저 도입, AI면접과 게임은 차원이 다르다. 이차원적인 게임을 한다.&lt;/span&gt; 게임에 임하는 태도와 영역을&amp;nbsp; 본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (모니터에 인식, 모니터에 기억된 나의 데이터를 통계치로 기억력, 성격, 성향을 살펴본다), - 면대면 면접도 준비를 하고 노력해야 한다. AI 면접은 1분안에 답변을 해야 한다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[과거에 비해 더 중요해진 인재상]&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;b&gt;책임감(52.8%), 소통(32.9%),&lt;/b&gt; 성실(32.9%), 팀워크(28.6%), 긍정적 태도(25.1%), 전문성(16.5%)&amp;nbsp; / 출처:&amp;nbsp;전자신문('22.3.20)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코로나 이후 인재상 평가비중이 늘었다.(50.9%)&lt;/li&gt;
&lt;li&gt;결과 채용시 인재상 부합 여부가 당락에 미치는 영향(58%)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지원자의 스팩은 충분하나 인재상에 부합하지 않아 탈락시킨 경험이 있다(77%)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스팩은 부족하지만 &quot;인재상&quot;이 부합해 합격시킨 기업 비중(86.1%)&amp;nbsp; / &lt;/b&gt;(예)경력직을 신입직원으로 채용하는 비중 증가 : 2023년에 현대차가 10년만에 공채 채용한 사례&lt;/li&gt;
&lt;li&gt;고용24 사이트 : 개인 성격검사.&amp;nbsp; &amp;nbsp;끝.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습 로그</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/84</guid>
      <comments>https://goodjop79.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 7 Jul 2026 11:13:14 +0900</pubDate>
    </item>
    <item>
      <title>D+62[C+MySQL] Workbench 예제 실습을 통한 이해</title>
      <link>https://goodjop79.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;7월&amp;nbsp;6일(월)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 주 제 : MySQL DataBase&amp;nbsp; 예제 실습 하기&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;1. MySQL 개념 &amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 데이터를 효율적으로 저장하고 관리하는 오픈소스 관계형 데이터베이스 관리 시스템(DBMS)&lt;br&gt;데이터를 읽고, 쓰고, 수정하고, 삭제하는 모든 과정은 구조화 질의 언어(SQL)를 통해 이루어 진다&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.1. 주요 핵심 용어의 의미&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;주요 핵심 용어 정리데이터베이스 (Database, DB) : 데이터가 저장되는 공간&lt;/li&gt;&lt;li&gt;테이블 (Table) : 행(Row)과 열(Column)로 이루어진 데이터의 저장 단위&lt;/li&gt;&lt;li&gt;열 / 필드 (Column / Field) : 데이터의 속성 (예: 이름, 나이)&lt;/li&gt;&lt;li&gt;행 / 레코드 (Row / Record) : 테이블에 저장되는 실제 데이터 묶음&lt;/li&gt;&lt;li&gt;기본 키 (PK, Primary Key) : 테이블 내에서 각 행을 고유하게 식별하는 값, 중복 불가, 빈 값 불가&lt;/li&gt;&lt;li&gt;외래 키 (FK, Foreign Key) : 다른 테이블의 기본 키와 연결하여 테이블 간의 관계를 맺는 열&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: justify;&quot;&gt;&lt;b&gt;2. MySQL 핵심 문법&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;※ 오늘 수업에서는 SELECT 관련 DB조회 문법만 &lt;br&gt;실습함. 나머지 핵심문법은 지속 학습예정.&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;2.1.&amp;nbsp; 핵심 SQL 문법 (CRUD) DML (데이터 조작어) - 데이터 관리데이터를 조회, 삽입, 수정, 삭제할 때 사용&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100.581%; height: 538px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 444px;&quot;&gt;&lt;td style=&quot;width: 100.0%; height: 444px; text-align: justify;&quot;&gt;가. DB 명령어 사용&amp;nbsp; &amp;nbsp; &amp;nbsp;* 명령의 종료시에는 세미콜론(;) 로 명령어 종료를 의미한다.&lt;br&gt;&amp;nbsp; &amp;nbsp; - 정렬하기 : ORDER BY&lt;br&gt;&amp;nbsp; &amp;nbsp; - 내림차순 : DESC;&lt;br&gt;&amp;nbsp; &amp;nbsp; - 오름차순 : ASC;&amp;nbsp; &amp;nbsp; * ASC명령어를 미기록해도 &lt;b&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;오름차순으로 기본&lt;/span&gt;&lt;/b&gt;으로 인식한다.&lt;br&gt;&amp;nbsp; &amp;nbsp; - SELECT (데이터 조회) : SELECT &lt;u&gt;컬럼1, 컬럼2&lt;/u&gt;&amp;nbsp; FROM &lt;u&gt;테이블이름 &lt;/u&gt;WHERE&amp;nbsp;조건;&lt;br&gt;&amp;nbsp; &amp;nbsp; - INSERT (데이터 삽입) :&amp;nbsp; INSERT INTO &lt;u&gt;테이블이름 (컬럼1, 컬럼2)&lt;/u&gt; VALUES&amp;nbsp;&lt;u&gt;(값1,&amp;nbsp;값2);&lt;/u&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - UPDATE (데이터 수정) : UPDATE &lt;u&gt;테이블이름&lt;/u&gt; SET &lt;u&gt;컬럼 = 새값&lt;/u&gt; WHERE&amp;nbsp;&lt;u&gt;조건;&lt;/u&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - DELETE (데이터 삭제) : DELETE FROM &lt;u&gt;테이블이름&lt;/u&gt; WHERE&amp;nbsp;&lt;u&gt;조건;&lt;br&gt;&lt;/u&gt;&lt;br&gt;나. 자주 사용하는 확장 문법 &lt;br&gt;&amp;nbsp; &amp;nbsp; - WHERE (조건문) 및 연산자 조건 : &lt;b&gt;= (같음),&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;!=&amp;nbsp;&lt;/b&gt; 또는&amp;nbsp; &lt;b&gt;&amp;lt;&amp;gt; (다름)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - LIKE (문자열 패턴) 논리&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp; - AND (그리고), OR (또는), NOT (부정)범위 : &lt;b&gt;BETWEEN&amp;nbsp; &lt;u&gt;A&lt;/u&gt;&amp;nbsp; AND&amp;nbsp; &lt;u&gt;B&amp;nbsp;&lt;/u&gt; (A와 B 사이), IN (값1, 값2)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; * 예시 (이름에 '김'이 포함된 사람 조회) &lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;☞&lt;/span&gt; SELECT * FROM &lt;u&gt;users&lt;/u&gt; WHERE &lt;u&gt;name&lt;/u&gt; LIKE &lt;u&gt;'%김%';&lt;/u&gt;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - ORDER BY (정렬) : 데이터를 오름차순(ASC) 또는 내림차순(DESC)으로 정렬&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;☞&lt;/span&gt;&lt;/b&gt; &lt;b&gt;SELECT * FROM &lt;u&gt;users&lt;/u&gt; ORDER BY &lt;u&gt;age&lt;/u&gt; DESC;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - LIMIT (결과 제한) 조회할 데이터의 개수를 제한 한다.&lt;b&gt; &lt;span style=&quot;color: #006DD7;&quot;&gt;☞ &lt;/span&gt;SELECT * FROM &lt;u&gt;users&lt;/u&gt; LIMIT &lt;u&gt;10;&lt;/u&gt;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - GROUP BY (그룹화)특정 기준을 바탕으로 데이터를 그룹으로 묶는다. 주로 집계 함수와 함께 쓰인다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; * 예시:&amp;nbsp; COUNT(), SUM(), AVG())&amp;nbsp; &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;☞ &lt;/span&gt;SELECT &lt;u&gt;age,&lt;/u&gt; COUNT&lt;u&gt;(*) AS 인원수&lt;/u&gt; FROM users&amp;nbsp;GROUP&amp;nbsp;BY&amp;nbsp;age;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; - JOIN (테이블 결합)두 개 이상의 테이블을 논리적으로 연결하여 하나의 데이터처럼 조회한다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; * INNER JOIN : 두 테이블에 모두 일치하는 데이터만 결합 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; * LEFT JOIN : 왼쪽 테이블의 데이터는 모두 출력하고, 오른쪽은 일치하는 것만 결합&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt; &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;☞ &lt;/span&gt;SELECT &lt;u&gt;users.name, orders.product&lt;/u&gt; FROM &lt;u&gt;users&lt;/u&gt; LEFT&amp;nbsp;JOIN&amp;nbsp;&lt;u&gt;orders&lt;/u&gt; ON&amp;nbsp;users.id&amp;nbsp;=&amp;nbsp;orders.user_id;&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 데이터는 정렬되지 않은 상태의 데이터를 저장한다. 원하는 데이터만 정렬해서 사용하기 때문이다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2. MySQL Workbench&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;※&lt;span style=&quot;color: #8A3DB6;&quot;&gt; 작성한 자료 내보내기 (CSV, JSON) : 작성한 파일에서 마우스 우클릭후 Table Data Exprot 선택 후 진행&lt;/span&gt;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnr7Ii/dJMcahZjvkg/T7aga0wjwE4PSiKf5T8ZWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnr7Ii/dJMcahZjvkg/T7aga0wjwE4PSiKf5T8ZWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnr7Ii/dJMcahZjvkg/T7aga0wjwE4PSiKf5T8ZWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnr7Ii%2FdJMcahZjvkg%2FT7aga0wjwE4PSiKf5T8ZWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;568&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.1. 터미널에서 실행 방법&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #8A3DB6;&quot;&gt;&amp;nbsp;mysql -u root -p&amp;nbsp; &amp;nbsp;-&amp;nbsp; &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;&lt;b&gt;비번입력 -&amp;nbsp; mysql&amp;gt; _____&amp;nbsp; &amp;nbsp;-&amp;gt; ; &lt;/b&gt;(세미콜론은 명령어 종료 의미, 터미널 종료시는 exti)&amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;nbsp;&lt;br&gt;- 워크벤치 화면 : 파일을 작성하기 위한 이름 설정하기&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;697&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ApkF9/dJMcahZjavr/eqTkgoHAfX9u2XT5wJtTv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ApkF9/dJMcahZjavr/eqTkgoHAfX9u2XT5wJtTv1/img.png&quot; data-alt=&quot;Workbench 화면 : 스키마 이름 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ApkF9/dJMcahZjavr/eqTkgoHAfX9u2XT5wJtTv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FApkF9%2FdJMcahZjavr%2FeqTkgoHAfX9u2XT5wJtTv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1061&quot; height=&quot;697&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;697&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Workbench 화면 : 스키마 이름 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 자료 입력후 코드 적용하기(Apply) 클릭&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLCRFI/dJMcai4WldW/JwBPwh3LM5fZxWEQNIelS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLCRFI/dJMcai4WldW/JwBPwh3LM5fZxWEQNIelS0/img.png&quot; data-alt=&quot;DB 입력 APPLY&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLCRFI/dJMcai4WldW/JwBPwh3LM5fZxWEQNIelS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLCRFI%2FdJMcai4WldW%2FJwBPwh3LM5fZxWEQNIelS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1854&quot; height=&quot;818&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DB 입력 APPLY&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 입력한 자료에서 행삭제시 Database 화면&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1613&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UeSR8/dJMcacjnF28/jYza5KsJhwCRc9HSjBuow1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UeSR8/dJMcacjnF28/jYza5KsJhwCRc9HSjBuow1/img.png&quot; data-alt=&quot;삭제행에 대한 Database&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UeSR8/dJMcacjnF28/jYza5KsJhwCRc9HSjBuow1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUeSR8%2FdJMcacjnF28%2FjYza5KsJhwCRc9HSjBuow1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1613&quot; height=&quot;798&quot; data-origin-width=&quot;1613&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;삭제행에 대한 Database&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 터미널에서 자료 조회하기&amp;nbsp;&lt;br&gt;&amp;nbsp; 부서(DEPT) 테이블에 저장된 모든 데이터(행)와 모든 열(컬럼)을 조회&amp;nbsp;:&lt;b&gt; SELECT * FROM DEPT;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; 260706스키마에서 멤버 조회하기 : &lt;b&gt;SELECT * new_schema_260706.member;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AOBWi/dJMcaiqotGr/kQ4ha3huR60XnKQblHrh5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AOBWi/dJMcaiqotGr/kQ4ha3huR60XnKQblHrh5K/img.png&quot; data-alt=&quot;터미널에 코드 입력후 DB 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AOBWi/dJMcaiqotGr/kQ4ha3huR60XnKQblHrh5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAOBWi%2FdJMcaiqotGr%2FkQ4ha3huR60XnKQblHrh5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1546&quot; height=&quot;922&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;터미널에 코드 입력후 DB 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 부서(DEPT) 테이블에서 부서 이름(DNAME) 데이터만 조회&amp;nbsp;: &lt;b&gt;SELECT DNAME FROM DEPT;&lt;/b&gt;&lt;br&gt;- DEPT (부서) 테이블에서 부서 이름(DNAME)과 부서 위치(LOC) 데이터를 조회하는 명령어&lt;br&gt;&lt;b&gt;&amp;nbsp; SELECT DNAME, LOC FROM DEPT;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;937&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cV1RIn/dJMcaiqotGv/oIPhQpNuMMQV7AEBiKEoJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cV1RIn/dJMcaiqotGv/oIPhQpNuMMQV7AEBiKEoJ0/img.png&quot; data-alt=&quot;SELECT 구문 활용 DB조회&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV1RIn/dJMcaiqotGv/oIPhQpNuMMQV7AEBiKEoJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcV1RIn%2FdJMcaiqotGv%2FoIPhQpNuMMQV7AEBiKEoJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;805&quot; height=&quot;937&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;937&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SELECT 구문 활용 DB조회&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- &lt;b&gt;EMP&lt;/b&gt; 테이블에서 급여(SAL)가 &lt;b&gt;1000 미만&lt;/b&gt;인 사원들의 이름(ENAME)만 조회하는 명령&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;b&gt; SELECT ENAME FROM EMP WERE SAL &amp;lt; 1000;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgsO11/dJMcajpdqEt/jCCtroksKkYRQmCDnlKiSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgsO11/dJMcajpdqEt/jCCtroksKkYRQmCDnlKiSk/img.png&quot; data-alt=&quot;DB 선별하여 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgsO11/dJMcajpdqEt/jCCtroksKkYRQmCDnlKiSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgsO11%2FdJMcajpdqEt%2FjCCtroksKkYRQmCDnlKiSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;815&quot; height=&quot;962&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DB 선별하여 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 'EMP'(직원) 테이블에서 직원의 이름(ENAME) 조건을 만족하는 모든 데이터를 조회 &amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;SELECT * FROM EMP WHERE ENAME LIKE '_ _I% ';&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAtkx8/dJMcahryuUx/68sEBQfe6KZsSN4TQYKKUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAtkx8/dJMcahryuUx/68sEBQfe6KZsSN4TQYKKUK/img.png&quot; data-alt=&quot;저장된 DB를 USE 사용하여 사용하도록 명령하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAtkx8/dJMcahryuUx/68sEBQfe6KZsSN4TQYKKUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAtkx8%2FdJMcahryuUx%2F68sEBQfe6KZsSN4TQYKKUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;423&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;저장된 DB를 USE 사용하여 사용하도록 명령하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 데이터베이스 (TEST)를 사용한다 :&lt;b&gt; USE TEST&lt;/b&gt;&lt;br&gt;- 'EMP'(직원) 테이블에서 직원의 이름(ENAME) 조건 ('_ _M%') 을 만족하는 모든 데이터를 조회 :&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; SELECT * FROM EMP WHERE ENAME LIKE '_ _M% ';&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5og9B/dJMcadWVOmH/V8qI3qgTL5ff645qU5xS3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5og9B/dJMcadWVOmH/V8qI3qgTL5ff645qU5xS3k/img.png&quot; data-alt=&quot;EMP 전체 데이터에서 이름이 세번째 글자가 M이고 뒤에는 모든값이 있는 번호 사람 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5og9B/dJMcadWVOmH/V8qI3qgTL5ff645qU5xS3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5og9B%2FdJMcadWVOmH%2FV8qI3qgTL5ff645qU5xS3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;855&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;855&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EMP 전체 데이터에서 이름이 세번째 글자가 M이고 뒤에는 모든값이 있는 번호 사람 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내림차순(DESC) 정렬하기 : &lt;b&gt;SELECT * FROM &lt;/b&gt;EMP &lt;b&gt;ORDER BY &lt;/b&gt;EMPNO&lt;b&gt; DESC;&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 오름차순(ASC) 정렬하기 : &lt;b&gt;SELECT * FROM &lt;/b&gt;EMP &lt;b&gt;ORDER BY &lt;/b&gt;EMPNO&lt;b&gt; ASC;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;965&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdEudb/dJMcafUNsKj/lNBpV0TK5Sl8qgvTxkig90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdEudb/dJMcafUNsKj/lNBpV0TK5Sl8qgvTxkig90/img.png&quot; data-alt=&quot;오름차순(ASC) 정렬하기, 내림차순(DESC) 정렬하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdEudb/dJMcafUNsKj/lNBpV0TK5Sl8qgvTxkig90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdEudb%2FdJMcafUNsKj%2FlNBpV0TK5Sl8qgvTxkig90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;965&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;965&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오름차순(ASC) 정렬하기, 내림차순(DESC) 정렬하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 이름순, 오름차순으로 정렬하기 : &lt;b&gt;SELECT * FROM&lt;/b&gt; EMP &lt;b&gt;ORDER BY&lt;/b&gt; ENAME;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mZk7s/dJMcacjnFjb/1dKKqc9OKkAXXsXiOKgxs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mZk7s/dJMcacjnFjb/1dKKqc9OKkAXXsXiOKgxs1/img.png&quot; data-alt=&quot;이름순으로 정렬하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mZk7s/dJMcacjnFjb/1dKKqc9OKkAXXsXiOKgxs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmZk7s%2FdJMcacjnFjb%2F1dKKqc9OKkAXXsXiOKgxs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;805&quot; height=&quot;547&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이름순으로 정렬하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- &quot;EMP(사원) 테이블의 모든 데이터를 조회하여 입사일(HIREDATE)을 기준으로 오름차순(과거 -&amp;gt; 최근 순) 정렬 출력 :&lt;br&gt;&amp;nbsp; &amp;nbsp;&lt;b&gt;SELECT&lt;/b&gt; * &lt;b&gt;FORM&lt;/b&gt; EMP &lt;b&gt;ORDER BY&lt;/b&gt; HIREDATE;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deBNjY/dJMcaiDXw78/mK8waC2w5QWE8uhnHh2ygk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deBNjY/dJMcaiDXw78/mK8waC2w5QWE8uhnHh2ygk/img.png&quot; data-alt=&quot;입사일 기준으로 정렬하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deBNjY/dJMcaiDXw78/mK8waC2w5QWE8uhnHh2ygk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeBNjY%2FdJMcaiDXw78%2FmK8waC2w5QWE8uhnHh2ygk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;805&quot; height=&quot;547&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;입사일 기준으로 정렬하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- &quot;EMP(직원) 테이블에서 급여가 2000 미만인 직원의 이름, 입사일, 급여를 조회하되, 입사일이 빠른 순서대로 정렬하기&lt;br&gt;&amp;nbsp; &lt;b&gt;SELECT&lt;/b&gt; ENAME, HIREDATE, SAL &lt;b&gt;FROM&lt;/b&gt; EMP &lt;b&gt;WHERE&lt;/b&gt; SAL &lt;b&gt;&amp;lt;&lt;/b&gt; 2000 &lt;b&gt;ORDER BY&lt;/b&gt; HIREDATE&lt;b&gt; ASC;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2zqTo/dJMcaccEq2S/XeT2TOB8TLSDgxEAuTUGMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2zqTo/dJMcaccEq2S/XeT2TOB8TLSDgxEAuTUGMK/img.png&quot; data-alt=&quot;복합적인 DB 조회하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2zqTo/dJMcaccEq2S/XeT2TOB8TLSDgxEAuTUGMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2zqTo%2FdJMcaccEq2S%2FXeT2TOB8TLSDgxEAuTUGMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;756&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;복합적인 DB 조회하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/c# c++</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/83</guid>
      <comments>https://goodjop79.tistory.com/83#entry83comment</comments>
      <pubDate>Mon, 6 Jul 2026 18:59:38 +0900</pubDate>
    </item>
    <item>
      <title>D+62[C+MySQL] DataBase, EED 에 대한 이해</title>
      <link>https://goodjop79.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;7월&amp;nbsp;6일(월)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 주 제 : MySQL DataBase &amp;amp; ERD, EED 개념 이해&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1. 주요 강조 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Database는 암기가 기본이다. SQL문법을 외우고 시작하라&lt;/li&gt;
&lt;li&gt;Class를 배울때와 같이 SQL 기본문법을 이해하고 있어야 한다.&lt;/li&gt;
&lt;li&gt;백업, 보안 : DBMS가 있어서 회사마다 다른 보안정책이 있다. 그러한 서비스를 유료로 해결 솔루션까지 판매한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;실습간 예시 HOST는 000 내에서만 접속이 가능하다.&lt;/li&gt;
&lt;li&gt;워크벤치 : + 아이콘 : 새로운 커넥션을 만들어서 다른 팀원의 DB로 접속이 가능하다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;서버와 데이터베이스를 연동해서 DB를 분석한다.&lt;/li&gt;
&lt;li&gt;로그인, 회원가입, DB연동해서 할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;파이참에 AI연동 해서 활용하도록 변화하고 있다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;LLM기반 바이브코딩을 하는 개발자들은 다시 기본부터 로직을 배우고 있다&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;기초지식을&amp;nbsp;알고&amp;nbsp;시작해야&amp;nbsp;원하는&amp;nbsp;개발을&amp;nbsp;진행할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;li&gt;지금까지 프로젝트한 내용들과는 다른점은 &quot;테이블&quot;이라고 하는 물리적 공간에 데이터를 넣어서 관리하는 것만 다르다.&lt;/li&gt;
&lt;li&gt;프로그래밍언어에 데이터베이스를 연동시키면 된다.&lt;/li&gt;
&lt;li&gt;프로그램에서 모델이란 &quot;&quot;데이터&quot;&quot;이다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;테이블 : 표 형태로 구성된 2차원 구조, 열과 행으로 구성됨&lt;/li&gt;
&lt;li&gt;물리적인 테이블 DB를 처리하는 것이 DBMS(MySQL)이다.&lt;/li&gt;
&lt;li&gt;데이터베이스 모델링 툴이 많이 있다.&amp;nbsp; (예, MYSQL Workbench)&lt;/li&gt;
&lt;li&gt;테이블 만들고 나면 EED다이어그램 자동으로 만들어 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byjONH/dJMb99NIjmy/mvhD9ygevbg80K4m54Lmk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byjONH/dJMb99NIjmy/mvhD9ygevbg80K4m54Lmk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byjONH/dJMb99NIjmy/mvhD9ygevbg80K4m54Lmk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyjONH%2FdJMb99NIjmy%2FmvhD9ygevbg80K4m54Lmk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;379&quot; height=&quot;150&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 데이터 베이스&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 1. 엔티티 관계 다이어그램(ERD) &lt;/b&gt;&lt;br /&gt;&lt;b&gt;데이터베이스의 구조를 나타내는&lt;/b&gt; 시각적 청사진으로 &lt;b&gt;데이터 객체(엔티티)가 시스템 내에서 어떻게 상호 작용&lt;/b&gt;하는지를 세 가지 주요 구성 요소, 즉 &lt;b&gt;엔티티 (객체 또는 테이블), 속성 (특성 또는 열), 관계 (연결 및 카디널리티)를 사용하여 설명한다. &lt;/b&gt;&lt;br /&gt;ERD는 일반적으로 세 가지 추상화 단계를 거쳐 진행된다.&lt;br /&gt;① 개념적 ERD : 비즈니스 개념과 범위를 논의하는 데 사용되는 고수준 개요&lt;br /&gt;② 논리적 ERD : 기본 키 및 외래 키와 같은 운영 및 트랜잭션 세부 정보를 추가하지만 데이터베이스에 종속되지 않는다.&lt;br /&gt;③ 물리적 ERD : 특정 데이터베이스 관리 시스템에 맞춰 테이블 이름, 데이터 유형, 인덱스 등을 구체적으로 명시한 최종 구현 준비 완료 도면이다.&lt;br /&gt;2.2. 두가지 방식 비교&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 97.093%; height: 175px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.9061%; text-align: center;&quot;&gt;&lt;b&gt;구&amp;nbsp; &amp;nbsp; 분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.976%; text-align: center;&quot;&gt;&lt;b&gt;워터풀(Waterfall) 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 41.1178%; text-align: center;&quot;&gt;&lt;b&gt;애자일(Agile) 방식&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.9061%; text-align: center;&quot;&gt;계획 방식&lt;/td&gt;
&lt;td style=&quot;width: 42.976%; text-align: center;&quot;&gt;초기단계에 고정된 세부 계획 수립&lt;/td&gt;
&lt;td style=&quot;width: 41.1178%; text-align: center;&quot;&gt;유동적이고 지속적인 계획 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.9061%; text-align: center;&quot;&gt;진행 흐름&lt;/td&gt;
&lt;td style=&quot;width: 42.976%; text-align: center;&quot;&gt;요구분석 - 설계 - 개발 - 테스트 (순차적)&lt;/td&gt;
&lt;td style=&quot;width: 41.1178%; text-align: center;&quot;&gt;계획 - 개발 - 테스트 - 피드백(반복적)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.9061%; text-align: center;&quot;&gt;요구사항 변경&lt;/td&gt;
&lt;td style=&quot;width: 42.976%; text-align: center;&quot;&gt;변경이 어렵고 비용이 많이 든다&lt;/td&gt;
&lt;td style=&quot;width: 41.1178%; text-align: center;&quot;&gt;상시 수용 및 빠른 반영이 가능하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.9061%; text-align: center;&quot;&gt;결과물 확인&lt;/td&gt;
&lt;td style=&quot;width: 42.976%; text-align: center;&quot;&gt;프로젝트의 최종 마무리 단계&lt;/td&gt;
&lt;td style=&quot;width: 41.1178%; text-align: center;&quot;&gt;짧은 주기마다 작동하는 제품 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 2. 데이터베이스(DB) 핵심 개념 요약&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;MySQL 개요 :&lt;/b&gt; 데이터를 효율적으로 저장&amp;middot;관리하는 오픈소스 관계형 데이터베이스 관리 시스템(RDBMS)으로, 구조화 질의 언어(SQL)를 통해 제어한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;핵심 용어 정리&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;(예) 개발산출물 중 테이블명세서
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,1,0,0&quot;&gt;테이블(Table) :&lt;/b&gt; 행(Row)과 열(Column)로 구성된 데이터 저장의 기본 단위&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,1,1,0&quot;&gt;기본 키(PK) :&lt;/b&gt; 각 행을 고유하게 식별하는 값 (중복 및 빈 값 불가)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,1,2,0&quot;&gt;외래 키(FK) :&lt;/b&gt; 테이블 간의 관계를 맺기 위해 다른 테이블의 PK와 연결하는 열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 3. 핵심 SQL 문법 (CRUD 및 확장)&lt;/b&gt;&lt;br /&gt;가.&amp;nbsp; DML (데이터 조작어) 기본&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조회 (SELECT) :&lt;/b&gt; SELECT 컬럼 FROM 테이블 WHERE 조건;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삽입 (INSERT) :&lt;/b&gt; INSERT INTO 테이블 (컬럼) VALUES (값);&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정 (UPDATE) :&lt;/b&gt; UPDATE 테이블 SET 컬럼 = 새값 WHERE 조건;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제 (DELETE) :&lt;/b&gt; DELETE FROM 테이블 WHERE 조건;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나.&amp;nbsp; 자주 쓰는 확장 문법 및 실습 규칙&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;조건절 (WHERE):&lt;/b&gt; LIKE '%김%' (패턴 매칭), BETWEEN A AND B (범위), IN (값1, 값2) (포함) 활용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;정렬 (ORDER BY):&lt;/b&gt; DB는 기본적으로 정렬되지 않은 상태로 데이터를 저장하므로 조회 시 정렬이 중요
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DESC: 내림차순 정렬&lt;/li&gt;
&lt;li&gt;ASC: 오름차순 정렬 (생략 시 기본값으로 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,2,0&quot;&gt;그룹화 (GROUP BY):&lt;/b&gt; 특정 기준별 집계 데이터를 도출 (COUNT, SUM 등과 연계).&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,3,0&quot;&gt;테이블 결합 (JOIN):&lt;/b&gt; INNER JOIN(교집합 데이터만)과 LEFT JOIN(왼쪽 테이블 기준 전체 출력)으로 다중 테이블 연계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3. IT 기술 및 시장 트렌드 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.1.&amp;nbsp; RDBMS 시장 동향&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ORACLE :&lt;/b&gt; 전 세계 대기업, 금융권, 공공기관의 핵심 시스템을 독점하는 1위 DB. 강력한 트랜잭션 및 보안성을 자랑하지만&amp;nbsp; 라이선스 비용이 매우 고가임.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL Server (T-SQL) :&lt;/b&gt; 마이크로소프트(MS)의 자산, Windows 서버 환경과 최고의 호환성을 가진 엔터프라이즈급 고가 DB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MySQL &amp;amp; MariaDB :&lt;/b&gt; 오라클이 MySQL을 인수하자, 오픈소스 정신을 지키려던 핵심 개발자들이 MySQL을 기반으로 분가(Fork)하여 만든 것이 &lt;b&gt;MariaDB&lt;/b&gt;. 두 DB는 상호 높은 호환성을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.1.&amp;nbsp; ERD &amp;amp; EED&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ERD(Entity-Relationship&amp;nbsp;Diagram)는&amp;nbsp;데이터베이스&amp;nbsp;설계의&amp;nbsp;핵심&amp;nbsp;도구&lt;/b&gt;이며,&amp;nbsp;&lt;b&gt;EED는&amp;nbsp;일반적인&amp;nbsp;IT&amp;nbsp;문서&amp;nbsp;체계에서&amp;nbsp;다루어지는&amp;nbsp;전기&amp;middot;전자&amp;nbsp;분야&amp;nbsp;문서&lt;/b&gt;(Electrical&amp;nbsp;Engineering&amp;nbsp;Document)&amp;nbsp;또는&amp;nbsp;특수한&amp;nbsp;엔터티&amp;nbsp;구조도(예:&amp;nbsp;E-R&amp;nbsp;모델의&amp;nbsp;확장인&amp;nbsp;EER&amp;nbsp;등)를&amp;nbsp;의미한다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.&amp;nbsp;ERD&amp;nbsp;(개체-관계&amp;nbsp;다이어그램)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; ERD(Entity-Relationship Diagram)는 데이터베이스 설계의 가장 기초이자 핵심적인 모델링 도구. 복잡한 데이터 구조를 직관적으로 시각화하여 개발자, 기획자, 이해관계자 간의 원활한 의사소통을 돕는다.&lt;br /&gt;- 엔티티 (Entity): 저장할 가치가 있는 사람, 장소, 물건 등 데이터의 기본 단위 (예: 회원, 상품)&lt;br /&gt;- 속성 (Attribute): 엔티티가 가지는 세부 정보 및 특성 (예: 회원의 이름, 이메일)&lt;br /&gt;- 관계 (Relationship): 엔티티 간의 연결 상태로 데이터의 흐름을 정의 (예: 고객은 주문을 '한다')&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;EED&amp;nbsp;(전기&amp;middot;전자&amp;nbsp;문서&amp;nbsp;체계)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp;일반적인 IT 및 엔지니어링 문서 체계에서 EED(Electrical and Electronic Documentation)는 전기 및 전자 하드웨어 설계의 핵심 요소와 관련된 표준화된 기술 문서 및 도면을 의미.&lt;br /&gt;- 회로도 (Schematic Diagram): 전기&amp;middot;전자 부품 간의 연결과 회로의 동작 원리를 기호와 선으로 표현한 도면.&lt;br /&gt;- 부품 명세서 (BOM, Bill of Materials): 회로를 구성하는 데 필요한 모든 전기, 전자 부품의 품명, 규격, 수량 등을 정리한 문서&lt;br /&gt;- PCB 레이아웃 (PCB Layout): 회로도를 실제 인쇄 회로 기판(PCB) 위에 구현하기 위해 부품의 배치와 구리 배선의 경로를 설계한 문서&lt;br /&gt;- 블록 다이어그램 (Block Diagram): 복잡한 시스템의 전체적인 구조와 신호 흐름을 단순한 사각형 블록으로 나누어 계층적으로 표현한 기술 문서&lt;br /&gt;- 데이터시트 (Datasheet): 개별 전자 부품의 전기적 특성, 동작 전압, 핀 배열, 성능 한계 등을 명시한 기술 사양 문서.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습 로그/c# c++</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/82</guid>
      <comments>https://goodjop79.tistory.com/82#entry82comment</comments>
      <pubDate>Mon, 6 Jul 2026 17:34:23 +0900</pubDate>
    </item>
    <item>
      <title>D+61[DBMS] 쿼리(Query)에 대한 소개</title>
      <link>https://goodjop79.tistory.com/81</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;7월&amp;nbsp;3일(금)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ&amp;nbsp;주&amp;nbsp;제&amp;nbsp;:&amp;nbsp; MySQL 빅쿼리에 대한 이해 학습&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;131&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceFhrx/dJMcag0nzm1/tTGjdb8gWkiAAcGoukcNH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceFhrx/dJMcag0nzm1/tTGjdb8gWkiAAcGoukcNH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceFhrx/dJMcag0nzm1/tTGjdb8gWkiAAcGoukcNH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceFhrx%2FdJMcag0nzm1%2FtTGjdb8gWkiAAcGoukcNH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;146&quot; height=&quot;125&quot; data-origin-width=&quot;131&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 참고 사항&amp;nbsp;&lt;/b&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- MySQL을 설치해 놓고 언제 어디서든 서버에 접근이 가능하다는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;(예시) Hostname: 127.0.0.0.&amp;nbsp; 예시 Port: 5000&lt;br /&gt;-&amp;nbsp;DB서버는&amp;nbsp;1인만&amp;nbsp;있으면&amp;nbsp;되고,&amp;nbsp;나머지는&amp;nbsp;서버에&amp;nbsp;접속하면&amp;nbsp;된다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;IP로&amp;nbsp;컴터를&amp;nbsp;찾고&amp;nbsp;포트를&amp;nbsp;찾아서&amp;nbsp;다른이의&amp;nbsp;서버에&amp;nbsp;접속을&amp;nbsp;하여&amp;nbsp;자료를&amp;nbsp;공유한다.&lt;br /&gt;-&amp;nbsp;개인마다&amp;nbsp;설정이&amp;nbsp;달라서&amp;nbsp;설정을&amp;nbsp;수정해&amp;nbsp;줘야&amp;nbsp;한다&lt;br /&gt;- 데이터 주권 확보, 토큰 경쟁, IT기본서(예)동물책&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;예)&amp;nbsp;내&amp;nbsp;교재를&amp;nbsp;만드는&amp;nbsp;타입,&amp;nbsp;개념을&amp;nbsp;알고&amp;nbsp;답을&amp;nbsp;만드는&amp;nbsp;타입&lt;br /&gt;-&amp;nbsp;혼공SQL&amp;nbsp;:&amp;nbsp;기초문법&amp;nbsp;이론&amp;nbsp;공부하기&amp;nbsp;(어디에&amp;nbsp;뭐가&amp;nbsp;있는지&amp;nbsp;알아야&amp;nbsp;한다)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://goodjop79.tistory.com/45&quot;&gt;2026.05.26 - [인공지능 AI] - D+33 [SQL] 빅쿼리(BigQuery)가 뭐야?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783390922259&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;D+33 [SQL]  빅쿼리(BigQuery)가 뭐야?&quot; data-og-description=&quot;ㅇ 일 자 : 2026년 5월 22일(금) 빅쿼리(BigQuery)란? 구글 클라우드 플랫폼(GCP)의 가장 강력한 무기이자, 데이터 엔지니어링과 분석 분야에서 업계 표준으로 자리 잡은 빅쿼리(BigQuery) 1. 빅쿼리(BigQuery)&quot; data-og-host=&quot;goodjop79.tistory.com&quot; data-og-source-url=&quot;https://goodjop79.tistory.com/45&quot; data-og-url=&quot;https://goodjop79.tistory.com/45&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bJCfut/dJMb9kmnXE5/XD7V0vEngsgAdPJbW71TZk/img.png?width=167&amp;amp;height=136&amp;amp;face=0_0_167_136,https://scrap.kakaocdn.net/dn/dpDysf/dJMb9lMmRWJ/YtdnhlEdJ9RWfOyMiuTg20/img.png?width=167&amp;amp;height=136&amp;amp;face=0_0_167_136,https://scrap.kakaocdn.net/dn/bVxLei/dJMb9lMmRWK/cA7wTgqMN2P3tLfJHhmJJ0/img.png?width=1408&amp;amp;height=768&amp;amp;face=0_0_1408_768&quot;&gt;&lt;a href=&quot;https://goodjop79.tistory.com/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://goodjop79.tistory.com/45&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bJCfut/dJMb9kmnXE5/XD7V0vEngsgAdPJbW71TZk/img.png?width=167&amp;amp;height=136&amp;amp;face=0_0_167_136,https://scrap.kakaocdn.net/dn/dpDysf/dJMb9lMmRWJ/YtdnhlEdJ9RWfOyMiuTg20/img.png?width=167&amp;amp;height=136&amp;amp;face=0_0_167_136,https://scrap.kakaocdn.net/dn/bVxLei/dJMb9lMmRWK/cA7wTgqMN2P3tLfJHhmJJ0/img.png?width=1408&amp;amp;height=768&amp;amp;face=0_0_1408_768');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;D+33 [SQL] 빅쿼리(BigQuery)가 뭐야?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ㅇ 일 자 : 2026년 5월 22일(금) 빅쿼리(BigQuery)란? 구글 클라우드 플랫폼(GCP)의 가장 강력한 무기이자, 데이터 엔지니어링과 분석 분야에서 업계 표준으로 자리 잡은 빅쿼리(BigQuery) 1. 빅쿼리(BigQuery)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;goodjop79.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/81</guid>
      <comments>https://goodjop79.tistory.com/81#entry81comment</comments>
      <pubDate>Mon, 6 Jul 2026 09:05:21 +0900</pubDate>
    </item>
    <item>
      <title>D+60[개인학습] 개인 포트폴리오 작성</title>
      <link>https://goodjop79.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 2일(목)&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 주 제 : 개인 포트폴리오 작성&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baaxyP/dJMb99UwSHM/yjTEe8IIfySAx5mhNcTo8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baaxyP/dJMb99UwSHM/yjTEe8IIfySAx5mhNcTo8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baaxyP/dJMb99UwSHM/yjTEe8IIfySAx5mhNcTo8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaaxyP%2FdJMb99UwSHM%2FyjTEe8IIfySAx5mhNcTo8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;228&quot; height=&quot;105&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 학습 참고 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI와 기술, 디자인을 알고 발빠르게 대처할수 있게 함&amp;nbsp;&lt;/li&gt;
&lt;li&gt;개인화 서비스가&amp;nbsp;더욱&amp;nbsp;고도화&amp;nbsp;되고&amp;nbsp;사용자가&amp;nbsp;원하는&amp;nbsp;데로&amp;nbsp;화면을&amp;nbsp;지정함(위젯)&lt;/li&gt;
&lt;li&gt;제로 버튼 시대가 오며 플로우는 댑스가 짧아지고 UI 디자인은 더욱 강조될 것&lt;/li&gt;
&lt;li&gt;사용자 상황을 반영한 몰입형 UI 인터페이스가 확대될 것&lt;/li&gt;
&lt;li&gt;디바이스와 별개로 어떤 환경에서도 디자인이 최적화 됨&lt;/li&gt;
&lt;li&gt;Card 디자인은 앞으로도 계속 유지되며 더욱 고도화됨&lt;/li&gt;
&lt;li&gt;일체형 디자인으로 끊기지 않는 시선 방향 제시&lt;/li&gt;
&lt;li&gt;OpenCV :컴퓨터 비젼 목적의 프로그램 라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://goodjop79.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2026.04.29 - [학습 로그] - D+19 [Portfolio] 포트폴리오 작성 Tip&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783390831713&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;D+19 [Portfolio] 포트폴리오  작성 Tip&quot; data-og-description=&quot;ㅇ 일 자 : 2026년 4월 29일(수)ㅇ 내 용※ 포트폴리오(Portfolio) 목적에 따른 세 가지 분류취업 / 입시 : 자신의 이력, 경력, 실력을 보여주기 위해 과거에 만든 작품이나 결과물을 모아놓은 자료철투&quot; data-og-host=&quot;goodjop79.tistory.com&quot; data-og-source-url=&quot;https://goodjop79.tistory.com/27&quot; data-og-url=&quot;https://goodjop79.tistory.com/27&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cZHbml/dJMb9fZGKdD/r8XG87Tj4ZIEF8ysenPNl1/img.png?width=248&amp;amp;height=134&amp;amp;face=0_0_248_134,https://scrap.kakaocdn.net/dn/LpJFY/dJMb8Yp6Vqw/hvrjeP88qCPAM2vZOjdvjK/img.png?width=248&amp;amp;height=134&amp;amp;face=0_0_248_134,https://scrap.kakaocdn.net/dn/wLa0h/dJMb9cBTn3E/QAN663kpZK6pdUpNTf8kQ1/img.png?width=1408&amp;amp;height=768&amp;amp;face=0_0_1408_768&quot;&gt;&lt;a href=&quot;https://goodjop79.tistory.com/27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://goodjop79.tistory.com/27&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cZHbml/dJMb9fZGKdD/r8XG87Tj4ZIEF8ysenPNl1/img.png?width=248&amp;amp;height=134&amp;amp;face=0_0_248_134,https://scrap.kakaocdn.net/dn/LpJFY/dJMb8Yp6Vqw/hvrjeP88qCPAM2vZOjdvjK/img.png?width=248&amp;amp;height=134&amp;amp;face=0_0_248_134,https://scrap.kakaocdn.net/dn/wLa0h/dJMb9cBTn3E/QAN663kpZK6pdUpNTf8kQ1/img.png?width=1408&amp;amp;height=768&amp;amp;face=0_0_1408_768');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;D+19 [Portfolio] 포트폴리오 작성 Tip&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ㅇ 일 자 : 2026년 4월 29일(수)ㅇ 내 용※ 포트폴리오(Portfolio) 목적에 따른 세 가지 분류취업 / 입시 : 자신의 이력, 경력, 실력을 보여주기 위해 과거에 만든 작품이나 결과물을 모아놓은 자료철투&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;goodjop79.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>학습 로그</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/80</guid>
      <comments>https://goodjop79.tistory.com/80#entry80comment</comments>
      <pubDate>Mon, 6 Jul 2026 09:04:23 +0900</pubDate>
    </item>
    <item>
      <title>D+60[Project] Team Project. Chatting app - &amp;quot;CodeLike&amp;quot;(개발 완료)</title>
      <link>https://goodjop79.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 2일(목)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 주 제 : Team Project Chatting app &quot;CodeLike&quot; (사내 채팅앱)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 프로젝트 간단 소개&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-02 10-05-13.png&quot; data-origin-width=&quot;1259&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdNJ3F/dJMcac4I5D9/gC9P2yE8upQP7q9o8nQ3cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdNJ3F/dJMcac4I5D9/gC9P2yE8upQP7q9o8nQ3cK/img.png&quot; data-alt=&quot;사내 개발자 메신저&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdNJ3F/dJMcac4I5D9/gC9P2yE8upQP7q9o8nQ3cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdNJ3F%2FdJMcac4I5D9%2FgC9P2yE8upQP7q9o8nQ3cK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;135&quot; data-filename=&quot;스크린샷 2026-07-02 10-05-13.png&quot; data-origin-width=&quot;1259&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사내 개발자 메신저&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.74419%; text-align: center;&quot;&gt;&lt;span&gt;&lt;span&gt;개발&lt;br /&gt;목적&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 93.1395%;&quot;&gt;&lt;span&gt;1. &lt;span&gt;팀 단위 실시간 소통 : 대형 앱 대신 팀 규모에 최적화된 소형 앱을 통해 프로젝트별 방 개설, 소스코드 및 파일 전송, &lt;br /&gt;&amp;nbsp; &amp;nbsp;실시간 알림 기능으로 신속한 의견 교환 환경을 구축하기 위함입니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;2. &lt;span&gt;보안 및 기밀 유지 : 외부망 노출 위험이 있는 소스코드와 핵심 개발 자료를 사&lt;/span&gt;&lt;span&gt;내 메신저 내에서 안전하게 공유하고 &lt;br /&gt;&amp;nbsp; &amp;nbsp;실시간으로 문제를 해결하고자 합니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;3. &lt;span&gt;업무 효율 및 역량 강화: AI 활용으로 회의 시간을 단축해 개발 집중 시간을 확보하며, 신입 개발자는 채팅 기반의 업무 &lt;br /&gt;&amp;nbsp; &amp;nbsp; 흐름을 모니터링하며 자연스럽게 역량을 강화하고자 합니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;4. &lt;span&gt;개발 및 UI 환경 : Linux와 Windows를 모두 지원하는 Python3 및 PySide6 기반으로 개발하며, 직관적인 Windows &lt;br /&gt;&amp;nbsp; &amp;nbsp; 응용프로그램 스타일의 인터페이스를 제공하기 위함입니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.74419%; text-align: center;&quot;&gt;&lt;span&gt;&lt;span&gt;추진배경&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 93.1395%;&quot;&gt;&lt;span&gt;&lt;span&gt;국가 주요 사업인 인공지능 응용시스템 개발자 양성과정 중, 개발자들의 소통&amp;nbsp; 채널이 외부망에만 의존하고 있다는 점에 착안하여 전용 &amp;lsquo;사내 채팅앱&amp;rsquo;의 필요성을 느꼈습니다. 개발자들이 업무나 학습에 몰입하면서도 채팅을 통해 실시간으로 의문점을 해결하고, 간접 지식을 공유할 수 있도록 지원하며, 나아가 채팅앱을 확장하여 개발자들만의 긴밀한 의견 공유와 상호 교류가 가능한 &lt;/span&gt;&lt;span&gt;독자적인&amp;nbsp;&amp;nbsp; 커뮤니티 공간&lt;/span&gt;&lt;span&gt;을 구축하고자 개발을 추진하게 되었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 6.74419%; text-align: center;&quot;&gt;&lt;span&gt;&lt;span&gt;개발 환경&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 93.1395%;&quot;&gt;&lt;span&gt;&lt;span&gt;1. OS / ver : Ubuntu Linux / Windows10,11 / Python 3.12&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;2. 서버 : 내부망 전용, Linux Internal Server, Windows Internal Server&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;3. 구조 : Client + Server + Protocol, JSON&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;4. 패턴 : MVC + Service Layer &lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;1. 개발 산출물 : 별도 첨부 유지&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[개발 완료 단계] 프로젝트 완료 보고서&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트 소개&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로그램 구현 영상 및 스크린샷&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 요구사항 명세서&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 유스케이스&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 순 서 도&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 메뉴 구조도&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개발 일정표&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로그램 구현 파일 :&amp;nbsp; 패키지 파일 목록, 소스 코드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로그램 개발 후기&amp;nbsp; 및 개발일지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.&amp;nbsp;API 명세서를 작성하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API명세서를 작성하지 않으면 사용자는 어떤 정보를 어떤 방식으로 요청해야하는지, 어떤 정보를 전달받을 수 있을지, 어떤 형식으로 데이터가 넘어오는지 알 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1.&amp;nbsp;API 명세서 작성법 (예시)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d10uVQ/dJMcaiDUi4K/7VdKbWGvJGNvA5icOWVSCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d10uVQ/dJMcaiDUi4K/7VdKbWGvJGNvA5icOWVSCk/img.png&quot; data-alt=&quot;출처 : velog.io&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d10uVQ/dJMcaiDUi4K/7VdKbWGvJGNvA5icOWVSCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd10uVQ%2FdJMcaiDUi4K%2F7VdKbWGvJGNvA5icOWVSCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;537&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : velog.io&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2.2. 핵 심&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Method, URI, Request body, Response body, Description&lt;span&gt;&amp;nbsp;&lt;/span&gt;속성은 되도록이면 포함되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Method: HTTP Method을 의미한다. (GET, POST, PUT, DELETE 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- URI: 인터넷의 자원을 식별할 수 있는 문자열 또는 주소&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Request Body: API에 제공할 데이터 (요청할 데이터 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 주로 POST, PUT일 때 함께 작성됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Response Body: API가 생성하는 응답 데이터&lt;br /&gt;- 일반적으로 JSON 및 XML 형식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Description: 상세 설명&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;** 용어 설명&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;Status: API 응답에서 반환되는 상태 코드와 설명 (예: 200 - OK, 400 - Bad Request 등).&lt;br /&gt;params: Query params, Path params 등&lt;br /&gt;Headers: Content-Type, Authorization 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 구현 가능한 코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;3.1. &lt;b&gt;&lt;b&gt;protocol.py (기본형)&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782954046219&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#=========== 기본형 툴 참고 ===========
# 기본형 (protocol.py) send_message, receive_message / 송수신 메세지 만 단순 활용시
&quot;&quot;&quot;
import json

ENCODING = &quot;utf-8&quot;
DELIMITER = &quot;\n&quot;


def send_message(sock, message):
    json_text = json.dumps(message, ensure_ascii=False)  # 메시지를 JSON 문자열로 변환
    data = json_text + DELIMITER  # 끝에 줄바꿈을 붙여 메시지 경계 표시
    sock.sendall(data.encode(ENCODING))  # 바이트로 바꿔 전부 전송


def receive_messages(sock, buffer):  # 25강에서 사용 (이번 강의에서는 호출 안 함)
    data = sock.recv(1024)

    if not data:
        return buffer, [], False

    buffer += data.decode(ENCODING)
    messages = []

    while DELIMITER in buffer:
        raw_message, buffer = buffer.split(DELIMITER, 1)

        if raw_message.strip():
            message = json.loads(raw_message)
            messages.append(message)

    return buffer, messages, True
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;3.2. protocol.py (1형)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782953879978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 단순응용형 (protocol.py) send_message, receive_message / 송수신 메세지 활용시
# 구현기능 포함요소: 소켓통신 및 파일전송 프로토콜 모듈,JSON 기반 메시지 송수신, Base64 기반 파일 인코딩/디코딩 및 송수신, 예외 오류처리 포함

import json
import base64
import os
import socket
from typing import Tuple, List, Dict, Any, Optional

ENCODING = &quot;utf-8&quot;
DELIMITER = &quot;\n&quot;


def send_message(sock: socket.socket, message: Dict[str, Any]) -&amp;gt; bool:
    # 딕셔너리 형태의 메시지를 JSON으로 변환하여 소켓으로 전송한다.

    try:
        json_text = json.dumps(message, ensure_ascii=False)
        data = json_text + DELIMITER
        sock.sendall(data.encode(ENCODING))
        return True
    except socket.error as e:
        print(f&quot;[오류] 서버로 데이터를 전송하는데 실패했습니다: {e}&quot;)
        return False
    except Exception as e:
        print(f&quot;[오류] 알 수 없는 전송 오류가 발생했습니다: {e}&quot;)
        return False


def receive_messages(sock: socket.socket, buffer: str) -&amp;gt; Tuple[str, List[Dict[str, Any]], bool]:
    # 소켓으로부터 데이터를 수신하여 완전한 JSON 메세지 리스트로 반환한다.

    try:
        data = sock.recv(4096)

        if not data:
            return buffer, [], False

        buffer += data.decode(ENCODING)
        messages = []

        while DELIMITER in buffer:
            raw_message, buffer = buffer.split(DELIMITER, 1)

            if raw_message.strip():
                try:
                    message = json.loads(raw_message)
                    messages.append(message)
                except json.JSONDecodeError:
                    print(&quot;[오류] 수신된 데이터의 JSON 형식이 올바르지 않습니다.&quot;)

        return buffer, messages, True

    except ConnectionResetError:
        print(&quot;[오류] 상대방과의 연결이 끊어졌습니다.&quot;)
        return buffer, [], False
    except Exception as e:
        print(f&quot;[오류] 메시지 수신 중 오류가 발생했습니다: {e}&quot;)
        return buffer, [], False


def send_file(sock: socket.socket, filepath: str) -&amp;gt; bool:
    # 사용자가 선택한 파일을 읽어 Base64로 변환한 뒤 서버로 전송한다.
    # 1. 파일 미선택 처리
    if not filepath:
        print(&quot;[안내] 전송할 파일이 선택되지 않았습니다.&quot;)
        return False

    # 파일 존재 여부 확인
    if not os.path.exists(filepath):
        print(f&quot;[오류] 해당 경로에 파일이 존재하지 않습니다: {filepath}&quot;)
        return False

    # 2. 파일 정보 추출 및 빈 파일 처리
    filesize = os.path.getsize(filepath)
    if filesize == 0:
        print(&quot;[오류] 내용이 없는 빈 파일은 전송할 수 없습니다.&quot;)
        return False

    filename = os.path.basename(filepath)

    try:
        # 3. 파일을 Base64로 변환
        with open(filepath, &quot;rb&quot;) as f:
            file_data = f.read()

        encoded_data = base64.b64encode(file_data).decode('utf-8')

        # 4. 파일 메시지 구성
        message = {
            &quot;type&quot;: &quot;file&quot;,
            &quot;filename&quot;: filename,
            &quot;size&quot;: filesize,
            &quot;data&quot;: encoded_data
        }

        # 5. 파일 정보 표시 및 서버 전송
        print(f&quot;[발신] 파일 전송을 시작합니다... (파일명: {filename}, 크기: {filesize} bytes)&quot;)

        is_success = send_message(sock, message)

        if is_success:
            print(&quot;[안내] 파일 전송이 성공적으로 완료되었습니다.&quot;)
        else:
            print(&quot;[오류] 파일 전송에 실패했습니다.&quot;)

        return is_success

    except Exception as e:
        print(f&quot;[오류] 파일 처리 및 전송 과정에서 치명적인 오류가 발생했습니다: {e}&quot;)
        return False


def receive_file(message: Dict[str, Any], save_dir: str = &quot;./downloads&quot;) -&amp;gt; bool:
    # 수신된 파일 메세지를 디코딩하여 로컬 디렉토리에 저장

    if message.get(&quot;type&quot;) != &quot;file&quot;:
        return False

    filename = message.get(&quot;filename&quot;, &quot;unknown_file&quot;)
    filesize = message.get(&quot;size&quot;, 0)
    encoded_data = message.get(&quot;data&quot;, &quot;&quot;)

    # 파일 정보 표시
    print(f&quot;[수신] 파일을 수신했습니다. (파일명: {filename}, 크기: {filesize} bytes)&quot;)

    try:
        # 저장 디렉토리 생성
        if not os.path.exists(save_dir):
            os.makedirs(save_dir)

        save_path = os.path.join(save_dir, filename)

        # Base64 디코딩 및 파일 저장
        file_data = base64.b64decode(encoded_data)

        with open(save_path, &quot;wb&quot;) as f:
            f.write(file_data)

        print(f&quot;[안내] 파일이 성공적으로 저장되었습니다: {save_path}&quot;)
        return True

    except base64.binascii.Error:
        print(&quot;[오류] 수신된 파일의 Base64 디코딩에 실패했습니다. 데이터가 손상되었을 수 있습니다.&quot;)
        return False
    except Exception as e:
        print(f&quot;[오류] 파일 저장 중 오류가 발생했습니다: {e}&quot;)
        return False&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;3.3. protocol.py (2형)&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782953927202&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# protocol.py 
# 코드 요약 설명 :
 # TCP 소켓 통신 환경에서 딕셔너리 데이터를 주고받는 구조로 TCP는 데이터의 경계가 없는 스트림 프로토콜이기 때문에,
 # 메시지를 보낼 때 데이터의 크기(Header)를 먼저 보내고, 그 다음 실제 데이터(Payload)를 보내는 방식이 가장 안전하고 일반적
 # 서버 측에서도 메시지를 안전하게 수신할 수 있도록 receive_message 함수도 함께 포함한다.

import json
import socket

def send_message(sock: socket.socket, msg_dict: dict):
# 딕셔너리 데이터를 JSON으로 변환하여 TCP 소켓으로 안전하게 전송한다. 4바이트 헤더(데이터 길이) + 데이터 바디 형태로 전송한다
    
    try:
        # 1. 딕셔너리를 JSON 문자열로 변환 후 bytes로 인코딩
        json_data = json.dumps(msg_dict, ensure_ascii=False).encode('utf-8')
        
        # 2. 데이터의 총 길이를 구함
        data_length = len(json_data)
        
        # 3. 4바이트 고정 길이 헤더 생성 (빅 엔디안 방식)
        header = data_length.to_bytes(4, byteorder='big')
        
        # 4. 헤더와 바디를 순서대로 전송
        sock.sendall(header + json_data)
        
    except Exception as e:
        raise RuntimeError(f&quot;메시지 전송 중 오류 발생: {e}&quot;)

def receive_message(sock: socket.socket) -&amp;gt; dict:
# 소켓으로부터 [4바이트 헤더 + 데이터 바디]를 읽어와 딕셔너리로 역직렬화, 서버측 코드나 클라이언트 수신 루프에서 사용하기 좋다.

    try:
        # 1. 4바이트 헤더(길이 정보) 수신
        header = _recv_all(sock, 4)
        if not header:
            return {&quot;type&quot;: &quot;exit&quot;, &quot;reason&quot;: &quot;연결이 끊어졌습니다.&quot;}
            
        # 2. 헤더로부터 데이터 크기 파싱
        data_length = int.from_bytes(header, byteorder='big')
        
        # 3. 데이터 크기만큼 바디 수신
        body_data = _recv_all(sock, data_length)
        if not body_data:
            return {&quot;type&quot;: &quot;exit&quot;, &quot;reason&quot;: &quot;데이터를 받는 도중 연결이 끊어졌습니다.&quot;}
            
        # 4. JSON 디코딩 후 딕셔너리로 반환
        return json.loads(body_data.decode('utf-8'))
        
    except Exception as e:
        return {&quot;type&quot;: &quot;error&quot;, &quot;message&quot;: str(e)}

def _recv_all(sock: socket.socket, length: int) -&amp;gt; bytes:
# TCP 소켓 스트림에서 지정한 크기(length)만큼 데이터를 완전히 수신할 때까지 반복한다

    data = b''
    while len(data) &amp;lt; length:
        packet = sock.recv(length - len(data))
        if not packet:
            return b''  # 소켓이 닫힌 경우
        data += packet
    return data&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/78</guid>
      <comments>https://goodjop79.tistory.com/78#entry78comment</comments>
      <pubDate>Thu, 2 Jul 2026 10:16:50 +0900</pubDate>
    </item>
    <item>
      <title>[D+59] 일반 채팅시 파일 첨부 전송기능은 어떻게 해?</title>
      <link>https://goodjop79.tistory.com/77</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 7월 1일(수)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 주 제 : LLM 채팅을 할때 파일을 첨부해서 전송하는 기능의 로직을 이해하도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;※ 팀프로젝트 시행간 채팅앱의 파일 첨부기능을 구혀하고자 하였으나, 학습이 더 필요하여 추후 확장 구현을 하기&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; 위한 학습내용이다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;016&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/016.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/016.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 파일첨부 전송기능 구현시 주의할 점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;&lt;/b&gt;파일전송 기능은 자칫 채팅 로직이랑 뒤섞여서 지저분해지기 쉬운 부분이라, 처음부터 책임을 잘 나눠두는 게 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 계층 분리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- UI 레이어 &lt;/b&gt;: 파일 선택 버튼, 드래그앤드롭, 진행률 표시만 담당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 전송 로직 레이어 &lt;/b&gt;: 파일 청크 분할, 업로드/다운로드, 재시도 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp; &lt;b&gt;&amp;nbsp;* 청크 분할&lt;/b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 700; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;&lt;b&gt;(Fil&lt;/b&gt;e Chunking) : &lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;대용량 파일이나 긴 문서를 다루기 쉽도록 &lt;/span&gt;여러 개의 작은 조각(청크, Chunk)으로 나누는 작업&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;주로 IT 개발과 인공지능(AI) 분야에서 성능 최적화를 위해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 프로토콜 레이어 &lt;/b&gt;: 기존 채팅 메시지와 파일 메타데이터(이름, 크기, 타입, 전송상태)를 구분되는 메시지 타입으로 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅 메시지 클래스에 파일 필드를 억지로 끼워넣지 말고, TextMessage와 FileMessage를 별도 타입(또는 상속/유니온 타입)으로 만드는 게 나중에 훨씬 편함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2. 상태 관리&lt;/b&gt;&lt;br /&gt;파일전송은 상태가 많다 (대기 &amp;rarr; 전송중 &amp;rarr; 완료/실패/취소). 이걸 별도의 상태 머신이나 enum으로 명확히 관리하고, UI는 이 상태를 구독만 하도록 설계하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;* &lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 700; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;Enum(열거형, Enumeration)&lt;/span&gt;&lt;/b&gt;은 &lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;서로 연관된 상수(Constant)들의 집합을 정의하는 특수한 자료형&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;입니다&lt;/span&gt;. 주로 요일, 계절, 상태(성공/실패) 등 한정된 값만을 갖는 데이터에 의미 있는 이름을 부여하여 코드의 가독성과 유지보수성을 높이는 데 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.3. 비동기 처리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 파일은 청크 단위로 나눠서 전송하고, 각 청크의 성공/실패를 독립적으로 추적&lt;/li&gt;
&lt;li&gt;콜백 지옥을 피하려면 async/await나 Promise 체인으로 정리&lt;/li&gt;
&lt;li&gt;진행률 이벤트는 별도 콜백/이벤트 이미터로 분리 (전송 로직과 UI 업데이트를 직접 묶지 않기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 첨부파일 전송을 위한 파일 구상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1. 인터페이스로 추상화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;추상화 하면 나중에 전송 방식(P2P, 서버 릴레이 등)을 바꿔도 UI나 채팅 로직은 안 건드려도 된다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;interface IFileTransport {
  send(file: FileData): Promise&amp;lt;TransferResult&amp;gt;
  onProgress(callback: (percent: number) =&amp;gt; void): void
  cancel(): void
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.2. &lt;span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;PySide6 : Qt의 시그널 / 슬롯 구조를 잘 활용하여 깔끔하게 분리하기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 폴더 구조 예시&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;chat_app/
├── core/
│   ├── file_transfer.py    # 전송 로직 (Qt 독립적)
│   ├── protocol.py         # 메시지 타입 정의
│   └── models.py           # 데이터 클래스
├── ui/
│   ├── chat_window.py
│   ├── file_bubble.py      # 파일 메시지 위젯
│   └── widgets/
└── workers/
    └── transfer_worker.py  # QThread 기반 전송 작업&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.3. 데이터 모델 (core/models.py)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;from dataclasses import dataclass
from enum import Enum, auto

class TransferStatus(Enum):
    PENDING = auto()
    IN_PROGRESS = auto()
    COMPLETED = auto()
    FAILED = auto()
    CANCELLED = auto()

@dataclass
class FileTransferInfo:
    file_id: str
    filename: str
    size: int
    file_path: str
    status: TransferStatus = TransferStatus.PENDING
    progress: int = 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.4. 전송 로직은 QThread + Signal로 분리 (workers/transfer_worker.py)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;UI 스레드를 절대 막지 않는 게 핵심. &lt;b&gt;파일 전송은 반드시 별도 스레드에서 한다&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;from PySide6.QtCore import QThread, Signal

class FileTransferWorker(QThread):
    progress_updated = Signal(str, int)      # file_id, percent
    transfer_completed = Signal(str)          # file_id
    transfer_failed = Signal(str, str)        # file_id, error_msg

    def __init__(self, file_info: FileTransferInfo, transport):
        super().__init__()
        self.file_info = file_info
        self.transport = transport  # 실제 전송 방식은 주입받음
        self._cancelled = False

    def run(self):
        try:
            for chunk_percent in self.transport.send(self.file_info.file_path):
                if self._cancelled:
                    return
                self.progress_updated.emit(self.file_info.file_id, chunk_percent)
            self.transfer_completed.emit(self.file_info.file_id)
        except Exception as e:
            self.transfer_failed.emit(self.file_info.file_id, str(e))

    def cancel(self):
        self._cancelled = True&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.5. UI는 시그널만 구독 (ui/chat_window.py)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;def start_file_send(self, file_path: str):
    file_info = FileTransferInfo(...)
    worker = FileTransferWorker(file_info, self.transport)
    worker.progress_updated.connect(self.on_progress)
    worker.transfer_completed.connect(self.on_completed)
    worker.transfer_failed.connect(self.on_failed)
    worker.start()
    self.active_transfers[file_info.file_id] = worker&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;◆ 핵심 원칙&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① transport (실제 소켓/HTTP 전송 방식)는 별도 클래스로 만들어 Worker에 주입&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; 나중에 전송 방식 바꿔도 Worker/UI는 건드리지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② UI 위젯(file_bubble.py)은 진행률 바 업데이트만 하고, 전송 로직은 절대 모름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ 파일 채팅 버블은 텍스트 버블과 별도 위젯 클래스로 만들되, 공통 MessageBubble 베이스 클래스를 상속&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;TCP 소켓이면 프로토콜 설계가 가장 중요하다. 파일 데이터와 일반 채팅 메시지가 같은 소켓을 타고 다니니까, 메시지 프레이밍&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;명확히 해야 섞이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.6. 프로토콜 설계 (core/protocol.py)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 길이 접두사(length-prefix) 방식을 쓰는 게 제일 안전하다. TCP는 스트림이라 메시지 경계가 없음&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;import struct
import json

class MessageType:
    TEXT = 0x01
    FILE_META = 0x02
    FILE_CHUNK = 0x03
    FILE_END = 0x04

def pack_message(msg_type: int, payload: bytes) -&amp;gt; bytes:
    # [1바이트 타입][4바이트 길이][payload]
    header = struct.pack('!BI', msg_type, len(payload))
    return header + payload

def pack_file_meta(file_id: str, filename: str, size: int) -&amp;gt; bytes:
    payload = json.dumps({
        &quot;file_id&quot;: file_id, &quot;filename&quot;: filename, &quot;size&quot;: size
    }).encode('utf-8')
    return pack_message(MessageType.FILE_META, payload)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.7. 수신 파서는 상태 머신으로 (core/protocol.py)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 소켓에서 받은 바이트가 메시지 경계랑 딱 맞아떨어진다는 보장이 없으므로 버퍼링은 필수&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;class MessageParser:
    def __init__(self):
        self._buffer = b''

    def feed(self, data: bytes):
        self._buffer += data

    def parse_messages(self):
        &quot;&quot;&quot;완성된 메시지들을 하나씩 yield&quot;&quot;&quot;
        while True:
            if len(self._buffer) &amp;lt; 5:
                break
            msg_type, length = struct.unpack('!BI', self._buffer[:5])
            if len(self._buffer) &amp;lt; 5 + length:
                break  # 아직 다 안 왔음
            payload = self._buffer[5:5+length]
            self._buffer = self._buffer[5+length:]
            yield msg_type, payload&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.8. Transport 클래스 (core/file_transfer.py) &amp;mdash; Qt 독립적&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;haskell&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;import socket
import os

class TcpFileTransport:
    CHUNK_SIZE = 64 * 1024  # 64KB

    def __init__(self, sock: socket.socket):
        self.sock = sock

    def send_file(self, file_path: str, file_id: str, progress_cb=None):
        size = os.path.getsize(file_path)
        filename = os.path.basename(file_path)

        self.sock.sendall(pack_file_meta(file_id, filename, size))

        sent = 0
        with open(file_path, 'rb') as f:
            while chunk := f.read(self.CHUNK_SIZE):
                self.sock.sendall(pack_message(MessageType.FILE_CHUNK, chunk))
                sent += len(chunk)
                if progress_cb:
                    progress_cb(int(sent / size * 100))

        self.sock.sendall(pack_message(MessageType.FILE_END, file_id.encode()))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.9. Worker는 이 transport를 감싸기만 함 (workers/transfer_worker.py)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;rust&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;class FileTransferWorker(QThread):
    progress_updated = Signal(str, int)
    transfer_completed = Signal(str)
    transfer_failed = Signal(str, str)

    def __init__(self, file_path: str, file_id: str, transport: TcpFileTransport):
        super().__init__()
        self.file_path = file_path
        self.file_id = file_id
        self.transport = transport

    def run(self):
        try:
            self.transport.send_file(
                self.file_path,
                self.file_id,
                progress_cb=lambda p: self.progress_updated.emit(self.file_id, p)
            )
            self.transfer_completed.emit(self.file_id)
        except Exception as e:
            self.transfer_failed.emit(self.file_id, str(e))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;◆&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 핵심 설계 포인트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;①&amp;nbsp;&lt;/b&gt;&lt;b&gt;소켓 I/O는 UI 스레드에서 절대 하지 말기 (&lt;/b&gt;이미 Worker(QThread)에서 하고 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 수신 측도 별도 스레드&lt;/b&gt;로 돌리면서 MessageParser로 파싱한 뒤, 완성된 메시지 단위로 Qt Signal 발행&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;rarr; UI는 그 시그널만 받아서 그린다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 텍스트 채팅과 파일 전송이 같은 소켓을 공유&lt;/b&gt;한다면, 큰 파일 전송 중에도 텍스트 메시지가 끼어들 수 있게 하려면 chunk 단위를 작게 유지하거나, 아예 파일 전송용 소켓을 별도 포트로 분리하는 것도 고려해볼 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ 파일 수신 시 file_id별로 임시 파일에 쓰다가 FILE_END 오면 최종 경로로 이동 (중간에 끊기면 임시 파일만 정리)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;기본 socket 모듈로 직접 구현한 서버라면, 보통 클라이언트 하나당 스레드 하나(threading) 방식을 많이 사용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;⑤ &lt;b&gt;서버는 파일을 파싱만 하고 그대로 중계&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;FILE_META &amp;rarr; FILE_CHUNK... &amp;rarr; FILE_END 흐름을 그대로 다른 클라이언트에 릴레이하면서, 동시에 서버 로컬에도 저장 (백업/이력용)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⑥ 클라이언트별 진행 중인 파일 상태(_current_file)를 인스턴스 변수로 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; 동시에 여러 클라이언트가 각자 파일을 보내도 섞이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑦ &lt;b&gt;한 클라이언트가 동시에 파일 2개를 보내는 경우&lt;/b&gt;는 이 구조로는 안 됨 (file_id별 dict로 관리하도록 확장 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑧ broadcast에 lock 걸어서 클라이언트 리스트 변경 중 레이스 컨디션 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 주의할 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;recv(65536) 크기와 클라이언트 쪽 CHUNK_SIZE(64KB)를 맞춰두면 처리가 더 매끄럽게 된다&lt;/li&gt;
&lt;li&gt;대용량 파일이 많다면 스레드당 메모리 사용은 적지만, 동시 접속자가 많아지면 threading 대신 asyncio 전환도 고려해 본다&lt;/li&gt;
&lt;li&gt;트래픽 규모가 크지 않다면 이 구조로 충분하다&lt;/li&gt;
&lt;li&gt;파일 크기 제한 : 악의적이든 실수든 너무 큰 파일이 들어오면 서버 디스크가 찰 수 있고. FILE_META 받을 때 size 체크해서 거부할수 도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;MAX_FILE_SIZE = 500 * 1024 * 1024  # 500MB

elif msg_type == MessageType.FILE_META:
    meta = json.loads(payload.decode('utf-8'))
    if meta[&quot;size&quot;] &amp;gt; MAX_FILE_SIZE:
        # 에러 메시지 보내고 연결 유지 (혹은 끊기)
        self.conn.sendall(pack_message(MessageType.ERROR, b&quot;File too large&quot;))
        return
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.10. 동시 파일 전송 대비 (같은 사람이 파일 2개 연달아 보낼 때)&lt;/b&gt;&lt;br /&gt;지금 구조는 _current_file이 하나뿐이라 겹치면 깨짐.&amp;nbsp; file_id를 키로 하는&lt;b&gt; dict로&lt;/b&gt; 바꿔두면 안전하다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;clean&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;self._current_files = {}  # file_id -&amp;gt; {&quot;handle&quot;:..., &quot;filename&quot;:...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.11. 클라이언트 접속 종료 시 정리&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송 도중 클라이언트가 끊기면 .tmp 파일이 남고, finally 블록에서 열린 파일 핸들 정리하고, 서버 시작 시 .tmp 파일 청소하는 루틴 하나 만들어두면 편리하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;def run(self):
    try:
        ...
    except ConnectionError:
        pass
    finally:
        # 진행 중이던 파일 핸들 정리
        for f in self._current_files.values():
            f[&quot;handle&quot;].close()
            os.remove(f[&quot;tmp_path&quot;])
        with self._lock:
            self.clients.remove(self)
        self.conn.close()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.12. 간단한 로깅&lt;/b&gt;&lt;br /&gt;- 누가 언제 무슨 파일을 보냈는지 정도는 로그 남겨두면 나중에 문제 생겼을 때 추적하기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;클라이언트는 &lt;b&gt;소켓 수신 전용 스레드&lt;/b&gt;를 하나 두고, 파싱된 메시지를 Signal로 UI에 던지는 구조로 가면 깔끔하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚠️ Tip : FILE_CHUNK에 file_id를 포함하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 프로토콜에서 FILE_CHUNK는 순수 파일 바이트만 담았는데, 여러 파일이 동시에 오갈 가능성이 있으면 특히 서버가 여러 클라이언트를 릴레이하는 구조라 여러 파일이 &quot;끼워넣기&quot;(인터리빙)될 수 있다. 청크에 file_id를 붙여야 안전하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;◆&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;b&gt;핵심 정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;송신용 FileTransferWorker와 수신용 ReceiverWorker는 완전히 분리된 스레드 &amp;mdash; 소켓 자체는 send/recv 양방향 가능하니 같은 socket 객체를 공유해도 되지만, 두 스레드가 동시에 sendall을 부르지 않도록 송신 쪽엔 lock을 걸어두는 게 안전&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FILE_CHUNK에 file_id 포함&lt;/b&gt;은 여러 파일이 뒤섞여 전송될 가능성이 있다면 필수임. 지금 규모(20명 미만)에선 한 번에 파일 하나씩만 주고받는다면 생략해도 되지만, 나중에 확장성 생각하면 처음부터 넣어두는 걸 추천&lt;/li&gt;
&lt;li&gt;FileBubble 위젯은 진행률 바 + 파일명 + 크기만 표시하고, 완료되면 &quot;열기&quot;/&quot;폴더에서 보기&quot; 버튼 정도만 붙이면 충분하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;b&gt;&lt;b&gt;◆&lt;span&gt; 파일&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;전체 흐름&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;UI (드래그앤드롭/버튼)
  &amp;rarr; FileTransferWorker(QThread) 생성, transport 주입
    &amp;rarr; TcpFileTransport.send_file()
      &amp;rarr; SafeSocketWriter.send() (lock으로 보호)
  &amp;rarr; Signal로 진행률/완료/실패를 FileBubble에 전달&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 나중에 전송 방식을 소켓에서 WebSocket이나 HTTP로 바꿔도 transport 클래스만 교체하고, UI는 전혀 안 건드려도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 후 기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분도 많이 어려운 부분이다. 채팅앱에서 첨부파일을 전송하고 저장하고 받기까지 절차와 흐름이 어렵다. 우리가 첨부파일 하나 올리고 전송하면 윈도우 화면에서 편리하게 사용하던 것들이 이와같은 로직으로 구동되고 있다는 것이 알면 알수록 새롭다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;파일 첨부하여 전송&quot; 기능은 자칫 &quot;채팅 로직&quot;이랑 뒤섞여서 지저분해지기 쉬운 부분이라, 처음부터 책임을 잘 나눠두는 게 핵심.&lt;br /&gt;(계층 분리, 상태관리, 비동기 처리, 인터페이스로 추상화 등) 또 다른 개발용어들이 익숙하지 않음을 증명이라도 하듯 새롭다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;오늘로서 교육 59일째가 지났다. 하면 할수록 어려워 지는 부분도 있지만 매우 흥미로운 부분들이 많이 있어서 도전은 계속된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;※ 예시 학습 참고 파일&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/c3CLgl/dJMcabEJ103/kGQeEAjM52kXXdktSWp2jK/chat_app.zip?attach=1&amp;amp;knm=tfile.zip&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;chat_app.zip&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.01MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;* 예시파일 : 압축 풀고 server/socket_server.py 먼저 띄운 다음 client_main.py의 HOST를 서버 IP로 바꿔서 두 개 이상 띄워서 실행&amp;nbsp; 하기&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;[파일 구조]&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-02 08-54-33.png&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5qeoC/dJMcabxX9l9/jyolByakOD9gWmX6tLfDFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5qeoC/dJMcabxX9l9/jyolByakOD9gWmX6tLfDFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5qeoC/dJMcabxX9l9/jyolByakOD9gWmX6tLfDFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5qeoC%2FdJMcabxX9l9%2FjyolByakOD9gWmX6tLfDFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;318&quot; height=&quot;645&quot; data-filename=&quot;스크린샷 2026-07-02 08-54-33.png&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;core / __init__.py., connection.py,&amp;nbsp; file_transfer.py,&amp;nbsp; models.py,&amp;nbsp; protocol.py&lt;/p&gt;
&lt;pre id=&quot;code_1782950007398&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#=========================
# connection.py
import socket
import threading


class SafeSocketWriter:
    &quot;&quot;&quot;여러 스레드(텍스트 전송, 파일 전송 워커 등)가 동시에
    같은 소켓에 sendall을 호출해도 메시지가 섞이지 않도록 lock으로 보호.&quot;&quot;&quot;

    def __init__(self, sock: socket.socket):
        self.sock = sock
        self._lock = threading.Lock()

    def send(self, data: bytes):
        with self._lock:
            self.sock.sendall(data)

    def close(self):
        try:
            self.sock.close()
        except OSError:
            pass


#=========================
# file_transfer.py
import os
from core.connection import SafeSocketWriter
from core.protocol import pack_file_meta, pack_file_chunk, pack_file_end


class TcpFileTransport:
    &quot;&quot;&quot;실제 파일을 청크로 나눠 소켓에 흘려보내는 역할만 담당.
    Qt(QThread, Signal)를 전혀 모른다 -&amp;gt; 나중에 다른 UI 프레임워크로 바꿔도 재사용 가능.&quot;&quot;&quot;

    CHUNK_SIZE = 64 * 1024  # 64KB, 서버 recv 버퍼 크기와 맞춤

    def __init__(self, writer: SafeSocketWriter):
        self.writer = writer

    def send_file(self, file_path: str, file_id: str, progress_cb=None, is_cancelled=None):
        &quot;&quot;&quot;
        progress_cb(percent: int) -&amp;gt; None
        is_cancelled() -&amp;gt; bool   # True 반환 시 즉시 중단
        &quot;&quot;&quot;
        size = os.path.getsize(file_path)
        filename = os.path.basename(file_path)

        self.writer.send(pack_file_meta(file_id, filename, size))

        sent = 0
        with open(file_path, &quot;rb&quot;) as f:
            while True:
                if is_cancelled and is_cancelled():
                    return
                chunk = f.read(self.CHUNK_SIZE)
                if not chunk:
                    break
                self.writer.send(pack_file_chunk(file_id, chunk))
                sent += len(chunk)
                if progress_cb:
                    percent = int(sent / size * 100) if size else 100
                    progress_cb(percent)

        self.writer.send(pack_file_end(file_id))


#=========================
# models.py
from dataclasses import dataclass
from enum import Enum, auto


class TransferStatus(Enum):
    PENDING = auto()
    IN_PROGRESS = auto()
    COMPLETED = auto()
    FAILED = auto()
    CANCELLED = auto()


@dataclass
class FileTransferInfo:
    file_id: str
    filename: str
    size: int
    file_path: str = &quot;&quot;          # 송신 시 로컬 경로
    status: TransferStatus = TransferStatus.PENDING
    progress: int = 0


#=========================
# protocol.py
&quot;&quot;&quot;
채팅 + 파일 전송 프로토콜 정의

메시지 포맷: [1바이트 타입][4바이트 길이(빅엔디안)][payload]
FILE_CHUNK / FILE_END의 payload는 [1바이트 file_id길이][file_id][data]
&quot;&quot;&quot;
import struct
import json


class MessageType:
    TEXT = 0x01
    FILE_META = 0x02
    FILE_CHUNK = 0x03
    FILE_END = 0x04
    ERROR = 0x05


HEADER_FMT = &quot;!BI&quot;          # type(1) + length(4)
HEADER_SIZE = struct.calcsize(HEADER_FMT)


# ---------------------------------------------------------------------------
# Pack helpers
# ---------------------------------------------------------------------------

def pack_message(msg_type: int, payload: bytes) -&amp;gt; bytes:
    header = struct.pack(HEADER_FMT, msg_type, len(payload))
    return header + payload


def pack_text(text: str) -&amp;gt; bytes:
    return pack_message(MessageType.TEXT, text.encode(&quot;utf-8&quot;))


def pack_file_meta(file_id: str, filename: str, size: int) -&amp;gt; bytes:
    payload = json.dumps(
        {&quot;file_id&quot;: file_id, &quot;filename&quot;: filename, &quot;size&quot;: size}
    ).encode(&quot;utf-8&quot;)
    return pack_message(MessageType.FILE_META, payload)


def _with_file_id(file_id: str, data: bytes) -&amp;gt; bytes:
    id_bytes = file_id.encode(&quot;utf-8&quot;)
    if len(id_bytes) &amp;gt; 255:
        raise ValueError(&quot;file_id must be &amp;lt;= 255 bytes&quot;)
    return struct.pack(&quot;!B&quot;, len(id_bytes)) + id_bytes + data


def _split_file_id(payload: bytes):
    id_len = payload[0]
    file_id = payload[1:1 + id_len].decode(&quot;utf-8&quot;)
    data = payload[1 + id_len:]
    return file_id, data


def pack_file_chunk(file_id: str, chunk: bytes) -&amp;gt; bytes:
    return pack_message(MessageType.FILE_CHUNK, _with_file_id(file_id, chunk))


def unpack_file_chunk(payload: bytes):
    &quot;&quot;&quot;returns (file_id, chunk_bytes)&quot;&quot;&quot;
    return _split_file_id(payload)


def pack_file_end(file_id: str) -&amp;gt; bytes:
    # FILE_END는 데이터가 없으니 chunk 자리에 빈 bytes만
    return pack_message(MessageType.FILE_END, _with_file_id(file_id, b&quot;&quot;))


def unpack_file_end(payload: bytes) -&amp;gt; str:
    file_id, _ = _split_file_id(payload)
    return file_id


def pack_error(message: str) -&amp;gt; bytes:
    return pack_message(MessageType.ERROR, message.encode(&quot;utf-8&quot;))


# ---------------------------------------------------------------------------
# Incremental parser (TCP는 스트림이라 메시지 경계가 없음 -&amp;gt; 버퍼링 필수)
# ---------------------------------------------------------------------------

class MessageParser:
    def __init__(self):
        self._buffer = bytearray()

    def feed(self, data: bytes):
        self._buffer.extend(data)

    def parse_messages(self):
        &quot;&quot;&quot;완성된 (msg_type, payload) 쌍을 하나씩 yield. 부족하면 멈추고 다음 feed를 기다림.&quot;&quot;&quot;
        while True:
            if len(self._buffer) &amp;lt; HEADER_SIZE:
                break
            msg_type, length = struct.unpack(HEADER_FMT, self._buffer[:HEADER_SIZE])
            total_needed = HEADER_SIZE + length
            if len(self._buffer) &amp;lt; total_needed:
                break  # 아직 다 안 옴
            payload = bytes(self._buffer[HEADER_SIZE:total_needed])
            del self._buffer[:total_needed]
            yield msg_type, payload&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;server / __init__.py.&amp;nbsp; scoket_server.py&lt;/p&gt;
&lt;pre id=&quot;code_1782950080221&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# server_uploads 폴더
#======================

# socket_server.py
import os
import socket
import threading
import json
import logging

from core.protocol import (
    MessageParser, MessageType,
    pack_message, pack_error,
    unpack_file_chunk, unpack_file_end,
)

UPLOAD_DIR = &quot;./server_uploads&quot;
MAX_FILE_SIZE = 500 * 1024 * 1024  # 500MB

logging.basicConfig(
    level=logging.INFO,
    format=&quot;%(asctime)s [%(levelname)s] %(message)s&quot;,
)
log = logging.getLogger(&quot;server&quot;)


class ClientHandler(threading.Thread):
    def __init__(self, conn: socket.socket, addr, server: &quot;SocketServer&quot;):
        super().__init__(daemon=True)
        self.conn = conn
        self.addr = addr
        self.server = server
        self.parser = MessageParser()
        self._send_lock = threading.Lock()
        # 동시에 여러 파일을 받을 수 있으므로 file_id를 키로 관리
        self._incoming_files: dict[str, dict] = {}

    # ------------------------------------------------------------------
    def run(self):
        log.info(f&quot;연결됨: {self.addr}&quot;)
        try:
            while True:
                data = self.conn.recv(65536)
                if not data:
                    break
                self.parser.feed(data)
                for msg_type, payload in self.parser.parse_messages():
                    self._handle_message(msg_type, payload)
        except (ConnectionError, OSError) as e:
            log.info(f&quot;연결 종료 ({self.addr}): {e}&quot;)
        finally:
            self._cleanup()

    def send_raw(self, data: bytes):
        &quot;&quot;&quot;다른 스레드(broadcast)에서도 안전하게 쓸 수 있도록 lock 보호&quot;&quot;&quot;
        try:
            with self._send_lock:
                self.conn.sendall(data)
        except (ConnectionError, OSError):
            pass

    # ------------------------------------------------------------------
    def _handle_message(self, msg_type: int, payload: bytes):
        if msg_type == MessageType.TEXT:
            self.server.broadcast(pack_message(MessageType.TEXT, payload), exclude=self)

        elif msg_type == MessageType.FILE_META:
            self._on_file_meta(payload)

        elif msg_type == MessageType.FILE_CHUNK:
            self._on_file_chunk(payload)

        elif msg_type == MessageType.FILE_END:
            self._on_file_end(payload)

    def _on_file_meta(self, payload: bytes):
        meta = json.loads(payload.decode(&quot;utf-8&quot;))
        file_id, filename, size = meta[&quot;file_id&quot;], meta[&quot;filename&quot;], meta[&quot;size&quot;]

        if size &amp;gt; MAX_FILE_SIZE:
            self.send_raw(pack_error(f&quot;파일이 너무 큽니다 (최대 {MAX_FILE_SIZE // (1024*1024)}MB)&quot;))
            return

        safe_name = os.path.basename(filename)  # 경로 조작 방지
        tmp_path = os.path.join(UPLOAD_DIR, f&quot;{file_id}.tmp&quot;)
        self._incoming_files[file_id] = {
            &quot;handle&quot;: open(tmp_path, &quot;wb&quot;),
            &quot;filename&quot;: safe_name,
            &quot;tmp_path&quot;: tmp_path,
            &quot;received&quot;: 0,
            &quot;size&quot;: size,
        }
        log.info(f&quot;파일 수신 시작: {safe_name} ({size} bytes) from {self.addr}&quot;)
        # 다른 클라이언트에게 그대로 릴레이 (원본 payload 재사용)
        self.server.broadcast(pack_message(MessageType.FILE_META, payload), exclude=self)

    def _on_file_chunk(self, payload: bytes):
        file_id, chunk = unpack_file_chunk(payload)
        info = self._incoming_files.get(file_id)
        if not info:
            return  # META 없이 온 chunk 무시
        info[&quot;handle&quot;].write(chunk)
        info[&quot;received&quot;] += len(chunk)
        # 릴레이
        self.server.broadcast(pack_message(MessageType.FILE_CHUNK, payload), exclude=self)

    def _on_file_end(self, payload: bytes):
        file_id = unpack_file_end(payload)
        info = self._incoming_files.pop(file_id, None)
        if not info:
            return
        info[&quot;handle&quot;].close()
        final_path = os.path.join(UPLOAD_DIR, info[&quot;filename&quot;])
        final_path = self._dedupe_path(final_path)
        os.rename(info[&quot;tmp_path&quot;], final_path)
        log.info(f&quot;파일 수신 완료: {final_path}&quot;)
        self.server.broadcast(pack_message(MessageType.FILE_END, payload), exclude=self)

    @staticmethod
    def _dedupe_path(path: str) -&amp;gt; str:
        if not os.path.exists(path):
            return path
        base, ext = os.path.splitext(path)
        i = 1
        while os.path.exists(f&quot;{base}({i}){ext}&quot;):
            i += 1
        return f&quot;{base}({i}){ext}&quot;

    # ------------------------------------------------------------------
    def _cleanup(self):
        # 전송 도중 끊긴 파일 임시본 정리
        for info in self._incoming_files.values():
            try:
                info[&quot;handle&quot;].close()
                os.remove(info[&quot;tmp_path&quot;])
            except OSError:
                pass
        self._incoming_files.clear()

        self.server.remove_client(self)
        try:
            self.conn.close()
        except OSError:
            pass


class SocketServer:
    def __init__(self, host=&quot;0.0.0.0&quot;, port=9000):
        self.host = host
        self.port = port
        self.clients: list[ClientHandler] = []
        self._lock = threading.Lock()

    def broadcast(self, data: bytes, exclude: ClientHandler = None):
        with self._lock:
            targets = [c for c in self.clients if c is not exclude]
        for client in targets:
            client.send_raw(data)

    def remove_client(self, client: ClientHandler):
        with self._lock:
            if client in self.clients:
                self.clients.remove(client)
        log.info(f&quot;클라이언트 제거: {client.addr} (현재 {len(self.clients)}명)&quot;)

    def _cleanup_stale_tmp_files(self):
        if not os.path.isdir(UPLOAD_DIR):
            return
        for name in os.listdir(UPLOAD_DIR):
            if name.endswith(&quot;.tmp&quot;):
                os.remove(os.path.join(UPLOAD_DIR, name))
                log.info(f&quot;이전 세션의 미완성 파일 정리: {name}&quot;)

    def start(self):
        os.makedirs(UPLOAD_DIR, exist_ok=True)
        self._cleanup_stale_tmp_files()

        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_sock.bind((self.host, self.port))
        server_sock.listen(20)
        log.info(f&quot;Listening on {self.host}:{self.port}&quot;)

        try:
            while True:
                conn, addr = server_sock.accept()
                handler = ClientHandler(conn, addr, self)
                with self._lock:
                    self.clients.append(handler)
                handler.start()
        except KeyboardInterrupt:
            log.info(&quot;서버 종료&quot;)
        finally:
            server_sock.close()


if __name__ == &quot;__main__&quot;:
    SocketServer().start()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;ui / __init__.py, chat_window.py, file_bubble.py&lt;/p&gt;
&lt;pre id=&quot;code_1782950275490&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ui / chat_window.py
import os
import socket
import uuid

from PySide6.QtWidgets import (
    QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QScrollArea,
    QLineEdit, QPushButton, QLabel, QFileDialog, QMessageBox
)

from core.connection import SafeSocketWriter
from core.file_transfer import TcpFileTransport
from core.protocol import pack_text
from workers.transfer_worker import FileTransferWorker
from workers.receiver_worker import ReceiverWorker
from ui.file_bubble import FileBubble

DOWNLOAD_DIR = os.path.join(os.path.expanduser(&quot;~&quot;), &quot;chat_downloads&quot;)


class ChatWindow(QMainWindow):
    def __init__(self, host: str, port: int, parent=None):
        super().__init__(parent)
        self.setWindowTitle(&quot;TCP Chat&quot;)
        self.resize(420, 600)

        # --- 소켓 연결 ---
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port))
        self.writer = SafeSocketWriter(self.sock)

        # file_id -&amp;gt; 위젯/워커 참조 저장소
        self.file_bubbles: dict[str, FileBubble] = {}
        self.active_send_workers: dict[str, FileTransferWorker] = {}

        self._build_ui()
        self._start_receiver()

    # ------------------------------------------------------------------
    # UI 구성
    # ------------------------------------------------------------------
    def _build_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        root = QVBoxLayout(central)

        # 메시지 목록 영역
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.message_container = QWidget()
        self.chat_layout = QVBoxLayout(self.message_container)
        self.chat_layout.addStretch(1)
        self.scroll_area.setWidget(self.message_container)
        root.addWidget(self.scroll_area, stretch=1)

        # 입력 영역
        input_row = QHBoxLayout()
        self.input_edit = QLineEdit()
        self.input_edit.setPlaceholderText(&quot;메시지를 입력하세요...&quot;)
        self.input_edit.returnPressed.connect(self.send_text_message)

        send_btn = QPushButton(&quot;전송&quot;)
        send_btn.clicked.connect(self.send_text_message)

        attach_btn = QPushButton(&quot; &quot;)
        attach_btn.setFixedWidth(36)
        attach_btn.clicked.connect(self.choose_and_send_file)

        input_row.addWidget(self.input_edit, stretch=1)
        input_row.addWidget(attach_btn)
        input_row.addWidget(send_btn)
        root.addLayout(input_row)

    def _add_bubble(self, widget: QWidget):
        # addStretch가 마지막에 있으므로 그 앞에 삽입
        self.chat_layout.insertWidget(self.chat_layout.count() - 1, widget)
        self.scroll_area.verticalScrollBar().setValue(
            self.scroll_area.verticalScrollBar().maximum()
        )

    # ------------------------------------------------------------------
    # 수신 스레드 시작 + Signal 연결
    # ------------------------------------------------------------------
    def _start_receiver(self):
        self.receiver = ReceiverWorker(self.sock, save_dir=DOWNLOAD_DIR)
        self.receiver.text_received.connect(self.on_text_received)
        self.receiver.file_meta_received.connect(self.on_file_incoming)
        self.receiver.file_progress.connect(self.on_file_progress)
        self.receiver.file_completed.connect(self.on_file_completed)
        self.receiver.file_failed.connect(self.on_file_failed)
        self.receiver.error_received.connect(self.on_server_error)
        self.receiver.connection_lost.connect(self.on_connection_lost)
        self.receiver.start()

    # ------------------------------------------------------------------
    # 텍스트 송/수신
    # ------------------------------------------------------------------
    def send_text_message(self):
        text = self.input_edit.text().strip()
        if not text:
            return
        self.writer.send(pack_text(text))
        self._add_bubble(QLabel(f&quot;나: {text}&quot;))
        self.input_edit.clear()

    def on_text_received(self, text: str):
        self._add_bubble(QLabel(f&quot;상대: {text}&quot;))

    # ------------------------------------------------------------------
    # 파일 송신
    # ------------------------------------------------------------------
    def choose_and_send_file(self):
        file_path, _ = QFileDialog.getOpenFileName(self, &quot;보낼 파일 선택&quot;)
        if not file_path:
            return
        self.send_file(file_path)

    def send_file(self, file_path: str):
        file_id = str(uuid.uuid4())
        filename = os.path.basename(file_path)
        size = os.path.getsize(file_path)

        bubble = FileBubble(filename, size, incoming=False)
        self._add_bubble(bubble)
        self.file_bubbles[file_id] = bubble

        transport = TcpFileTransport(self.writer)
        worker = FileTransferWorker(file_path, file_id, transport)
        worker.progress_updated.connect(self.on_send_progress)
        worker.transfer_completed.connect(self.on_send_completed)
        worker.transfer_failed.connect(self.on_send_failed)
        worker.start()
        self.active_send_workers[file_id] = worker

    def on_send_progress(self, file_id: str, percent: int):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_progress(percent)

    def on_send_completed(self, file_id: str):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_completed(bubble.filename)  # 로컬 원본 경로 대신 이름만 표기해도 무방
            bubble.status_label.setText(&quot;전송 완료&quot;)
        self.active_send_workers.pop(file_id, None)

    def on_send_failed(self, file_id: str, error_msg: str):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_failed(error_msg)
        self.active_send_workers.pop(file_id, None)

    # ------------------------------------------------------------------
    # 파일 수신
    # ------------------------------------------------------------------
    def on_file_incoming(self, file_id: str, filename: str, size: int):
        bubble = FileBubble(filename, size, incoming=True)
        self._add_bubble(bubble)
        self.file_bubbles[file_id] = bubble

    def on_file_progress(self, file_id: str, percent: int):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_progress(percent)

    def on_file_completed(self, file_id: str, saved_path: str):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_completed(saved_path)

    def on_file_failed(self, file_id: str, error_msg: str):
        if bubble := self.file_bubbles.get(file_id):
            bubble.set_failed(error_msg)

    # ------------------------------------------------------------------
    # 에러 / 연결 종료
    # ------------------------------------------------------------------
    def on_server_error(self, message: str):
        QMessageBox.warning(self, &quot;서버 오류&quot;, message)

    def on_connection_lost(self, reason: str):
        QMessageBox.critical(self, &quot;연결 끊김&quot;, f&quot;서버와의 연결이 끊어졌습니다: {reason}&quot;)

    def closeEvent(self, event):
        self.receiver.stop()
        self.writer.close()
        super().closeEvent(event)


#=====================
# file_bubble.py
import os
import subprocess
import sys

from PySide6.QtWidgets import (
    QFrame, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar, QPushButton
)


def human_readable_size(size_bytes: int) -&amp;gt; str:
    size = float(size_bytes)
    for unit in [&quot;B&quot;, &quot;KB&quot;, &quot;MB&quot;, &quot;GB&quot;]:
        if size &amp;lt; 1024:
            return f&quot;{size:.1f} {unit}&quot;
        size /= 1024
    return f&quot;{size:.1f} TB&quot;


class FileBubble(QFrame):
    def __init__(self, filename: str, size: int, incoming: bool = True, parent=None):
        super().__init__(parent)
        self.filename = filename
        self.size = size
        self.incoming = incoming
        self.saved_path = None

        self.setObjectName(&quot;fileBubble&quot;)
        self.setStyleSheet(&quot;&quot;&quot;
            #fileBubble {
                background-color: #2b2b2b;
                border-radius: 10px;
                padding: 8px;
            }
            QLabel { color: #e0e0e0; }
            QProgressBar {
                border: none;
                background-color: #444;
                border-radius: 4px;
                height: 6px;
            }
            QProgressBar::chunk {
                background-color: #4a9eff;
                border-radius: 4px;
            }
        &quot;&quot;&quot;)
        self._build_ui()

    def _build_ui(self):
        layout = QVBoxLayout(self)
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(4)

        top_row = QHBoxLayout()
        top_row.addWidget(QLabel(&quot; &quot;))
        self.name_label = QLabel(self.filename)
        self.name_label.setStyleSheet(&quot;font-weight: bold;&quot;)
        self.size_label = QLabel(human_readable_size(self.size))
        self.size_label.setStyleSheet(&quot;color: #999; font-size: 11px;&quot;)
        top_row.addWidget(self.name_label, stretch=1)
        top_row.addWidget(self.size_label)
        layout.addLayout(top_row)

        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)
        layout.addWidget(self.progress_bar)

        bottom_row = QHBoxLayout()
        self.status_label = QLabel(&quot;수신 대기 중...&quot; if self.incoming else &quot;전송 대기 중...&quot;)
        self.status_label.setStyleSheet(&quot;color: #999; font-size: 11px;&quot;)

        self.action_btn = QPushButton(&quot;열기&quot;)
        self.action_btn.setVisible(False)
        self.action_btn.clicked.connect(self._open_file)

        bottom_row.addWidget(self.status_label, stretch=1)
        bottom_row.addWidget(self.action_btn)
        layout.addLayout(bottom_row)

    def set_progress(self, percent: int):
        self.progress_bar.setValue(percent)
        direction = &quot;수신&quot; if self.incoming else &quot;전송&quot;
        self.status_label.setText(f&quot;{direction} 중... {percent}%&quot;)

    def set_completed(self, saved_path: str):
        self.saved_path = saved_path
        self.progress_bar.setValue(100)
        self.status_label.setText(&quot;완료&quot;)
        self.action_btn.setVisible(True)

    def set_failed(self, error_msg: str = &quot;&quot;):
        self.status_label.setText(f&quot;실패: {error_msg}&quot; if error_msg else &quot;전송 실패&quot;)
        self.status_label.setStyleSheet(&quot;color: #ff6b6b; font-size: 11px;&quot;)

    def _open_file(self):
        if not self.saved_path or not os.path.exists(self.saved_path):
            return
        if sys.platform == &quot;win32&quot;:
            os.startfile(self.saved_path)
        elif sys.platform == &quot;darwin&quot;:
            subprocess.run([&quot;open&quot;, self.saved_path])
        else:
            subprocess.run([&quot;xdg-open&quot;, self.saved_path])&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;workers / __init__.py, receiver_worker.py, transfer_workder.py&lt;/p&gt;
&lt;pre id=&quot;code_1782950343994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# receiver_workder.py
import os
import json
import socket

from PySide6.QtCore import QThread, Signal

from core.protocol import MessageParser, MessageType, unpack_file_chunk, unpack_file_end


class ReceiverWorker(QThread):
    text_received = Signal(str)                    # text
    file_meta_received = Signal(str, str, int)      # file_id, filename, size
    file_progress = Signal(str, int)                # file_id, percent
    file_completed = Signal(str, str)               # file_id, saved_path
    file_failed = Signal(str, str)                  # file_id, error_msg
    error_received = Signal(str)                    # server ERROR 메시지
    connection_lost = Signal(str)

    def __init__(self, sock: socket.socket, save_dir: str, parent=None):
        super().__init__(parent)
        self.sock = sock
        self.save_dir = save_dir
        self.parser = MessageParser()
        self._running = True
        self._incoming_files = {}  # file_id -&amp;gt; dict(handle, filename, tmp_path, received, size)
        os.makedirs(self.save_dir, exist_ok=True)

    def run(self):
        try:
            while self._running:
                data = self.sock.recv(65536)
                if not data:
                    break
                self.parser.feed(data)
                for msg_type, payload in self.parser.parse_messages():
                    self._handle_message(msg_type, payload)
        except (ConnectionError, OSError) as e:
            if self._running:
                self.connection_lost.emit(str(e))
        finally:
            self._cleanup_incomplete()

    def _handle_message(self, msg_type: int, payload: bytes):
        if msg_type == MessageType.TEXT:
            self.text_received.emit(payload.decode(&quot;utf-8&quot;))

        elif msg_type == MessageType.FILE_META:
            meta = json.loads(payload.decode(&quot;utf-8&quot;))
            file_id, filename, size = meta[&quot;file_id&quot;], meta[&quot;filename&quot;], meta[&quot;size&quot;]
            tmp_path = os.path.join(self.save_dir, f&quot;{file_id}.tmp&quot;)
            self._incoming_files[file_id] = {
                &quot;handle&quot;: open(tmp_path, &quot;wb&quot;),
                &quot;filename&quot;: filename,
                &quot;tmp_path&quot;: tmp_path,
                &quot;received&quot;: 0,
                &quot;size&quot;: size,
            }
            self.file_meta_received.emit(file_id, filename, size)

        elif msg_type == MessageType.FILE_CHUNK:
            file_id, chunk = unpack_file_chunk(payload)
            info = self._incoming_files.get(file_id)
            if not info:
                return  # META 없이 온 chunk는 무시 (순서 깨짐 방지)
            info[&quot;handle&quot;].write(chunk)
            info[&quot;received&quot;] += len(chunk)
            percent = int(info[&quot;received&quot;] / info[&quot;size&quot;] * 100) if info[&quot;size&quot;] else 100
            self.file_progress.emit(file_id, percent)

        elif msg_type == MessageType.FILE_END:
            file_id = unpack_file_end(payload)
            info = self._incoming_files.pop(file_id, None)
            if not info:
                return
            info[&quot;handle&quot;].close()
            final_path = os.path.join(self.save_dir, info[&quot;filename&quot;])
            final_path = self._dedupe_path(final_path)
            os.rename(info[&quot;tmp_path&quot;], final_path)
            self.file_completed.emit(file_id, final_path)

        elif msg_type == MessageType.ERROR:
            self.error_received.emit(payload.decode(&quot;utf-8&quot;))

    @staticmethod
    def _dedupe_path(path: str) -&amp;gt; str:
        &quot;&quot;&quot;같은 이름 파일이 이미 있으면 (1), (2) 붙여서 덮어쓰기 방지&quot;&quot;&quot;
        if not os.path.exists(path):
            return path
        base, ext = os.path.splitext(path)
        i = 1
        while os.path.exists(f&quot;{base}({i}){ext}&quot;):
            i += 1
        return f&quot;{base}({i}){ext}&quot;

    def _cleanup_incomplete(self):
        &quot;&quot;&quot;연결이 끊겼을 때 미완성 파일 핸들/임시파일 정리&quot;&quot;&quot;
        for file_id, info in list(self._incoming_files.items()):
            try:
                info[&quot;handle&quot;].close()
                os.remove(info[&quot;tmp_path&quot;])
            except OSError:
                pass
            self.file_failed.emit(file_id, &quot;connection closed&quot;)
        self._incoming_files.clear()

    def stop(self):
        self._running = False
        try:
            self.sock.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        self.sock.close()


#===================
#transfer_workdr.py
from PySide6.QtCore import QThread, Signal

from core.file_transfer import TcpFileTransport


class FileTransferWorker(QThread):
    progress_updated = Signal(str, int)      # file_id, percent
    transfer_completed = Signal(str)         # file_id
    transfer_failed = Signal(str, str)       # file_id, error_msg

    def __init__(self, file_path: str, file_id: str, transport: TcpFileTransport, parent=None):
        super().__init__(parent)
        self.file_path = file_path
        self.file_id = file_id
        self.transport = transport
        self._cancelled = False

    def run(self):
        try:
            self.transport.send_file(
                self.file_path,
                self.file_id,
                progress_cb=lambda p: self.progress_updated.emit(self.file_id, p),
                is_cancelled=lambda: self._cancelled,
            )
            if not self._cancelled:
                self.transfer_completed.emit(self.file_id)
        except Exception as e:
            self.transfer_failed.emit(self.file_id, str(e))

    def cancel(self):
        self._cancelled = True&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;{core,wrokdrs,ui,server} / __init__.py, client_main.py,&amp;nbsp; requirements.txt&lt;/p&gt;
&lt;pre id=&quot;code_1782950544235&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# {core, workers, ui, server}

#==========
# client_main.py
import sys
from PySide6.QtWidgets import QApplication

from ui.chat_window import ChatWindow

HOST = &quot;127.0.0.1&quot;   # 임의의 주소를 실제 서버 IP로 교체해서 활용
PORT = 5000


def main():
    app = QApplication(sys.argv)
    try:
        window = ChatWindow(HOST, PORT)
    except ConnectionError as e:
        print(f&quot;서버 연결 실패: {e}&quot;)
        sys.exit(1)
    window.show()
    sys.exit(app.exec())


if __name__ == &quot;__main__&quot;:
    main()

#===========================
# requirements.txt
PySide6&amp;gt;=6.5

# TCP Chat + File Transfer (PySide6)

## 구조
&quot;&quot;&quot;
chat_app/
├── core/
│   ├── protocol.py       # 메시지 포맷 정의 + 파싱 (Qt 독립적)
│   ├── models.py         # FileTransferInfo, TransferStatus
│   ├── connection.py     # SafeSocketWriter (스레드 안전 send)
│   └── file_transfer.py  # TcpFileTransport (파일 청크 전송 로직)
├── workers/
│   ├── transfer_worker.py   # 파일 송신용 QThread
│   └── receiver_worker.py   # 소켓 수신 전용 QThread
├── ui/
│   ├── chat_window.py    # 메인 창, 모든 컴포넌트 연결
│   └── file_bubble.py    # 파일 전송/수신 진행률 위젯
├── server/
│   └── socket_server.py  # threading 기반 TCP 서버 (클라이언트당 스레드)
├── client_main.py        # 클라이언트 실행 진입점
└── requirements.txt
```

## 실행

서버 (리눅스):
```
cd chat_app
python3 server/socket_server.py
```
기본적으로 127.0.0.0:5000 에서 대기함 `server_uploads/` 폴더에 파일을 백업 저장

클라이언트 (PySide6 필요):
```
pip install -r requirements.txt
cd chat_app
python3 client_main.py
```
`client_main.py` 상단의 `HOST`를 서버의 실제 IP로 바꿔준다. 받은 파일은 `~/chat_downloads/` 에 저장

## 프로토콜
`[1B 타입][4B 길이][payload]` 형식의 길이-접두사 프레이밍을 사용
FILE_CHUNK / FILE_END의 payload는 `[1B file_id 길이][file_id][data]` 로 감싸,
서버가 여러 클라이언트의 여러 파일을 동시에 릴레이해도 섞이지 않도록 함

## 검증
`server/socket_server.py`를 백그라운드로 띄운 뒤 순수 소켓으로 접속해
텍스트 릴레이, 200KB 파일의 청크 전송/재조립, 서버 측 파일 저장까지
정상 동작하는 것을 확인 (GUI 없이 프로토콜/서버 로직만 테스트).

## 확장 아이디어
- 여러 파일 동시 전송: 클라이언트도 `file_id`별로 여러 `FileTransferWorker`를 동시에 띄울 수 있음 (이미 dict로 관리 중)
- 인증: 접속 시 TEXT 대신 별도 AUTH 메시지 타입 추가해 서버에서 검증
- 재전송/재개: FILE_META에 offset 필드를 추가해 중단된 지점부터 이어받기 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/77</guid>
      <comments>https://goodjop79.tistory.com/77#entry77comment</comments>
      <pubDate>Thu, 2 Jul 2026 09:04:01 +0900</pubDate>
    </item>
    <item>
      <title>D+55 [Project] 팀프로젝트 채팅앱 프로그램(개발 계획)</title>
      <link>https://goodjop79.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 25일(목)~7. 2.(목)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 주 제 : 팀 프로젝트(2), 사내 개발자 채팅앱 프로그램 개발&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;목&amp;nbsp;표&amp;nbsp;:&amp;nbsp;Pyside6&amp;nbsp;&amp;amp;&amp;nbsp;GUI를&amp;nbsp;활용한&amp;nbsp;두번째&amp;nbsp;팀프로젝트&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 팀채널 : 사내 개발팀 소통, 배포, &lt;/b&gt;&lt;b&gt;오픈, 클로즈, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 커뮤니티 : 프로젝트별 방, 실시간 커뮤니티, 소스코드 전송(파일 보내기)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 개발 계획 산출물&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 산출물 목차&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 95.5814%; height: 317px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span&gt;&lt;b&gt;[개발 계획 단계]&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;1. 프로그램 개발 계획서&lt;br /&gt;2. 유스케이스&lt;br /&gt;3. 요구사항 분석서&lt;br /&gt;4. 순서도&lt;br /&gt;5. 화면구성도&lt;/span&gt;&lt;br /&gt;&lt;span&gt;6. 목업&lt;br /&gt;7. 일정표&lt;/span&gt;&lt;br /&gt;&lt;span&gt;8. API 명세서&lt;br /&gt;9. 테이블 명세서&lt;br /&gt;10. ERD(엔티티 관계도)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;b&gt;[개발&amp;nbsp;완료&amp;nbsp;단계]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;프로젝트&amp;nbsp;완료&amp;nbsp;보고서&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;1.&amp;nbsp;프로젝트&amp;nbsp;소개&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;2. 프로그램 구현 영상 및 스크린샷&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;3.&amp;nbsp;요구사항&amp;nbsp;명세서&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;4.&amp;nbsp;유스케이스&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;5.&amp;nbsp;순&amp;nbsp;서&amp;nbsp;도&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;6.&amp;nbsp;메뉴&amp;nbsp;구조도&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;7.&amp;nbsp;개발&amp;nbsp;일정표&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;8.&amp;nbsp;프로그램&amp;nbsp;구현&amp;nbsp;파일&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8.1.&amp;nbsp;패키지&amp;nbsp;파일&amp;nbsp;목록&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8.2.&amp;nbsp;소스&amp;nbsp;코드&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;9.&amp;nbsp;프로그램&amp;nbsp;개발&amp;nbsp;후기&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&amp;nbsp;&amp;nbsp;10.&amp;nbsp;프로그램&amp;nbsp;개발&amp;nbsp;일지&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2. 개발 문서 핵심 용어 정의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAA&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;개발 계획서&lt;/b&gt;: 프로젝트의 목표, 범위, 예천 예산, 인력 구성, 추진 방향을 총망라하여 기록한 전체 로드맵 문서&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAD&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;유스케이스&lt;/b&gt;: 사용자의 관점에서 시스템이 제공해야 하는 기능과 상호작용 과정을 시나리오 형태로 표준화하여 표현한 것&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAG&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;요구사항 명세서&lt;/b&gt;: 구축할 시스템이 반드시 갖추어야 할 기능적&amp;middot;비기능적 조건과 제약사항을 명확하고 구체적으로 기록한 문서&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAJ&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;테이블 명세서&lt;/b&gt;: 데이터베이스에 저장될 테이블의 구조, 데이터 타입, 주키(PK) 여부, 컬럼 설명 등을 상세히 기록한 설계도&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAM&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;순서도&lt;/b&gt;: 프로그램이나 프로세스의 처리 절차와 논리적 흐름을 표준화된 기호(도형)와 화살표로 시각화한 다이어그램&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBBAP&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;개발 일정표&lt;/b&gt;: 프로젝트의 전체 기간 동안 수행할 작업들을 일정에 따라 배치하고, 마일스톤과 담당자를 지정해 둔 일정 관리표(예: 간트차트, 색상그래프 형식 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-sfc-root=&quot;c&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;1.3. 폭포수 문서의 유기적 관계 (동작 방식)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAA&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;강한 종속성(Dependency)&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;요구사항 명세서&lt;/b&gt;가 승인되지 않으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유스케이스&lt;/b&gt;를 작성할 수 없고, 설계가 끝나지 않으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개발 일정표&lt;/b&gt;와 코딩을 시작할 수 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAD&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;계약서의 역할&lt;/b&gt;: 각 문서는 고객과의 합의점(Baseline)이 됩니다. 문서에 없는 기능은 개발하지 않으며, 변경하려면 복잡한 '변경 관리 절차'를 거쳐야 함&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAG&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;이정표(Milestone) 기반 관리&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개발 일정표&lt;/b&gt;는 프로젝트 초기에 정한 종료일을 맞추기 위해 엄격하게 통제되며, 각 문서의 완료 시점이 중요한 마일스톤이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-sfc-root=&quot;c&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;1.4. 권장 작성 순서 : &lt;/b&gt;프로젝트는 큰 그림에서 시작하여 점차 구체적인 설계로 진행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBhAA&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;개발 계획서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(전체 방향 설정)&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBhAB&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;요구사항 명세서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(필요한 기능 정의)&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBhAC&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;유스케이스&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(기능의 구체적 시나리오화)&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBhAD&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;순서도&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;amp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;테이블 명세서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(동시 진행: 로직 및 데이터 설계)&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAIBhAE&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;개발 일정표&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(구체적 업무 기반 일정 산정)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-sfc-root=&quot;c&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;1.5. 문서 간의 유기적 관계&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAA&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;개발 계획서 &amp;rarr; 요구사항 명세서&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAB&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;계획서에 담긴 프로젝트의 '범위(Scope)'를 바탕으로, 구체적으로 무엇(What)을 개발할지 기능별로 쪼개어 요구사항 명세서를 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAE&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;요구사항 명세서 &amp;rarr; 유스케이스&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAF&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;요구사항 명세서의 기능들을 사용자가 실제로 어떻게 사용하는지(How) 사용자 관점의 시나리오로 구체화한 것이 유스케이스입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAI&quot; data-processed=&quot;true&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;유스케이스 &amp;rarr; 순서도 &amp;amp; 테이블 명세서&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAJ&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;유스케이스의 상호작용 흐름을 시스템의 논리적 절차로 바꾸면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;순서도&lt;/b&gt;가 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAK&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;유스케이스 시나리오에서 오고 가는 데이터(회원 정보, 게시글 등)를 추출하여 저장할 공간을 만들면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;테이블 명세서&lt;/b&gt;가 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAN&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;b&gt;구체적 설계(순서도/테이블) &amp;rarr; 개발 일정표&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAAICRAO&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;구현해야 할 로직(순서도)과 데이터 구조(테이블)의 복잡도를 파악해야만, 개발자별로 정확한 작업 소요 시간(M/M)을 계산하여 현실적인 개발 일정표를 짤 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 프로젝트 계획서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;2.1. 프로젝트 개발 계획서 : 별도 첨부&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.2. Protocol 함수 작성&lt;/b&gt;&lt;br /&gt;- 파일 선택 : [Client / Protocol]사용자가 전송할 파일 선택,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Base64 전송 : [Client / Protocol] 선택한 파일을 Base64로&amp;nbsp;변환하여&amp;nbsp;서버&amp;nbsp;전송&lt;br /&gt;- 파일 수신 : [Client / Protocol] 채팅방&amp;nbsp;참여자가&amp;nbsp;전송된&amp;nbsp;파일&amp;nbsp;정보를&amp;nbsp;수신&lt;br /&gt;- 파일 정보 표시 : [Client / Protocol] 파일명,&amp;nbsp;파일&amp;nbsp;크기,&amp;nbsp;전송&amp;nbsp;상태&amp;nbsp;표시&lt;br /&gt;- 파일 오류 처리 : [Client / Protocol] 파일&amp;nbsp;미선택,&amp;nbsp;빈&amp;nbsp;파일,&amp;nbsp;전송&amp;nbsp;실패&amp;nbsp;안내&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.3. Server 와&amp;nbsp; Client&amp;nbsp; 연동할 때 구성 정리 및 &quot;예시코드&quot; 참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 서버에서 클라이언트 응답 메세지는 password, id 같은 요청 필드가 없는 구조이므로 응답 전용메세지는&amp;nbsp; validate() 대신 빌더 함수 만으로 생성함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;[protocol.py]&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%; text-align: center;&quot;&gt;클래스(함수)&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%; text-align: center;&quot;&gt;내&amp;nbsp; &amp;nbsp; &amp;nbsp;용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;MsgType&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;login, message&amp;nbsp; 타입 상수 + all 집합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;field&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;id, roon_id, 필드키 상수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;validate( )&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;protocol Error 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;build_*( )&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;클라이언트--&amp;gt; 서버&amp;nbsp; &amp;nbsp;build_login, build_mesage, build_file 8개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;build_*_response()&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;서버--&amp;gt; 클라이언트&amp;nbsp; build_login_response, build_system, build_error 7개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;encode() / decode_lie()&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;직력, 역직렬화 유틸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;strcamParser&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;버퍼&amp;nbsp; TCP 스트림 파서 (client, server) 대처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;decode_file_data()&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;base64 파일 데이터 디스크 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.3954%;&quot;&gt;__main__&lt;/td&gt;
&lt;td style=&quot;width: 78.6046%;&quot;&gt;빌더, 검증, 파서 동작&amp;nbsp; test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Protocol.py&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;[client.py&lt;/b&gt;&lt;/u&gt;&lt;u&gt;&lt;b&gt;]&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782655026942&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from protocol import build_login, encode, Stream Parser, MsgType

# 빌더로 페이로더 생성
Pyload = build_login(uid, password))
self._sook.sendall(encode (payload))

#수신루프를 streamparser로 교체
parser = streamparser()
for mag in parser.fead(chunk):
    self._handle_received(msg)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;[server.py&lt;/b&gt;&lt;/u&gt;&lt;u&gt;&lt;b&gt;]&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782655243140&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from Protocol improt validate, build_login_response, build_error, ProtocolError.enc
try:
    validate(data)
except ProtocolError ase:
    send_json(conn,build_error(Str(e)))
        Continue
# 응답 빌더 사용
Send_Json(conn, build_login_response (Success = True, uid = uid))&lt;/code&gt;&lt;/pre&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[학습 프로그램 예시]&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. server.py&lt;/p&gt;
&lt;pre id=&quot;code_1782653995746&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# server.py

import socket
import threading

from protocol import receive_messages, send_message

HOST = &quot;127.0.0.1&quot;
PORT = 5000

clients = []

users ={
    &quot;alice&quot;: &quot;1234&quot;,
    &quot;bob&quot;: &quot;abcd&quot;
}

def print_clients():
    print(f&quot;현재 접속자 수: {len(clients)}&quot;)

def get_user_list():  # 
    return [client[&quot;address&quot;] for client in clients]

def broadcast_user_list():  # 
    user_list_message = {
        &quot;type&quot;: &quot;user_list&quot;,
        &quot;user_list&quot;: get_user_list()
    }

    for client in clients[:]:
        try:
            send_message(client[&quot;socket&quot;], user_list_message)
        except Exception:
            if client in clients:
                clients.remove(client)

def broadcast(message, sender_socket=None):
    for client in clients[:]:
        client_socket = client[&quot;socket&quot;]  #  딕셔너리 구조에 맞게 접근

        if client_socket != sender_socket:
            try:
                send_message(client_socket, message)
            except Exception:
                if client in clients:
                    clients.remove(client)

def send_error(client_socket, content):
    send_message(client_socket, {
        &quot;type&quot;: &quot;error&quot;,
        &quot;content&quot;: content
    })

def remove_client(client_socket):  #  딕셔너리에서 찾아 제거
    for client in clients[:]:
        if client[&quot;socket&quot;] == client_socket:
            clients.remove(client)
            return client[&quot;address&quot;]
    return None

def find_client_by_identifier(identifier):
    for client in clients:
        if client[&quot;address&quot;] == identifier:
            return client
        if client.get(&quot;nickname&quot;) == identifier:
            return client
    return None

def send_whisper(sender_identifier, target_identifier, content):
    target_client = find_client_by_identifier(target_identifier)
    if target_client is None:
        return False
    try:
        send_message(target_client[&quot;socket&quot;], {
            &quot;type&quot;: &quot;whisper&quot;,
            &quot;sender&quot;: sender_identifier,
            &quot;content&quot;: content
        })
        return True
    except Exception:
        return False

def handle_client(client_socket, client_address):
    print(f&quot;클라이언트 처리 시작: {client_address}&quot;)

    buffer = &quot;&quot;
    address_text = str(client_address)

    broadcast({
        &quot;type&quot;: &quot;system&quot;,
        &quot;content&quot;: f&quot;{address_text} 님이 입장했습니다.&quot;
    }, client_socket)
    broadcast_user_list()  #  입장 후 목록 동기화

    with client_socket:
        while True:
            try:
                buffer, messages, connected = receive_messages(client_socket, buffer)

                if not connected:
                    print(f&quot;클라이언트 연결 끊김: {client_address}&quot;)
                    break

                exit_requested = False

                for message in messages:
                    print(f&quot;{client_address} 메시지: {message}&quot;)
                    message_type = message.get(&quot;type&quot;)

                    # 회원가입 처리
                    if message_type == &quot;register&quot;:
                        username = message.get(&quot;username&quot;,)
                        password = message.get(&quot;password&quot;,)

                        if username in users:
                            send_message(client_socket, {
                                &quot;type&quot;: &quot;auth_result&quot;,
                                &quot;success&quot;: False,
                                &quot;message&quot; : &quot;이미 사용중인 아이디 입니다.&quot;
                            })
                        else:
                            users[username] = password
                            send_message(client_socket, {
                                &quot;type&quot;: &quot;auth_result&quot;,
                                &quot;success&quot;: True,
                                &quot;message&quot; : &quot;회원가입 완료, 로그인 하세요&quot;
                            })
                        continue

                    # 로그인 처리
                    if message_type == &quot;login&quot;:
                        username = message.get(&quot;username&quot;,)
                        password = message.get(&quot;password&quot;,)

                        if users.get(username) == password:
                            send_message(client_socket, {
                                &quot;type&quot;: &quot;auth_result&quot;,
                                &quot;success&quot;: True,
                                &quot;message&quot; : f&quot;{username}님 환영합니다.&quot;
                            })
                        else:
                            send_message(client_socket, {
                                &quot;type&quot;: &quot;auth_result&quot;,
                                &quot;success&quot;: False,
                                &quot;message&quot; : &quot;아이디 또는 비밀번호가 틀렸습니다.&quot;
                            })
                        continue

                    if message_type == &quot;exit&quot;:
                        print(f&quot;클라이언트 종료 요청: {client_address}&quot;)
                        exit_requested = True
                        break

                    if message_type == &quot;chat&quot;:
                        content = message.get(&quot;content&quot;, &quot;&quot;).strip()
                        if not content:
                            send_error(client_socket, &quot;빈 메시지는 보낼 수 없습니다.&quot;)
                            continue
                        broadcast({
                            &quot;type&quot;: &quot;chat&quot;,
                            &quot;sender&quot;: address_text,
                            &quot;content&quot;: content
                        }, client_socket)
                        continue

                    if message_type == &quot;whisper&quot;:
                        target = message.get(&quot;target&quot;, &quot;&quot;)
                        content = message.get(&quot;content&quot;, &quot;&quot;)
                        if not target:
                            send_error(client_socket, &quot;귓속말 대상을 선택하세요&quot;)
                            continue
                        if not content:
                            send_error(client_socket, &quot;귓속말 내용을 입력하세요&quot;)
                            continue
                        success = send_whisper(address_text, target, content)
                        if target == address_text:
                            send_error(client_socket, &quot;자기 자신에게는 귓속말을 보낼 수 없습니다.&quot;)
                        if not success:
                            send_error(client_socket, &quot;귓속말 대상을 찾을 수 없습니다.&quot;)
                            broadcast_user_list()
                            continue
                        continue

                    send_error(client_socket, f&quot;지원하지 않는 메시지 타입입니다: {message_type}&quot;)

                if exit_requested:
                    break

            except Exception as error:
                print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
                break

    removed_address = remove_client(client_socket)

    broadcast({
        &quot;type&quot;: &quot;system&quot;,
        &quot;content&quot;: f&quot;{address_text} 님이 퇴장했습니다.&quot;
    }, client_socket)
    broadcast_user_list()  #  퇴장 후 목록 동기화

    print(f&quot;접속자 목록에서 제거: {removed_address}&quot;)
    print_clients()
    print(f&quot;클라이언트 처리 종료: {client_address}&quot;)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print(&quot;사용자 목록 동기화 서버를 시작합니다.&quot;)
    print(&quot;클라이언트 접속 대기 중...&quot;)

    while True:
        client_socket, client_address = server_socket.accept()
        address_text = str(client_address)

        print(f&quot;클라이언트 접속: {client_address}&quot;)

        clients.append({  # 소켓+주소 딕셔너리로 추가
            &quot;socket&quot;: client_socket,
            &quot;address&quot;: address_text
        })

        print(f&quot;접속자 목록에 추가: {client_address}&quot;)
        print_clients()

        client_thread = threading.Thread(
            target=handle_client,
            args=(client_socket, client_address)
        )
        client_thread.start()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. protocol.py&lt;/p&gt;
&lt;pre id=&quot;code_1782653915096&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1팀 사내 채팅앱 프로젝트  protocol.py

import json
import base64
import os
import time

# 파일 전송 시 사용할 청크(조각)의 크기 설정 (64KB)
CHUNK_SIZE = 65536

def send_message(sock, message):
    # 소켓을 통해 JSON 메시지를 전송하는 함수
    # 메시지 길이를 먼저 4바이트(Big-endian)로 보내고, 그 뒤에 실제 데이터를 전송한다.

    # 1. 딕셔너리 형태의 메시지를 JSON 문자열로 변환 후 UTF-8 바이트로 인코딩
    data = json.dumps(message).encode(&quot;utf-8&quot;)
    # 2. 인코딩된 데이터의 전체 길이 계산
    length = len(data)
    # 3. 데이터 길이(4바이트 정수)와 실제 데이터를 결합하여 소켓으로 일괄 전송
    sock.sendall(length.to_bytes(4, &quot;big&quot;) + data)

def recv_message(sock):
    # 소켓으로부터 전송된 JSON 메시지를 수신하는 함수
    # 앞선 4바이트에서 메시지 길이를 파악한 후, 그 길이만큼 데이터를 정확히 읽어온다

    # 1. 메시지의 길이를 나타내는 헤더(4바이트) 수신
    raw_length = sock.recv(4)\
    # 수신된 데이터가 없으면 소켓 연결이 종료된 것으로 간주하고 None 반환
    if not raw_length:
        return None
    # 2. 바이트 배열을 정수형(정해진 데이터 길이)으로 변환
    length = int.from_bytes(raw_length, &quot;big&quot;)
    data = b&quot;&quot;
    # 3. 지정된 길이만큼 데이터를 루프를 돌며 완전하게 수신
    while len(data) &amp;lt; length:
        # 아직 받지 못한 남은 크기만큼 데이터 요청
        packet = sock.recv(length - len(data))
        # 데이터 수신 중 연결이 끊긴 경우 None 반환
        if not packet:
            return None
        # 수신한 패킷을 기존 데이터에 누적
        data += packet
    # 4. 수신 완료된 바이트 데이터를 문자열로 디코딩 후 다시 JSON(딕셔너리)으로 파싱하여 반환
    return json.loads(data.decode(&quot;utf-8&quot;))

def send_file(sock, file_path, sender):
    # 지정한 경로의 파일을 청크 단위로 나누어 소켓으로 전송하는 함수
    # 파일 경로에서 순수 파일 이름만 추출
    filename = os.path.basename(file_path)
    # 전체 파일 크기(바이트) 구하기
    size = os.path.getsize(file_path)
    # 현재 시간을 밀리초 단위까지 활용하여 고유한 파일 식별자(ID) 생성
    file_id = f&quot;{filename}_{int(time.time() * 1000)}&quot;
    # 1단계: 수신 측에 파일 정보(메타데이터)를 먼저 전송
    send_message(sock, {
        &quot;type&quot;: &quot;file_info&quot;,
        &quot;file_id&quot;: file_id,
        &quot;filename&quot;: filename,
        &quot;size&quot;: size,
        &quot;sender&quot;: sender
    })

    try:
        # 2단계: 파일을 바이너리 읽기(rb) 모드로 열어 청크 단위로 전송 진행
        with open(file_path, &quot;rb&quot;) as file:
            while True:
                # 지정한 크기(64KB)만큼 파일 읽기
                chunk = file.read(CHUNK_SIZE)
                # 더 이상 읽을 데이터가 없으면 루프 탈출
                if not chunk:
                    break
                # 이진 데이터를 텍스트 기반의 JSON에 담기 위해 base64로 인코딩 후 문자열 변환
                encoded = base64.b64encode(chunk).decode(&quot;utf-8&quot;)
                # 파일 조각 데이터 전송
                send_message(sock, {
                    &quot;type&quot;: &quot;file_chunk&quot;,
                    &quot;file_id&quot;: file_id,
                    &quot;data&quot;: encoded
                })
    except Exception as error:
        # 파일 읽기 및 전송 중 예외 발생 시 에러 메시지 전송 후 실패(False) 반환
        send_message(sock, {
            &quot;type&quot;: &quot;error&quot;,
            &quot;content&quot;: f&quot;파일 전송 실패: {error}&quot;
        })
        return False

    # 3단계: 파일의 모든 조각이 성공적으로 전송되었음을 알리는 종료 메시지 전송
    send_message(sock, {
        &quot;type&quot;: &quot;file_end&quot;,
        &quot;file_id&quot;: file_id
    })
    return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/76</guid>
      <comments>https://goodjop79.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 28 Jun 2026 22:41:51 +0900</pubDate>
    </item>
    <item>
      <title>D+54[Python] GUI 채팅 사용자 목록과 귓속말 파일 송수신(27강~33강)</title>
      <link>https://goodjop79.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;6월&amp;nbsp;24일(수)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 주 제 : 교육자료 27강~33강 예시문제 실습 및 이해하기&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;▶&amp;nbsp; PySide6 GUI 채팅 사용자 목록과 귓속말 파일 송수신&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 파일 전송과 데이터 스트림, 전송 메타데이터에 대한 이해&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;1.1. 스트림(Stream)의 정의&lt;/b&gt;&lt;br /&gt;가. 컴퓨터 과학 (I/O): 파일, 키보드, 네트워크 등에서 데이터가 바이트(Byte) 단위로 흐르는 가상의 연속적인 통로&lt;br /&gt;나. 데이터/이벤트 처리: 데이터를 한 번에 모아서 처리하지 않고, 실시간으로 연속해서 들어오는 데이터를 즉시 수집하고 분석&lt;br /&gt;&amp;nbsp; &amp;nbsp; (예시) Java 8 스트림 API(함수형 데이터 가공), Steam(게임 플랫폼), 미디어 스트리밍(실시간 영상/음악 재생) &lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1.2. 바이너리 데이터(Binary Data) &lt;/b&gt;&lt;br /&gt;&amp;nbsp;0, 1로 이루어진 이진 형식의 데이터로, 텍스트 형식을 제외한 이미지, 동영상, 실행 파일 등 컴퓨터가 직접 이해하는 형태의 데이터&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1.3 파일 전송 프로세스 및 메타데이터&lt;/b&gt;&lt;br /&gt;패킷의 경계 : 네트워크로 데이터를 보낼 때 데이터가 쪼개지므로, 어디서부터 어디까지가 하나의 파일인지 경계를 구분해야 하는데 이때 패킷의 경계라고 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;1.4. 전송 설계&lt;/b&gt;&lt;br /&gt;가.&amp;nbsp;&amp;nbsp;메타데이터(JSON/TXT) 우선 전송 : 파일 이름, 파일 크기(Bytes) 등의 정보를 먼저 전달한다.&lt;br /&gt;나.&amp;nbsp; 바이너리 데이터 전송 : 실제 파일 내용을 나중에 전달하며, 수신 측에서 정상 수신 완료를 확인&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.5. 메타데이터(Metadata)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터에 대한 데이터. 즉, 실제 파일 내용이 아니라 파일의 이름, 크기, 유형 등 데이터의 속성을 기술한 정보&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;[27강~30강]&amp;nbsp;사용자&amp;nbsp;목록&amp;nbsp;동기화&amp;nbsp;및&amp;nbsp;귓속말&amp;nbsp;기능&amp;nbsp;구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2.1. 사용자 목록 및 상대 선택 UI (27~28강)&lt;/b&gt;&lt;br /&gt;가. 사용자 목록 동기화: user_list를 추가하여 서버에 접속한 클라이언트 주소(client_address)를 실시간으로 표시&lt;br /&gt;&lt;br /&gt;나. UI 상호작용: 목록에서 특정 사용자를 클릭하면 selected_user에 저장되며 선택 라벨이 변경되고,&lt;br /&gt;선택된 사용자가 퇴장(종료)하면 선택 상태가 자동으로 해제된다&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2.2. 귓속말 메시지 전송 및 오류 처리 (29~30강)&lt;/b&gt;&lt;br /&gt;가. 귓속말 메커니즘: 전체에 전송하는 broadcast()와 달리, 선택된 대상(selected_user) 한 명의 소켓에만 메시지를 전송&lt;br /&gt;나. 결과 반환 및 UI 표시: 전송 성공 여부를 True/False로 반환하여 발신자에게 실패 결과를 알린다&lt;br /&gt;성공 시 내 화면에는 [귓속말 보냄], 상대 화면에는 [귓속말 받음]이 표시&lt;br /&gt;다. 오류&amp;nbsp;검증&amp;nbsp;(사전&amp;nbsp;검사&amp;nbsp;&amp;amp;&amp;nbsp;서버&amp;nbsp;검증)&lt;br /&gt;라.GUI&amp;nbsp;자체&amp;nbsp;사전&amp;nbsp;검사(4단계):&amp;nbsp;메시지&amp;nbsp;공백&amp;nbsp;여부,&amp;nbsp;상대&amp;nbsp;선택&amp;nbsp;여부,&amp;nbsp;GUI&amp;nbsp;show_error()&amp;nbsp;처리&amp;nbsp;등.&lt;br /&gt;마.서버&amp;nbsp;검증:&amp;nbsp;자기&amp;nbsp;자신에게&amp;nbsp;귓속말을&amp;nbsp;보내는&amp;nbsp;행위&amp;nbsp;차단&amp;nbsp;및&amp;nbsp;전송&amp;nbsp;실패&amp;nbsp;방어&amp;nbsp;로직&amp;nbsp;구축.&lt;br /&gt;바. 시그널과 슬롯(Signal &amp;amp; Slot): PySide6(Qt)의 핵심 이벤트 처리 메커니즘.&lt;br /&gt;버튼 클릭 등의 이벤트(Signal)가 발생하면, 그에 연결된 특정 함수나 메서드(Slot)가 자동으로 실행되는 구조&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;3.&amp;nbsp;[31강~33강]&amp;nbsp;텍스트/바이너리&amp;nbsp;차이&amp;nbsp;및&amp;nbsp;파일&amp;nbsp;분할&amp;nbsp;전송&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3.1. 텍스트와 바이너리 데이터의 변환 (31강)&lt;/b&gt;&lt;br /&gt;가. 컴퓨터는 문자열을 그대로 보낼 수 없으므로 인코딩(Encoding)을 통해 바이트로 변환한 뒤 전송하고, 수신 측은 이를 다시 문자열로 디코딩(Decoding)&lt;br /&gt;(예시) Python&lt;br /&gt;text&amp;nbsp;=&amp;nbsp;&quot;늑구&amp;nbsp;안녕&quot;&lt;br /&gt;data&amp;nbsp;=&amp;nbsp;text.encode(&quot;utf-8&quot;)&amp;nbsp;&amp;nbsp;#&amp;nbsp;바이트&amp;nbsp;데이터로&amp;nbsp;변환&amp;nbsp;(b'\xec\x95...')&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3.2. 파일 분할 전송(Chunking), 메타데이터 설계 개념 (32~34강)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;가. 소켓 점유 방지&lt;/b&gt;&lt;br /&gt;대용량 파일을 한 번에 보내면 파일 전송 동안 소켓의 send() 동작이 끝나지 않아 다른 채팅 메시지를 보낼 수 없다. 따라서 파일을 작은 조각(Chunk)으로 나누어 보내야 한다.&lt;br /&gt;&lt;b&gt;나. 데이터 인코딩 (Base64)&lt;/b&gt; : 조각낸 바이너리 파일을 안전하게 전송하기 위해 문자열 형태로 변환(Base64)하여 JSON 구조에 담아 보낸다.&lt;br /&gt;&lt;b&gt;다. 구현 및 확인 &lt;/b&gt;: * protocol.py에 send_file() 함수 추가, client.py의 ChatClient 클래스에 send_file() 메서드를 구현&lt;br /&gt;라. 파일 전송 완료 후 클라이언트에 &quot;[파일 전송 완료]&quot;가 출력되고 서버 연결이 끊김 없이 유지되는지 확인한다.&lt;br /&gt;&lt;b&gt;마. Base64&lt;/b&gt; : 바이너리(이진) 데이터를 화면에 표시할 수 있는 64개의 ASCII 문자(영문, 숫자 등)로 바꾸는 인코딩 방식&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;네트워크 전송 중 데이터가 손실되거나 왜곡되는 것을 막기 위해 사용한다.&lt;br /&gt;바. 대용량 파일 전송 시&lt;b&gt; 소켓의 동시성 한계를 극복하기 위해 파일을 조각내어 전송&lt;/b&gt;하며, &lt;b&gt;파일 조각은 Base64로 인코딩&lt;/b&gt;되어 JSON 메타데이터와 함께 전송된다. protocol.py와 client.py에 전송 기능을 구현하고, handle_file_end()에서 파일 자원을 정리하며 os 모듈 등을 활용해 저장 경로를 상수(대문자 사용)로 지정하여 파일 전송 시스템을 완성한다.&lt;br /&gt;&lt;b&gt;사.&lt;/b&gt; &lt;b&gt;핵심 구현 내용 및 관련 기술:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 분할 전송&lt;/b&gt;: 대용량 파일은 소켓 send 작업 중 차단 방지를 위해 조각(Chunk) 단위로 분할 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 인코딩&lt;/b&gt;: 파일 조각은 바이너리 데이터를 텍스트로 변환하는 Base64로 인코딩&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메타데이터/JSON&lt;/b&gt;: 파일 전송 시 파일ID, 크기 등을 포함한 정보(Metadata)를 JSON 형태로 함께 교환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 핵심&lt;/b&gt;: protocol.py의 send_file() 함수와 client.py의 메서드 구현, 수신 시 handle_file_end()의 file.close() 호출, 상수(대문자)를 이용한 RECEIVE_DIR 지정.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;&amp;lt; 예시문제 실습하기 &amp;gt;&amp;gt;&amp;nbsp; &amp;nbsp;** 가상의 환경에서 실습한 화면임 **&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;1. 27강. 사용자 목록 동기화&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdiNy/dJMcabLwyOM/zajkVVLwDMP5ihKbJVELb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdiNy/dJMcabLwyOM/zajkVVLwDMP5ihKbJVELb1/img.png&quot; data-alt=&quot;서버에 연결하여 동기화 실습 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdiNy/dJMcabLwyOM/zajkVVLwDMP5ihKbJVELb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdiNy%2FdJMcabLwyOM%2FzajkVVLwDMP5ihKbJVELb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;829&quot; height=&quot;776&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버에 연결하여 동기화 실습 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbl8Lx/dJMcahdQRNe/noFzC0ZPPa9az6jZQ9KFO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbl8Lx/dJMcahdQRNe/noFzC0ZPPa9az6jZQ9KFO0/img.png&quot; data-alt=&quot;서버에 연결하여 동기화 테스트 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbl8Lx/dJMcahdQRNe/noFzC0ZPPa9az6jZQ9KFO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbl8Lx%2FdJMcahdQRNe%2FnoFzC0ZPPa9az6jZQ9KFO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;829&quot; height=&quot;776&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버에 연결하여 동기화 테스트 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1177&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BasNR/dJMcahZnyrp/pHpXEMDrnQymBTUhAqk6rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BasNR/dJMcahZnyrp/pHpXEMDrnQymBTUhAqk6rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BasNR/dJMcahZnyrp/pHpXEMDrnQymBTUhAqk6rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBasNR%2FdJMcahZnyrp%2FpHpXEMDrnQymBTUhAqk6rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1177&quot; height=&quot;547&quot; data-origin-width=&quot;1177&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;27강 소스코드&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_client / main.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 27강. GUI 에서 메세지 보내기 (25강)&amp;nbsp;&amp;nbsp;### 이전 강의에서 추가한 부분임
# main.py
# main.py

import sys&amp;nbsp;&amp;nbsp;# 시스템 관련 파라미터 및 프로그램 종료를 위한 라이브러리
import socket&amp;nbsp;&amp;nbsp;# 네트워크 통신을 위한 소켓 라이브러리
import threading&amp;nbsp;&amp;nbsp;# 백그라운드에서 메시지를 수신하기 위한 멀티쓰레딩 라이브러리

# PySide6에서 GUI 이벤트 처리 및 시그널-슬롯 구조를 사용하기 위한 모듈 임포트
from PySide6.QtCore import Signal, Slot, Qt
from PySide6.QtWidgets import (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QApplication,&amp;nbsp;&amp;nbsp;# QT 어플리케이션을 제어하는 메인 클래스
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QWidget,&amp;nbsp;&amp;nbsp;# 기본 윈도우 창 레이아웃의 베이스 클래스
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTextEdit,&amp;nbsp;&amp;nbsp;# 멀티라인 텍스트 표시 및 편집 창 (채팅 기록 창)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLineEdit,&amp;nbsp;&amp;nbsp;# 단일라인 텍스트 입력 창 (메시지 입력 창)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QPushButton,&amp;nbsp;&amp;nbsp;# 클릭 가능한 버튼 (접속, 전송 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLabel,&amp;nbsp;&amp;nbsp;# 텍스트를 화면에 표시하는 라벨 (상태 표시용)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QVBoxLayout,&amp;nbsp;&amp;nbsp;# 위젯을 수직(세로)으로 배열하는 레이아웃
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QHBoxLayout,&amp;nbsp;&amp;nbsp;# 위젯을 수평(가로)으로 배열하는 레이아웃
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox,&amp;nbsp;&amp;nbsp;# 경고 및 알림 메시지 창을 띄우기 위한 클래스 (추가됨)
)

# 사용자 정의 프로토콜 모듈에서 메시지 송수신 함수를 임포트
from protocol import receive_messages, send_message

# 접속할 서버의 IP 주소와 포트 번호 설정 (로컬 호스트)
HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 1. 메모리 기반 사용자 데이터 (서버 인증용 딕셔너리)
USER_DB = {&quot;user1&quot;: &quot;1234&quot;, &quot;user2&quot;: &quot;5678&quot;}


# 2. 회원가입 페이지 (추가됨)
class RegisterPage(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;register_success = Signal()&amp;nbsp;&amp;nbsp;# 회원가입 성공 시 발생할 시그널

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;회원가입&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(300, 200)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout = QVBoxLayout()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.id_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.id_input.setPlaceholderText(&quot;새로운 아이디&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input.setPlaceholderText(&quot;새로운 비밀번호&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input.setEchoMode(QLineEdit.Password)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.register_btn = QPushButton(&quot;가입하기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.cancel_btn = QPushButton(&quot;취소&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(QLabel(&quot;회원가입 정보를 입력하세요&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.id_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.pw_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.register_btn)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.cancel_btn)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.register_btn.clicked.connect(self.check_register)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.cancel_btn.clicked.connect(self.close)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def check_register(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_id = self.id_input.text().strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password = self.pw_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not user_id or not password:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.warning(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self, &quot;오류&quot;, &quot;아이디와 비밀번호를 모두 입력해주세요.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if user_id in USER_DB:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.warning(self, &quot;오류&quot;, &quot;이미 존재하는 아이디입니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;USER_DB[user_id] = password
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.information(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self, &quot;성공&quot;, &quot;회원가입이 완료되었습니다!&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.register_success.emit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.close()


# 3. 로그인 페이지
class LoginPage(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 로그인 성공 시 실행할 시그널 (아이디 전달)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;login_success = Signal(str)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;로그인&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(300, 200)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout = QVBoxLayout()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.id_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.id_input.setPlaceholderText(&quot;아이디&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input.setPlaceholderText(&quot;비밀번호&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.pw_input.setEchoMode(QLineEdit.Password)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.login_btn = QPushButton(&quot;로그인&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.register_btn = QPushButton(&quot;회원가입&quot;)&amp;nbsp;&amp;nbsp;# 회원가입 이동 버튼 추가

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(QLabel(&quot;채팅 프로그램 로그인&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.id_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.pw_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.login_btn)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;layout.addWidget(self.register_btn)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.login_btn.clicked.connect(self.check_login)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.register_btn.clicked.connect(self.open_register_page)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.reg_page = None&amp;nbsp;&amp;nbsp;# 회원가입 창 객체 보유용 변수

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def check_login(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_id = self.id_input.text().strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password = self.pw_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버 인증 시뮬레이션
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if user_id in USER_DB and USER_DB[user_id] == password:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.login_success.emit(user_id)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.close()&amp;nbsp;&amp;nbsp;# 로그인 창 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.warning(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self, &quot;인증 실패&quot;, &quot;아이디 또는 비밀번호가 틀렸습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def open_register_page(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.reg_page = RegisterPage()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 회원가입 성공 시 로그인 입력창에 가입한 아이디를 세팅해주면 편리함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.reg_page.register_success.connect(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;lambda: self.id_input.setText(self.reg_page.id_input.text())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.reg_page.show()


# 4. 채팅창 페이지
class ChatWindow(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 백그라운드 쓰레드에서 수신한 메시지 데이터를 메인 GUI 쓰레드로 안전하게 전달하기 위한 커스텀 시그널 정의
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_received = Signal(dict)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()&amp;nbsp;&amp;nbsp;# 부모 클래스(QWidget)의 생성자 호출

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 초기 변수 및 상태 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_id = &quot;&quot;&amp;nbsp;&amp;nbsp;# 현재 로그인한 사용자의 아이디 저장 변수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None&amp;nbsp;&amp;nbsp;# 서버와 연결할 소켓 객체 저장 변수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = None&amp;nbsp;&amp;nbsp;# 메시지 수신을 담당할 백그라운드 쓰레드 변수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False&amp;nbsp;&amp;nbsp;# 수신 루프 실행 여부를 제어하는 플래그

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# GUI 창의 기본 속성 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;채팅방&quot;)&amp;nbsp;&amp;nbsp;# 창 제목 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(560, 480)&amp;nbsp;&amp;nbsp;# 창 크기 설정 (가로 560, 세로 480)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 채팅 기록창 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display = QTextEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setReadOnly(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 사용자가 직접 수정하지 못하도록 읽기 전용 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setPlaceholderText(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;채팅 메시지가 여기에 표시됩니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 입력창 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setEnabled(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 서버 연결 전까지는 입력 불가능하게 설정

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 전송 버튼 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button = QPushButton(&quot;보내기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.setEnabled(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 서버 연결 전까지는 비활성화 설정

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 상단 서버 접속 영역 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button = QPushButton(&quot;서버 접속&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 상단 레이아웃 구성 (상태 라벨 | 접속 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.status_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.connect_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 하단 레이아웃 구성 (메시지 입력창 | 전송 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.message_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.send_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 레이아웃 구성 (상단 -&amp;gt; 채팅창 -&amp;gt; 하단 순으로 세로 배치)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(top_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addWidget(self.chat_display)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(input_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 구성된 메인 레이아웃을 현재 위젯 창에 적용

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 이벤트 시그널과 핸들러(슬롯) 함수들을 연결 (Connect)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.clicked.connect(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_to_server
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 접속 버튼 클릭 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.clicked.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp;# 전송 버튼 클릭 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.returnPressed.connect(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.on_send_clicked
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 입력창에서 엔터키 입력 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.connect(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.display_message
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 새 메시지 시그널 수신 시 화면 표시 함수 호출

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def set_user(self, user_id):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;로그인 성공 후 유저 아이디를 세팅하고 타이틀을 변경하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_id = user_id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(f&quot;채팅방 - [{self.user_id}]님&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def connect_to_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버에 TCP 소켓 연결을 시도하고 수신 쓰레드를 시작하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# IPv4, TCP 프로토콜을 사용하는 소켓 생성 및 서버 연결 시도
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = socket.socket(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;socket.AF_INET, socket.SOCK_STREAM
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.connect((HOST, PORT))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 연결 성공 시 UI 상태 업데이트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[시스템] 서버에 연결되었습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 버튼 및 입력창 활성화/비활성화 제어
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&amp;nbsp;&amp;nbsp;# 재접속 방지를 위해 비활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setEnabled(True)&amp;nbsp;&amp;nbsp;# 입력 허용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.setEnabled(True)&amp;nbsp;&amp;nbsp;# 전송 허용

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 백그라운드 수신 쓰레드 생성 및 시작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = threading.Thread(target=self.receive_loop)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.daemon = (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;True&amp;nbsp;&amp;nbsp;# 메인 프로그램 종료 시 쓰레드도 자동 종료 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except ConnectionRefusedError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;[오류] 서버가 실행 중인지 확인하세요.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;f&quot;[오류] 연결 중 문제가 발생했습니다: {error}&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def receive_loop(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;백그라운드에서 실행되며 서버로부터 데이터를 지속적으로 수신하는 루프 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;&amp;nbsp;&amp;nbsp;# 데이터가 쪼개져 올 때를 대비한 임시 버퍼 공간

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket, buffer
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;서버와의 연결이 끊어졌습니다.&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(message)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;메시지 수신 중 오류가 발생했습니다: {error}&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_send_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;사용자가 입력한 메시지를 서버로 전송하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 보낼 때 발신자 정보(sender)도 프로토콜에 실어 보냅니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;type&quot;: &quot;chat&quot;, &quot;sender&quot;: self.user_id, &quot;content&quot;: text},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;나: {text}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 메시지 전송 실패: {error}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Slot(dict)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def display_message(self, message):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;수신 쓰레드가 보낸 시그널을 받아 화면에 분류 처리하는 슬롯 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내가 보낸 메시지가 상대방에게서 다시 브로드캐스트되어 오는 중복 출력을 방지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if sender != self.user_id:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;{sender}: {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;system&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[시스템] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;error&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[알 수 없는 메시지] {message}&quot;)


# 5. 흐름 제어 및 프로그램 실행 진입점
if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app = QApplication(sys.argv)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;login_page = LoginPage()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chat_window = ChatWindow()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 로그인 성공 시그널 연결 로직
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_login_success(user_id):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chat_window.set_user(user_id)&amp;nbsp;&amp;nbsp;# 채팅창에 로그인된 유저 세팅
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chat_window.show()&amp;nbsp;&amp;nbsp;# 채팅창 켜기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chat_window.connect_to_server()&amp;nbsp;&amp;nbsp;# 로그인 완료와 동시에 서버 접속 자동 실행

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;login_page.login_success.connect(on_login_success)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;login_page.show()&amp;nbsp;&amp;nbsp;# 처음에 로그인 화면 띄우기

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;27강 소스코드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_server / server.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 27강. 사용자 목록 동기화&amp;nbsp;&amp;nbsp;### 수정 구현한 부분
# server.py
import socket
import threading

from protocol import receive_messages, send_message

HOST = &quot;127.0.0.1&quot;
PORT = 5000

clients = []


def print_clients():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;현재 접속자 수: {len(clients)}&quot;)


def get_user_list():&amp;nbsp;&amp;nbsp;### 2단계에서 구현
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return [client[&quot;address&quot;] for client in clients]


def broadcast_user_list():&amp;nbsp;&amp;nbsp;### 2단계에서 구현
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_list_message = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;user_list&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;users&quot;: get_user_list()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client[&quot;socket&quot;], user_list_message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass


def broadcast(message, sender_socket=None):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket = client[&quot;socket&quot;]&amp;nbsp;&amp;nbsp;### 딕셔너리 구조에 맞게 접근

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client_socket != sender_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass


def send_error(client_socket, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})


def remove_client(client_socket):&amp;nbsp;&amp;nbsp;###️ 딕셔너리에서 찾아 제거
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client[&quot;socket&quot;] == client_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.remove(client)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return client[&quot;address&quot;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None


def handle_client(client_socket, client_address):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 시작: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 입장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()&amp;nbsp;&amp;nbsp;### 입장 후 목록 동기화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 연결 끊김: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{client_address} 메시지: {message}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;exit&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 종료 요청: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;).strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not content:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;빈 메시지는 보낼 수 없습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sender&quot;: address_text,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, f&quot;지원하지 않는 메시지 타입입니다: {message_type}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if exit_requested:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:&amp;nbsp;&amp;nbsp;### with 대신 try/finally로 소켓 관리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;removed_address = remove_client(client_socket)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 퇴장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()&amp;nbsp;&amp;nbsp;###퇴장 후 목록 동기화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에서 제거: {removed_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 종료: {client_address}&quot;)


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.bind((HOST, PORT))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.listen()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;서버를 시작합니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;클라이언트 접속 대기 중...&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket, client_address = server_socket.accept()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 접속: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.append({&amp;nbsp;&amp;nbsp;### 소켓+주소 딕셔너리로 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;socket&quot;: client_socket,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;address&quot;: address_text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에 추가: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread = threading.Thread(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target=handle_client,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;args=(client_socket, client_address)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.daemon = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.start()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 28강. 상대 선택 UI 만들기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zoELf/dJMb99Nzvig/VKdIuJjFwbEvwnimcuu7J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zoELf/dJMb99Nzvig/VKdIuJjFwbEvwnimcuu7J1/img.png&quot; data-alt=&quot;채팅 상대를 선택한 후 채팅을 하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zoELf/dJMb99Nzvig/VKdIuJjFwbEvwnimcuu7J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzoELf%2FdJMb99Nzvig%2FVKdIuJjFwbEvwnimcuu7J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;701&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅 상대를 선택한 후 채팅을 하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2FuMN/dJMcab5KECl/3acTgOSBgNstpXxo1nKjt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2FuMN/dJMcab5KECl/3acTgOSBgNstpXxo1nKjt0/img.png&quot; data-alt=&quot;채팅 대상을 선택하여 대상자하고만 채팅을 하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2FuMN/dJMcab5KECl/3acTgOSBgNstpXxo1nKjt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2FuMN%2FdJMcab5KECl%2F3acTgOSBgNstpXxo1nKjt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;701&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅 대상을 선택하여 대상자하고만 채팅을 하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbcUU/dJMb99Nzvih/kCozfWeHtRuF90v6Qt0S5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbcUU/dJMb99Nzvih/kCozfWeHtRuF90v6Qt0S5k/img.png&quot; data-alt=&quot;서버에서 대상자들과 채팅하는 목록들이 보이는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbcUU/dJMb99Nzvih/kCozfWeHtRuF90v6Qt0S5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbcUU%2FdJMb99Nzvih%2FkCozfWeHtRuF90v6Qt0S5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;313&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버에서 대상자들과 채팅하는 목록들이 보이는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wlUMV/dJMcaasgHiU/XNyEGkJlYKA1Uoat0ZKY21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wlUMV/dJMcaasgHiU/XNyEGkJlYKA1Uoat0ZKY21/img.png&quot; data-alt=&quot;다수의 채팅자 중에서 상대방 1명을 선택하여 채팅하는 서버목록 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wlUMV/dJMcaasgHiU/XNyEGkJlYKA1Uoat0ZKY21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwlUMV%2FdJMcaasgHiU%2FXNyEGkJlYKA1Uoat0ZKY21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;352&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다수의 채팅자 중에서 상대방 1명을 선택하여 채팅하는 서버목록 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;28강 소스코드&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_client / main.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# main.py
import socket&amp;nbsp;&amp;nbsp;# 서버와 TCP 통신을 하기 위한 소켓 모듈
import sys&amp;nbsp;&amp;nbsp;# 프로그램 종료 및 인자 전달을 위한 모듈
import threading&amp;nbsp;&amp;nbsp;# 백그라운드에서 메시지를 계속 받기 위한 스레드 모듈

# PySide6에서 UI 이벤트 처리(시그널, 슬롯)를 위해 가져온 모듈
from PySide6.QtCore import Signal, Slot
# PySide6에서 창을 만들고 위젯들을 배치하기 위해 가져온 모듈들
from PySide6.QtWidgets import (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QApplication,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTextEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLineEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QPushButton,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLabel,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QListWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QVBoxLayout,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QHBoxLayout,
)

# 직접 만든 protocol 모듈에서 메시지 송수신용 함수를 가져옴
from protocol import receive_messages, send_message

# 접속할 서버 정보 (로컬 호스트, 포트 번호 5000)
HOST = &quot;127.0.0.1&quot;
PORT = 5000


class ChatWindow(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 커스텀 시그널 정의: 서버로부터 딕셔너리 형태의 메시지를 받으면 UI 스레드로 전달함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_received = Signal(dict)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 초기 상태 변수들 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None&amp;nbsp;&amp;nbsp;# 서버와 연결될 소켓 객체 저장용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = None&amp;nbsp;&amp;nbsp;# 메시지 수신을 담당할 스레드 객체 저장용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False&amp;nbsp;&amp;nbsp;# 수신 루프를 돌릴지 제어하는 플래그
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None&amp;nbsp;&amp;nbsp;# 현재 리스트에서 선택한 상대방 이름

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 창 타이틀과 크기 지정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;채팅&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(700, 480)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- UI 위젯들 생성 ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)&amp;nbsp;&amp;nbsp;# 연결 상태 표시 라벨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button = QPushButton(&quot;서버 접속&quot;)&amp;nbsp;&amp;nbsp;# 접속 버튼

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display = QTextEdit()&amp;nbsp;&amp;nbsp;# 채팅 창 (메시지 보여주는 곳)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setReadOnly(True)&amp;nbsp;&amp;nbsp;# 사용자가 직접 수정 못 하게 읽기 전용으로 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setPlaceholderText(&quot;채팅 메시지가 여기에 표시됩니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list = QListWidget()&amp;nbsp;&amp;nbsp;# 접속자 명단 리스트 위젯
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.setMaximumWidth(150)&amp;nbsp;&amp;nbsp;# 가로 길이를 최대 150으로 제한

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label = QLabel(&quot;선택한 상대: 없음&quot;)&amp;nbsp;&amp;nbsp;# 선택된 귓속말 상대 표시용

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input = QLineEdit()&amp;nbsp;&amp;nbsp;# 메시지 입력창
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button = QPushButton(&quot;보내기&quot;)&amp;nbsp;&amp;nbsp;# 전송 버튼

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- 레이아웃 배치 (UI 뼈대 잡기) ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 상단 레이아웃: 상태 라벨과 접속 버튼을 가로로 배치
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.status_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.connect_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 유저 목록 레이아웃: 선택된 상대 라벨과 유저 리스트를 세로로 배치
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.selected_user_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.user_list)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 중간 레이아웃: 채팅창과 유저 목록 레이아웃을 가로로 배치
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addWidget(self.chat_display)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addLayout(user_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 하단 레이아웃: 입력창과 전송 버튼을 가로로 배치
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.message_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.send_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 5. 메인 레이아웃: 상단, 중간, 하단 레이아웃을 위에서 아래로 순서대로 쌓음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(top_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(content_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(input_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 위에서 만든 메인 레이아웃을 이 윈도우 창의 레이아웃으로 등록
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(main_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- 이벤트 연결 (시그널과 슬롯 바인딩) ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.clicked.connect(self.connect_to_server)&amp;nbsp;&amp;nbsp;# 접속 버튼 클릭 시 서버 연결
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.clicked.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp;# 보내기 버튼 클릭 시 메시지 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.returnPressed.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp;# 입력창에서 엔터 쳐도 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.connect(self.display_message)&amp;nbsp;&amp;nbsp;# 메시지 수신 시그널이 오면 화면에 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.itemClicked.connect(self.on_user_selected)&amp;nbsp;&amp;nbsp;# 유저 리스트 클릭 시 상대방 선택

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 맨 처음에는 연결되지 않은 상태의 UI 세팅을 적용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def set_connected_state(self, connected):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;연결 상태에 따라 버튼이나 입력창의 활성화/비활성화 상태를 바꿔주는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(not connected)&amp;nbsp;&amp;nbsp;# 연결되면 접속 버튼 비활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setEnabled(connected)&amp;nbsp;&amp;nbsp;# 연결되어야 입력창 활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.setEnabled(connected)&amp;nbsp;&amp;nbsp;# 연결되어야 보내기 버튼 활성화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.clear()&amp;nbsp;&amp;nbsp;# 연결 끊기면 유저 리스트 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(&quot;선택한 상대: 없음&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def connect_to_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버 접속 버튼을 눌렀을 때 실행되는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# TCP 소켓을 생성하고 설정된 HOST와 PORT로 접속 시도
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.connect((HOST, PORT))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 백그라운드에서 실시간으로 메시지를 받을 수신 스레드 가동
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = threading.Thread(target=self.receive_loop)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.daemon = True&amp;nbsp;&amp;nbsp;# 메인 프로그램 종료 시 스레드도 같이 자동 종료되도록 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 상태를 연결됨 상태로 전환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[시스템] 서버에 연결되었습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except ConnectionRefusedError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버가 켜져있지 않는 등 연결 거부되었을 때 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버가 실행 중인지 확인하세요.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 그 외 예상치 못한 에러 발생 시 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 연결 중 문제가 발생했습니다: {error}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def receive_loop(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;별도의 스레드에서 돌면서 서버로부터 오는 데이터를 무한 대기하는 루프 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;&amp;nbsp;&amp;nbsp;# 잘려서 들어오는 데이터를 임시 저장할 버퍼 공간

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# protocol 모듈의 함수를 통해 서버 메시지 수신 시도
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(self.client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버가 먼저 연결을 끊은 경우
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;서버와의 연결이 끊어졌습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 정상적으로 받아온 메시지가 있다면 하나씩 UI 스레드로 던져줌
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(message)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except OSError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 소켓이 강제로 닫히거나 에러가 났을 때 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 기타 에러 발생 시 UI에 에러 전송 후 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;메시지 수신 중 오류가 발생했습니다: {error}&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 루프를 빠져나왔다는 건 연결이 끝났다는 의미이므로 상태 정리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;state&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;connected&quot;: False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_send_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;보내기 버튼을 누르거나 엔터를 쳤을 때 실행되는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip()&amp;nbsp;&amp;nbsp;# 앞뒤 공백 제거한 텍스트 가져오기

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return&amp;nbsp;&amp;nbsp;# 입력된 내용이 없으면 아무것도 안 함

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 만약 입력창에 'exit'라고 치면 서버와의 연결을 끊음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if text == &quot;exit&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# protocol 모듈을 사용해 채팅 타입의 데이터를 JSON(or 프로토콜 형태)으로 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내가 보낸 메시지를 내 채팅창에도 띄워주고 입력창 비우기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;나: {text}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 전송 실패 시 에러 띄우고 연결 종료 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 메시지 전송 실패: {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def disconnect_from_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버와의 연결을 안전하게 종료하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버에게 '나 나간다'고 알려주는 exit 메시지 전송 시도
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {&quot;type&quot;: &quot;exit&quot;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass&amp;nbsp;&amp;nbsp;# 보내다 실패해도 무시하고 그냥 닫음

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False&amp;nbsp;&amp;nbsp;# 수신 스레드 루프 정지용 플래그 OFF

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.close()&amp;nbsp;&amp;nbsp;# 소켓 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)&amp;nbsp;&amp;nbsp;# UI 상태를 끊김 상태로 변경
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[시스템] 서버와의 연결을 종료했습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Slot(dict)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def display_message(self, message):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;수신 스레드가 시그널로 던져준 메시지를 분석해서 실제 화면에 그려주는 슬롯 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 일반 채팅 메시지일 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;{sender}: {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 시스템 공지 메시지일 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;system&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[시스템] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 에러 메시지일 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;error&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 유저 목록 업데이트 지시일 때 (서버가 주기적으로 전체 유저를 쏴줌)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;user_list&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;users = message.get(&quot;users&quot;, [])
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.update_user_list(users)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 5. 연결 상태가 변했을 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;state&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connected = message.get(&quot;connected&quot;, False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 6. 그 외 정의되지 않은 메시지 타입 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[알 수 없는 메시지] {message}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_user_selected(self, item):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;우측 리스트에서 특정 유저 아이템을 클릭했을 때 실행되는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = item.text()&amp;nbsp;&amp;nbsp;# 클릭한 아이템의 텍스트(이름) 저장
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(f&quot;선택한 상대: {self.selected_user}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def update_user_list(self, users):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버가 보내준 최신 접속자 명단으로 QListWidget을 갱신하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.clear()&amp;nbsp;&amp;nbsp;# 기존 목록 싹 지우기

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for user in users:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.addItem(user)&amp;nbsp;&amp;nbsp;# 유저 한 명씩 추가

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 만약 내가 선택해뒀던 유저가 방을 나가서 최신 명단에 없다면 선택 해제 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.selected_user not in users:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(&quot;선택한 상대: 없음&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def closeEvent(self, event):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;사용자가 우측 상단 'X' 버튼을 눌러서 프로그램을 끌 때 자동으로 호출되는 이벤트 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()&amp;nbsp;&amp;nbsp;# 그냥 꺼지지 말고 서버에 로그아웃 알린 후 소켓 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;event.accept()&amp;nbsp;&amp;nbsp;# 창 닫기 요청 승인


# --- 프로그램 시작점 (실행 루프) ---
app = QApplication(sys.argv)&amp;nbsp;&amp;nbsp;# PySide6 애플리케이션 생성

window = ChatWindow()&amp;nbsp;&amp;nbsp;# 내가 만든 채팅창 인스턴스 생성
window.show()&amp;nbsp;&amp;nbsp;# 화면에 띄우기

sys.exit(app.exec())&amp;nbsp;&amp;nbsp;# GUI 이벤트 루프 시작 (창이 닫힐 때까지 대기 및 프로그램 정상 종료 유도)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;28강 소스코드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_server / server.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 28강. 사용자 목록 동기화&amp;nbsp;&amp;nbsp;### 수정 구현한 부분
# server.py
import socket
import threading

# custom 프로토콜 모듈에서 메시지 수신/송신 함수를 가져옴
from protocol import receive_messages, send_message

# 서버가 실행될 IP 주소와 포트 번호 설정
HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 현재 접속한 클라이언트들의 정보를 저장할 리스트 (딕셔너리 형태로 저장할 예정)
clients = []


def print_clients():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;현재 서버에 접속 중인 클라이언트 수를 콘솔에 출력하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;현재 접속자 수: {len(clients)}&quot;)


def get_user_list():&amp;nbsp;&amp;nbsp;### 2단계에서 구현
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;현재 접속 중인 모든 클라이언트의 IP/포트 주소 텍스트만 뽑아서 리스트로 반환함&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return [client[&quot;address&quot;] for client in clients]


def broadcast_user_list():&amp;nbsp;&amp;nbsp;### 2단계에서 구현
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;접속 중인 모든 클라이언트에게 현재 유저 리스트를 전송해서 동기화하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_list_message = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;user_list&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;users&quot;: get_user_list()&amp;nbsp;&amp;nbsp;# 위에서 만든 주소 리스트를 담음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# clients[:]를 쓰는 이유는 루프 도중 리스트가 변경되어도 안전하게 복사본으로 돌리기 위함이야
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 각각의 클라이언트 소켓으로 유저 리스트 메시지를 보냄
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client[&quot;socket&quot;], user_list_message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 에러가 나면 그냥 넘어가 (나중에 종료 처리될 테니까)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass


def broadcast(message, sender_socket=None):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;메시지를 현재 접속한 모든 클라이언트에게 뿌리는 함수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;단, sender_socket이 지정되면 메시지를 보낸 본인에게는 다시 보내지 않아
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket = client[&quot;socket&quot;]&amp;nbsp;&amp;nbsp;### 딕셔너리 구조에 맞게 키값으로 소켓을 꺼냄

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지를 보낸 사람이 아닌 다른 클라이언트들에게만 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client_socket != sender_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass


def send_error(client_socket, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;클라이언트에게 에러 메시지를 정해진 프로토콜 양식에 맞춰 전송하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})


def remove_client(client_socket):&amp;nbsp;&amp;nbsp;###️ 딕셔너리에서 찾아 제거
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;인자로 받은 소켓을 가진 클라이언트를 찾아서 리스트에서 지우는 함수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;지운 클라이언트의 주소를 반환함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client[&quot;socket&quot;] == client_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.remove(client)&amp;nbsp;&amp;nbsp;# 리스트에서 삭제
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return client[&quot;address&quot;]&amp;nbsp;&amp;nbsp;# 삭제된 클라이언트 주소 반환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None


def handle_client(client_socket, client_address):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;클라이언트 한 명당 하나의 스레드로 실행되어 메시지 수신을 전담하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 시작: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;&amp;nbsp;&amp;nbsp;# TCP 스트림 데이터 파싱을 위한 개별 버퍼 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 새로운 유저가 입장했음을 본인을 제외한 다른 모든 사람에게 알림
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 입장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()&amp;nbsp;&amp;nbsp;### 입장했으니 유저들에게 새로운 목록 동기화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 수신 무한 루프 시작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 데이터를 수신하고 메시지가 잘라져 왔을 때를 대비해 버퍼를 업데이트함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 연결이 끊겼다고 판단되면 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 연결 끊김: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 수신된 메시지들을 하나씩 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{client_address} 메시지: {message}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 클라이언트가 종료 요청('exit')을 보냈을 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;exit&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 종료 요청: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break&amp;nbsp;&amp;nbsp;# 메시지 처리 루프 탈출

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 클라이언트가 일반 채팅('chat')을 보냈을 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;).strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 빈 메시지 방지 로직
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not content:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;빈 메시지는 보낼 수 없습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue&amp;nbsp;&amp;nbsp;# 다음 메시지로 넘어감
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내용이 잘 있으면 다른 모든 클라이언트에게 채팅 내용 방송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sender&quot;: address_text,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 정해진 타입(exit, chat)이 아닐 경우 에러 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, f&quot;지원하지 않는 메시지 타입입니다: {message_type}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# exit 요청을 받았다면 수신 무한 루프 자체를 빠져나감
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if exit_requested:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:&amp;nbsp;&amp;nbsp;###예외가 발생하든 루프를 빠져나왔든 무조건 실행해서 소켓 안전하게 정리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket.close()&amp;nbsp;&amp;nbsp;# 소켓 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 접속자 리스트에서 해당 클라이언트 지우기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;removed_address = remove_client(client_socket)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 유저가 나갔음을 다른 유저들에게 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 퇴장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()&amp;nbsp;&amp;nbsp;###퇴장했으니 유저들에게 바뀐 목록 다시 동기화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에서 제거: {removed_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 종료: {client_address}&quot;)


# 서버 소켓 생성 및 바인딩 (with 구문 사용으로 프로그램 종료 시 소켓 자동 반환)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 소켓 재사용 옵션 설정 (서버가 강제 종료된 후 재실행할 때 포트가 묶여있는 에러 방지)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.bind((HOST, PORT))&amp;nbsp;&amp;nbsp;# 설정한 IP와 포트에 서버 소켓 연결
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.listen()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 클라이언트 접속 대기 상태로 진입

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;서버를 시작합니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;클라이언트 접속 대기 중...&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 새로운 클라이언트를 계속해서 받아들이는 메인 루프
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 클라이언트가 접속하면 소켓 객체와 주소를 받아옴 (Blocking 방식)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket, client_address = server_socket.accept()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 접속: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;### 관리하기 편하도록 소켓과 주소를 하나의 딕셔너리로 묶어서 리스트에 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.append({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;socket&quot;: client_socket,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;address&quot;: address_text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에 추가: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 각 클라이언트마다 독립된 스레드를 생성해서 handle_client 함수를 실행함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread = threading.Thread(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target=handle_client,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;args=(client_socket, client_address)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 스레드가 종료되면 이 스레드도 같이 강제 종료되도록 데몬 스레드로 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.daemon = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.start()&amp;nbsp;&amp;nbsp;# 스레드 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 29강. 귓속말 메세지 전송 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b34GrA/dJMcaglE0WI/GPDQDkpO6hW4nki4Mnxr90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b34GrA/dJMcaglE0WI/GPDQDkpO6hW4nki4Mnxr90/img.png&quot; data-alt=&quot;귓속말을 대상자를 지정해서 하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b34GrA/dJMcaglE0WI/GPDQDkpO6hW4nki4Mnxr90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb34GrA%2FdJMcaglE0WI%2FGPDQDkpO6hW4nki4Mnxr90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;780&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;귓속말을 대상자를 지정해서 하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;781&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AGLiw/dJMcahx8P56/qQj86dH0i7h6Vr2TEXFVZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AGLiw/dJMcahx8P56/qQj86dH0i7h6Vr2TEXFVZk/img.png&quot; data-alt=&quot;대상자를 지정한 경우에 귓속말 메세지는 대상자에게만 전달하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AGLiw/dJMcahx8P56/qQj86dH0i7h6Vr2TEXFVZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAGLiw%2FdJMcahx8P56%2FqQj86dH0i7h6Vr2TEXFVZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1099&quot; height=&quot;781&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;781&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대상자를 지정한 경우에 귓속말 메세지는 대상자에게만 전달하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;29강 소스코드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_client / main.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 29강. 귓속말 메세지 전송 구현
import socket
import sys
import threading

# PySide6에서 UI 이벤트(Signal/Slot) 처리를 위한 모듈 임포트
from PySide6.QtCore import Signal, Slot
# PySide6에서 GUI 구성에 필요한 위젯 및 레이아웃 임포트
from PySide6.QtWidgets import (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QApplication,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTextEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLineEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QPushButton,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLabel,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QListWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QVBoxLayout,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QHBoxLayout,
)

# 별도로 정의된 프로토콜 모듈에서 메시지 송수신 함수 임포트
from protocol import receive_messages, send_message

# 서버 접속 정보 설정 (로컬 호스트, 포트 5000)
HOST = &quot;127.0.0.1&quot;
PORT = 5000


class ChatWindow(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 커스텀 시그널 정의: 서버로부터 받은 딕셔너리 형태의 메시지를 GUI 스레드로 전달 목적
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_received = Signal(dict)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 네트워크 및 스레드 관련 상태 변수 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버와 연결할 소켓
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = None&amp;nbsp;&amp;nbsp; # 메시지 수신을 담당할 백그라운드 스레드
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 스레드 루프 제어 플래그
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 현재 귓속말 대상으로 선택된 유저 이름

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 윈도우 창 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;채팅&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(700, 480)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- UI 위젯 생성 ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button = QPushButton(&quot;서버 접속&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display = QTextEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setReadOnly(True)&amp;nbsp;&amp;nbsp;# 채팅창은 읽기 전용으로 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setPlaceholderText(&quot;채팅 메시지가 여기에 표시됩니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list = QListWidget()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 접속자 목록을 표시할 리스트 위젯
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.setMaximumWidth(150)&amp;nbsp;&amp;nbsp;# 가로 최대 크기 제한

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label = QLabel(&quot;선택한 상대: 없음&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input = QLineEdit()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 메시지 입력창
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button = QPushButton(&quot;보내기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button = QPushButton(&quot;귓속말&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- UI 레이아웃 배치 ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 상단 레이아웃 (상태 표시, 접속 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.status_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.connect_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 우측 접속자 목록 레이아웃 (선택된 상대 표시, 리스트)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.selected_user_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.user_list)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 중앙 레이아웃 (채팅창 + 우측 접속자 목록)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addWidget(self.chat_display)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addLayout(user_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 하단 레이아웃 (메시지 입력창, 보내기 버튼, 귓속말 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.message_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.send_button)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.whisper_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 5. 메인 레이아웃 (상단 + 중앙 + 하단 전체 세로 배치)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(top_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(content_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(input_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(main_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# --- 시그널과 슬롯(이벤트 핸들러) 연결 ---
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.clicked.connect(self.connect_to_server)&amp;nbsp;&amp;nbsp;# 접속 버튼 클릭 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.clicked.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 보내기 버튼 클릭 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.returnPressed.connect(self.on_send_clicked) # 입력창에서 엔터 키 입력 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button.clicked.connect(self.on_whisper_clicked)&amp;nbsp;&amp;nbsp;# 귓속말 버튼 클릭 시
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.connect(self.display_message)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 수신 시그널 발생 시 GUI 업데이트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.itemClicked.connect(self.on_user_selected)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 유저 목록 클릭 시

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 초기 상태 설정 (연결되지 않음 상태로 위젯 비활성화)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def set_connected_state(self, connected):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;연결 상태에 따라 UI 위젯들의 활성화/비활성화 상태를 제어하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(not connected) # 연결되면 접속 버튼 비활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setEnabled(connected)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 연결되어야 입력창 활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.setEnabled(connected)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 연결되어야 보내기 버튼 활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button.setEnabled(connected)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 연결되어야 귓속말 버튼 활성화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(&quot;선택한 상대: 없음&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def connect_to_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버 소켓 연결을 시도하고 수신용 백그라운드 스레드를 실행하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# TCP 소켓 생성 및 서버 연결
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.connect((HOST, PORT))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 백그라운드 메시지 수신 스레드 시작
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = threading.Thread(target=self.receive_loop)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.daemon = True # 메인 프로세스 종료 시 함께 종료되도록 데몬 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 상태 업데이트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[시스템] 서버에 연결되었습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except ConnectionRefusedError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버가 실행 중인지 확인하세요.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 연결 중 문제가 발생했습니다: {error}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def receive_loop(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;백그라운드 스레드에서 무한 루프를 돌며 서버의 메시지를 수신하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# protocol 모듈의 함수를 통해 메시지 파싱 및 수신
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(self.client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 연결이 끊어졌다면 GUI 스레드로 시스템 알림 발생 후 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;서버와의 연결이 끊어졌습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 수신된 메시지가 있다면 순차적으로 GUI 스레드에 전송(Emit)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(message)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except OSError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 소켓이 강제로 닫히는 등의 OS 에러 발생 시 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;메시지 수신 중 오류가 발생했습니다: {error}&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 루프가 끝나면 스레드를 정지하고 연결 종료 상태를 GUI에 알림
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;state&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;connected&quot;: False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_send_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;일반 채팅 메시지를 전송하는 슬롯 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip() # 공백 제거 후 텍스트 가져오기

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 'exit' 입력 시 접속 종료 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if text == &quot;exit&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 프로토콜 규격(JSON 형태의 딕셔너리)에 맞춰 데이터 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내가 보낸 메시지를 화면에 출력하고 입력창 비우기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;나: {text}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 메시지 전송 실패: {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_whisper_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;선택된 상대에게 귓속말을 전송하는 슬롯 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.selected_user is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[오류] 귓속말 상대를 선택하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 귓속말 프로토콜 데이터 전송 (대상 target 포함)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;whisper&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;target&quot;: self.selected_user,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[귓속말 보냄] {self.selected_user}: {text}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] 귓속말 전송 실패: {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def disconnect_from_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버와의 연결을 안전하게 종료하는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버에 종료하겠다는 의사(exit)를 전달
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {&quot;type&quot;: &quot;exit&quot;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.close() # 소켓 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(&quot;[시스템] 서버와의 연결을 종료했습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Slot(dict)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def display_message(self, message):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;message_received 시그널을 전달받아 타입별로 UI에 메시지를 출력하는 슬롯 함수 (GUI 스레드에서 실행됨)&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 일반 채팅 메시지 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;{sender}: {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 시스템 공지 메시지 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;system&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[시스템] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 에러 메시지 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;error&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 실시간 유저 목록 업데이트 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;user_list&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;users = message.get(&quot;users&quot;, [])
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.update_user_list(users)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 5. 수신된 귓속말 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;whisper&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[귓속말 받음] {sender}: {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 6. 스레드 루프 종료에 따른 상태 비활성화 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif message_type == &quot;state&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connected = message.get(&quot;connected&quot;, False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;30강. 귓속말 오류 처리와 표시 방식 실습하기&amp;nbsp; &amp;nbsp; *** 가상의 주소와 서버 화면 실습 전용 ***&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGuEHM/dJMcaicNQC8/zJ3GCajzImK0JSGdwD5vGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGuEHM/dJMcaicNQC8/zJ3GCajzImK0JSGdwD5vGk/img.png&quot; data-alt=&quot;귀속말을 전송 했을때 오류메세지 &amp;amp;amp; 대상자 지정후 귓속말 전송 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGuEHM/dJMcaicNQC8/zJ3GCajzImK0JSGdwD5vGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGuEHM%2FdJMcaicNQC8%2FzJ3GCajzImK0JSGdwD5vGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1010&quot; height=&quot;873&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;귀속말을 전송 했을때 오류메세지 &amp;amp; 대상자 지정후 귓속말 전송 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZQMEk/dJMcadvDMz0/8GAlEpdkYXwcJk5KuRKXE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZQMEk/dJMcadvDMz0/8GAlEpdkYXwcJk5KuRKXE0/img.png&quot; data-alt=&quot;자신에게는 귓속말을 보낼수 없다는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZQMEk/dJMcadvDMz0/8GAlEpdkYXwcJk5KuRKXE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZQMEk%2FdJMcadvDMz0%2F8GAlEpdkYXwcJk5KuRKXE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;890&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;자신에게는 귓속말을 보낼수 없다는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TI8yT/dJMcaaFPVDb/HzSLX3Zh3LPQEk70oTG8ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TI8yT/dJMcaaFPVDb/HzSLX3Zh3LPQEk70oTG8ek/img.png&quot; data-alt=&quot;대상자를 지정해서 귓속말을 보내는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TI8yT/dJMcaaFPVDb/HzSLX3Zh3LPQEk70oTG8ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTI8yT%2FdJMcaaFPVDb%2FHzSLX3Zh3LPQEk70oTG8ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1383&quot; height=&quot;882&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;882&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대상자를 지정해서 귓속말을 보내는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;30강. 소스코드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_client / main.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 30강. 귓속말 오류 처리와 표시 방식
# main.py

import socket
import sys
import threading

# PySide6 구성 요소 임포트 (GUI 및 시그널/슬롯)
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QApplication,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTextEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLineEdit,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QPushButton,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QLabel,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QListWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QVBoxLayout,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QHBoxLayout,
)

# 사용자 정의 프로토콜 모듈 임포트 (메시지 송수신 함수)
from protocol import receive_messages, send_message

# 서버 접속 정보 설정
HOST = &quot;127.0.0.1&quot;
PORT = 5000


class ChatWindow(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 멀티스레드에서 수신한 메시지를 메인 GUI 스레드로 전달하기 위한 시그널 정의
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_received = Signal(dict)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 클라이언트 내부 상태 관리 변수 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 서버와 연결할 소켓
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = None&amp;nbsp;&amp;nbsp; # 메시지 수신을 전담할 스레드
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 수신 루프 실행 제어 플래그
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 리스트에서 선택된 귓속말 상대방 주소
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.my_address = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 서버가 할당한 내 고유 주소

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# GUI 창 기본 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;채팅&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.resize(700, 480)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 상단 영역 위젯 (상태 표시 및 접속 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button = QPushButton(&quot;서버 접속&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 중앙 영역 위젯 (채팅창 및 유저 리스트)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display = QTextEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setReadOnly(True) # 채팅창 수정 불가 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.setPlaceholderText(&quot;채팅 메시지가 여기에 표시됩니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list = QListWidget()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 접속자 목록 표시 리스트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.setMaximumWidth(150)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 하단 영역 위젯 (귓속말 상대 표시, 입력창, 전송 버튼)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label = QLabel(&quot;선택한 상대: 없음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input = QLineEdit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button = QPushButton(&quot;보내기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button = QPushButton(&quot;귓속말&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 레이아웃 배치 - 상단 조립
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.status_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;top_layout.addWidget(self.connect_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 레이아웃 배치 - 우측 접속자 목록 조립
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.selected_user_label)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_layout.addWidget(self.user_list)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 레이아웃 배치 - 중앙 콘텐츠 조립
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addWidget(self.chat_display)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content_layout.addLayout(user_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# UI 레이아웃 배치 - 하단 입력부 조립
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.message_input)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.send_button)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;input_layout.addWidget(self.whisper_button)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 전체 메인 레이아웃 병합 및 등록
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout = QVBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(top_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(content_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addLayout(input_layout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setLayout(main_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 위젯 이벤트(시그널)와 제어 메서드(슬롯) 연결
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.clicked.connect(self.connect_to_server)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 접속 버튼 클릭
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.clicked.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 보내기 버튼 클릭
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.returnPressed.connect(self.on_send_clicked)&amp;nbsp;&amp;nbsp; # 입력창 엔터키 입력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button.clicked.connect(self.on_whisper_clicked)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 귓속말 버튼 클릭
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.connect(self.display_message)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 수신 시그널 발생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.itemClicked.connect(self.on_user_selected)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 유저 목록 클릭

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 초기 화면 상태 설정 (미연결 상태 위젯 비활성화)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def show_error(self, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;오류 메시지를 채팅창에 붉은 텍스트 등으로 구분하여 표시하기 위한 래퍼 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[오류] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def show_system(self, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;시스템 안내 메시지를 채팅창에 표시하기 위한 래퍼 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[시스템] {content}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def set_connected_state(self, connected):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;연결 상태에 따라 UI 위젯들의 활성화/비활성화 및 텍스트를 제어하는 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(not connected) # 연결 상태면 접속 버튼 비활성화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.setEnabled(connected)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.send_button.setEnabled(connected)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.whisper_button.setEnabled(connected)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버에 연결되지 않음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.clear()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 접속 끊김 시 유저 목록 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 선택된 상대 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(&quot;선택한 상대: 없음&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.my_address = None&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 내 주소 초기화

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def connect_to_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버 접속을 시도하고 성공 시 수신 백그라운드 스레드를 시작하는 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# TCP 소켓 생성 및 서버 연결
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.connect((HOST, PORT))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.my_address = str(self.client_socket.getsockname()) # 내 고유 IP/Port 저장

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 백그라운드 메시지 수신 스레드 생성 및 실행
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread = threading.Thread(target=self.receive_loop)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.daemon = True # 메인 스레드 종료 시 함께 종료되도록 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.receive_thread.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_system(&quot;서버에 연결되었습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except ConnectionRefusedError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;서버가 실행 중인지 확인하세요.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.connect_button.setEnabled(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(f&quot;연결 중 문제가 발생했습니다: {error}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def receive_loop(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;백그라운드에서 서버가 보낸 데이터를 지속적으로 수신하는 무한 루프 메서드 (스레드 전용)&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# protocol 모듈을 통해 메시지 단위 분할 수신
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(self.client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected: # 서버가 연결을 끊은 경우
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: &quot;서버와의 연결이 끊어졌습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 수신된 메시지들을 하나씩 메인 GUI 스레드로 전달
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit(message)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except OSError: # 소켓이 강제로 닫힌 경우 루프 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;메시지 수신 중 오류가 발생했습니다: {error}&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 루프가 끝나면 연결 종료 처리 시그널 발생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_received.emit({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;state&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;connected&quot;: False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_send_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;일반 전체 메시지를 전송하는 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text: # 빈 메시지 전송 차단
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if text == &quot;exit&quot;: # 특정 명령어 입력 시 연결 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 프로토콜 형태(JSON 규격 dict)에 맞춰 일반 채팅 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;나: {text}&quot;) # 내가 보낸 메시지 화면에 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(f&quot;메시지 전송 실패: {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_whisper_clicked(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;선택한 상대방에게 귓속말을 전송하는 메서드 (예외 처리 기능 포함)&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;text = self.message_input.text().strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 예외 처리 1: 미연결 상태 검증
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;서버에 먼저 접속하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 예외 처리 2: 내용 빈칸 검증
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;귓속말 내용을 입력하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 예외 처리 3: 귓속말 대상 미선택 검증
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.selected_user is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;귓속말 상대를 먼저 선택하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 예외 처리 4: 본인에게 전송하는지 검증
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.selected_user == self.my_address:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(&quot;자기 자신에게는 귓속말을 보낼 수 없습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 프로토콜 형태에 맞춰 대상(target)을 지정하여 귓속말 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;whisper&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;target&quot;: self.selected_user,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내 화면에 귓속말 발신 내역 기록
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.chat_display.append(f&quot;[귓속말 보냄] {self.selected_user}: {text}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.message_input.clear()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_error(f&quot;귓속말 전송 실패: {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.disconnect_from_server()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_user_selected(self, item):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;유저 목록 리스트 위젯의 아이템을 클릭했을 때 선택 항목을 업데이트하는 슬롯&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user = item.text()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.selected_user_label.setText(f&quot;선택한 상대: {self.selected_user}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def update_user_list(self, users):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;서버가 동기화해준 전체 유저 리스트를 UI 목록에 갱신하는 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for user in users:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 내 주소는 목록에 노출하되 별도 표시를 해주거나 구분을 위해 유지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if user == self.my_address:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.addItem(f&quot;{user} (나)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.user_list.addItem(user)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def disconnect_from_server(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;소켓을 안전하게 닫고 상태를 미연결로 되돌리는 종료 메서드&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.client_socket is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(self.client_socket, {&quot;type&quot;: &quot;exit&quot;}) # 서버에 종료 알림 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.running = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket.close() # 소켓 해제
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.client_socket = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.set_connected_state(False)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.show_system(&quot;서버와의 연결을 종료했습니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Slot(dict)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def display_message(self, message):&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;30강 소스코드&lt;/b&gt;&lt;br /&gt;&lt;b&gt;chat_server / server.py&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 30강. 귓속말 오류 처리와 표시 방식
# server.py

import socket
import threading

from protocol import receive_messages, send_message

# 서버 접속 정보 설정 (로컬 호스트 및 포트 5000)
HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 접속한 클라이언트 정보(딕셔너리)들을 저장할 리스트
clients = []


# 현재 접속 중인 클라이언트 수를 콘솔에 출력하는 함수
def print_clients():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;현재 접속자 수: {len(clients)}&quot;)


# 현재 접속자들의 고유 주소(IP, Port 문자열) 목록을 반환하는 함수
def get_user_list():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return [client[&quot;address&quot;] for client in clients]


# 모든 클라이언트에게 최신 접속자 명단을 전송하는 함수
def broadcast_user_list():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;user_list_message = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;user_list&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;users&quot;: get_user_list()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 리스트 복사본([:])을 사용하여 안전하게 순회하며 명단 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client[&quot;socket&quot;], user_list_message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass&amp;nbsp;&amp;nbsp;# 전송 실패 시 무시 (연결 끊김은 다른 곳에서 처리)


# 메시지를 보낸 사람을 제외한 모든 접속자에게 메시지를 브로드캐스트하는 함수
def broadcast(message, sender_socket=None):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket = client[&quot;socket&quot;]

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 본인이 아닌 경우에만 메시지 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client_socket != sender_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, message)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass


# 특정 클라이언트에게 에러 메시지를 형식을 갖추어 전송하는 함수
def send_error(client_socket, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(client_socket, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})


# 접속을 종료한 클라이언트를 목록에서 제거하고 해당 주소를 반환하는 함수
def remove_client(client_socket):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients[:]:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client[&quot;socket&quot;] == client_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.remove(client)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return client[&quot;address&quot;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None


# 주소(address) 또는 닉네임(nickname)으로 특정 클라이언트를 검색하는 함수
def find_client_by_identifier(identifier):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for client in clients:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client[&quot;address&quot;] == identifier:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return client
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if client.get(&quot;nickname&quot;) == identifier:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return client
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None


# 지정한 대상(타겟)에게 일대일 귓속말 메시지를 전송하는 함수
def send_whisper(sender_identifier, target_identifier, content):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target_client = find_client_by_identifier(target_identifier)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 대상이 접속 리스트에 없는 경우 실패 반환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if target_client is None:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 대상 클라이언트 소켓으로 귓속말 패킷 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(target_client[&quot;socket&quot;], {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;whisper&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sender&quot;: sender_identifier,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False


# 개별 클라이언트와의 통신을 독립적으로 처리하는 스레드 함수
def handle_client(client_socket, client_address):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 시작: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer = &quot;&quot;&amp;nbsp;&amp;nbsp;# TCP 데이터 스트림을 메시지 단위로 조립하기 위한 버퍼
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 클라이언트가 입장했음을 다른 모든 사용자에게 알림
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 입장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 새로운 유저가 들어왔으므로 유저 목록 갱신 및 전송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메시지 수신 대기 루프
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 프로토콜 모듈을 통해 버퍼와 메시지 패킷 파싱
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffer, messages, connected = receive_messages(client_socket, buffer)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 클라이언트가 소켓 연결을 정상/비정상 종료한 경우 루프 탈출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not connected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 연결 끊김: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 파싱된 메시지들을 순차적으로 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for message in messages:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{client_address} 메시지: {message}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;message_type = message.get(&quot;type&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 종료(exit) 메시지 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;exit&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 종료 요청: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit_requested = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 일반 채팅(chat) 메시지 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;chat&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;).strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not content:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;빈 메시지는 보낼 수 없습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 보낸 사람 제외 전체 사용자에게 채팅 전달
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;chat&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sender&quot;: address_text,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: content
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 귓속말(whisper) 메시지 처리 및 오류 예외 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if message_type == &quot;whisper&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target = message.get(&quot;target&quot;, &quot;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content = message.get(&quot;content&quot;, &quot;&quot;).strip()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 검증 1: 귓속말 대상 지정 여부 확인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not target:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;귓속말 대상을 선택하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 검증 2: 귓속말 내용 입력 여부 확인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not content:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;귓속말 내용을 입력하세요.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 검증 3: 본인에게 보내는지 확인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if target == address_text:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;자기 자신에게는 귓속말을 보낼 수 없습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 귓속말 전송 시도
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;success = send_whisper(address_text, target, content)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 전송 실패 시 (대상이 방을 나간 경우 등) 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not success:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, &quot;선택한 사용자가 더 이상 접속 중이 아닙니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()&amp;nbsp;&amp;nbsp;# 동기화를 위해 목록 재갱신
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 알 수 없는 메시지 타입 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_error(client_socket, f&quot;지원하지 않는 메시지 타입입니다: {message_type}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if exit_requested:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 연결 끊김, 루프 종료, 에러 발생 시 최종 자원 정리 구간
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket.close()&amp;nbsp;&amp;nbsp;# 클라이언트 소켓 닫기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 전역 접속자 리스트에서 제거
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;removed_address = remove_client(client_socket)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 다른 사용자들에게 퇴장 알림 공지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;system&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;{address_text} 님이 퇴장했습니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}, client_socket)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 유저 목록 리프레시 공지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;broadcast_user_list()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에서 제거: {removed_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 처리 종료: {client_address}&quot;)


# 메인 서버 소켓 생성 및 설정
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 소켓 재사용 옵션 설정 (서버 재시작 시 포트 바인딩 에러 방지)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.bind((HOST, PORT))&amp;nbsp;&amp;nbsp;# 지정한 주소와 포트에 소켓 결합
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;server_socket.listen()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 클라이언트 연결 요청 대기 상태 진입

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;서버를 시작합니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;클라이언트 접속 대기 중...&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 무한 루프를 돌며 다중 클라이언트 접속을 수락
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 새로운 클라이언트 접속 승인
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_socket, client_address = server_socket.accept()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;address_text = str(client_address)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;클라이언트 접속: {client_address}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 접속자 관리 리스트에 소켓 및 주소 정보 등록
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clients.append({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;socket&quot;: client_socket,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;address&quot;: address_text
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;접속자 목록에 추가: {client_address}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print_clients()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 멀티스레딩을 통해 각 클라이언트 전용 handle_client 함수 실행
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread = threading.Thread(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;target=handle_client,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;args=(client_socket, client_address)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.daemon = True&amp;nbsp;&amp;nbsp;# 메인 스레드가 종료되면 서브 스레드도 함께 종료되도록 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;client_thread.start()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # 스레드 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;31강. 텍스트와 바이너리 데이터의 차이&lt;/b&gt;&lt;/h4&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAIIAAgDEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-subtree=&quot;aimfl&quot;&gt;가. 텍스트 데이터와 바이너리 데이터의 핵심 차이는 &lt;/span&gt;&lt;b&gt;사람이 직접 읽을 수 있는가와 컴퓨터가 효율적으로 처리하는가&lt;/b&gt;의 여부&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgFEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;나. 모든 데이터는 컴퓨터 내부에서 0과 1의 이진수(바이너리) 형태로 저장되지만, 이를 &lt;b&gt;해석하고 표현하는 방식&lt;/b&gt;에서 차이가 발생&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;텍스트 데이터 (Text Data) &lt;b&gt;&quot;사람이 이해할 수 있는 문자 코드로 이루어진 데이터&quot;&amp;nbsp;&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가독성&lt;/b&gt;: 메모장 같은 편집기로 열었을 때 사람이 바로 읽고 이해 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인코딩&lt;/b&gt;: 문자를 표현하기 위해 ASCII, UTF-8, CP949 같은 문자 인코딩 규격을 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 구조&lt;/b&gt;: 줄바꿈 문자(\n, \r\n)와 공백을 기준으로 데이터의 경계를 구분&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대표 예시&lt;/b&gt;: TXT, HTML, JSON, XML, 소스 코드 파일(.py, .java 등)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;다. 바이너리 데이터 (Binary Data) :&amp;nbsp;&lt;/b&gt;컴퓨터가 직접 읽고 실행하거나 특정 프로그램이 해석해야 하는 데이터&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가독성&lt;/b&gt;: 메모장으로 열면 깨진 문자나 알 수 없는 기호()&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율성&lt;/b&gt;: 문자에 국한되지 않고 이미지, 소리, 실행 명령 등을 0과 1로 압축하여 저장하므로 공간 효율성이 높다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 구조&lt;/b&gt;: 고정된 크기(바이트)의 헤더 정보나 파일 포맷 구조에 따라 데이터를 해석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대표 예시&lt;/b&gt;: 이미지(PNG, JPG), 오디오/비디오(MP3, MP4), 실행 파일(EXE), 압축 파일(ZIP) 및 일반적인 BIN 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;32강. 파일 전송 개념과 메타데이터 설계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가. 파일 전송(File Transfer)&lt;/b&gt;과 &lt;b&gt;메타데이터 설계(Metadata Design)&lt;/b&gt;는 시스템 간 데이터를 안전하고 정확하게 주고받기 위한 개념. 데이터 유실 없이 전송되려면 파일 본문뿐만 아니라, 그 파일을 설명하는 구조화된 데이터(메타데이터)의 명확한 정의가 필수적&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;나. 파일 전송 (File Transfer) 핵심 개념 및 용어&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp; &amp;nbsp;파일을 한 시스템(송신측)에서 다른 시스템(수신측)으로 이동하는 과정에서 사용되는 기술적 용어&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;페이로드 (Payload) &lt;/b&gt;: 전송되는 데이터의 &lt;b&gt;실제 본문 내용&lt;/b&gt;을 의미. 헤더나 메타데이터를 제외한 순수 파일 데이터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;프로토콜 (Protocol) : 컴퓨터 간에 파일을 안전하게 주고받기 위해 약속한 통신 규약&amp;nbsp;(예: FTP, SFTP, HTTP/HTTPS)&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;체크섬 (Checksum) / 해시 (Hash)&lt;/b&gt;: 전송된 파일이 원래 파일과 완전히 일치하는지(무결성) 검증하기 위해 산출하는 &lt;b&gt;고유한 문자열 값&lt;/b&gt;&amp;nbsp;(예: MD5, SHA-256)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;청크 (Chunk) / 블록 (Block)&lt;/b&gt;: 대용량 파일을 한 번에 보내지 않고 네트워크 과부하를 막기 위해 &lt;b&gt;쪼갠 작은 데이터 단위&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이어받기 (Resume / Partial Content)&lt;/b&gt;: 전송 중 네트워크가 끊겼을 때 처음부터 다시 받지 않고, &lt;b&gt;중단된 지점부터 이어서 전송&lt;/b&gt;하는 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;다. 메타데이터 설계 (Metadata Design) 용어 정의&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIExAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;메타데이터란 &lt;b&gt;&quot;데이터를 설명하는 데이터&quot;&lt;/b&gt;이며, 이를 시스템에 맞게 표준화하고 구조를 짜는 것을 메타데이터 설계&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 700; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;※ 메타데이터의 3대 분류&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기술(설명) 메타데이터 (Descriptive Metadata)&lt;/b&gt;: 콘텐츠를 검색하고 식별하기 위한 정보 (예: 파일 이름, 작성자, 키워드)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조 메타데이터 (Structural Metadata)&lt;/b&gt;: 복합적인 파일들이 어떻게 결합해 있는지 관계를 나타냄 (예: 압축파일 내부 폴더 구조, 비디오와 자막 파일의 연결 관계)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관리 메타데이터 (Administrative Metadata)&lt;/b&gt;: 파일의 관리, 권한, 보존을 위한 정보 (예: 생성 일시, 파일 크기, 접근 권한, DRM 보안 정보)&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 700; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;※ 설계 단계별 핵심 용어&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 사전 (Data Dictionary)&lt;/b&gt;: 메타데이터 설계 시 사용되는 모든 데이터 요소(필드)의 &lt;b&gt;이름, 의미, 타입, 제약조건을 기록해 둔 명세서&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스키마 (Schema)&lt;/b&gt;: 메타데이터가 어떤 구조와 관계를 가지고 저장되는지 정의한 &lt;b&gt;전체적인 데이터 구조의 틀&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 표준화 (Data Standardization)&lt;/b&gt;: 시스템 간 데이터 오해를 없애기 위해 용어의 단어, 형식, 코드 구성을 &lt;b&gt;일관되게 규칙화하는 작업&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네임스페이스 (Namespace)&lt;/b&gt;: 메타데이터 요소의 이름이 서로 충돌하지 않도록 도메인별로 범위를 구분해 놓은 &lt;b&gt;이름 공간&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;라. 파일 전송용 메타데이터 설계 예시 (JSON 포맷)&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIJhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;실제 개발 환경에서 파일 전송 기능을 구현할 때, 파일 본문(바이너리)과 함께 전송하는 &lt;b&gt;메타데이터 스키마 설계&lt;/b&gt;의 직관적인 예시&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIJxAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;m8iB2c_6n,m8iB2c_6m&quot; data-sfc-root=&quot;ep&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.5px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;예시 (json)&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;pre class=&quot;json&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;code&gt;{
  &quot;file_id&quot;: &quot;FILE_20260625_001&quot;,     // 파일 고유 식별자 (ID)
  &quot;file_name&quot;: &quot;report_v2.pdf&quot;,       // 파일 이름
  &quot;file_size_bytes&quot;: 1048576,         // 파일 크기 (바이트 단위)
  &quot;mime_type&quot;: &quot;application/pdf&quot;,     // 파일 형식 (바이너리/텍스트 종류 구분)
  &quot;checksum_sha256&quot;: &quot;a1b2c3d4...&quot;,   // 무결성 검증용 해시값
  &quot;created_at&quot;: &quot;2026-06-25T10:11:00Z&quot;// 생성 일시 (표준 시각 포맷)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;33강.&amp;nbsp; 파일을 조각으로 나누어 보내기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 1. 테스트 스크립트로 전송 확인 하기&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;main.py에 파일 버튼 미연결 상태하 임시 테스트 파일로 확인해 본다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3EUPV/dJMcagF3jiL/AoIADtRnFuv5XVFBsXkMNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3EUPV/dJMcagF3jiL/AoIADtRnFuv5XVFBsXkMNK/img.png&quot; data-alt=&quot;샘플파일 png를 전송하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3EUPV/dJMcagF3jiL/AoIADtRnFuv5XVFBsXkMNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3EUPV%2FdJMcagF3jiL%2FAoIADtRnFuv5XVFBsXkMNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;775&quot; height=&quot;412&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;샘플파일 png를 전송하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;33강 테스트 소스코드&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# test_send_file.py (임시 테스트 파일 &amp;mdash; 확인 후 삭제 가능)
import socket
import sys
sys.path.insert(0, &quot;chat_client&quot;)
 
from protocol import send_file
 
HOST = &quot;127.0.0.1&quot;
PORT = 5000
 
test_path = sys.argv[1] if len(sys.argv) &amp;gt; 1 else &quot;sample.png&quot;
 
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sock.connect((HOST, PORT))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;success = send_file(sock, test_path, &quot;tester&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if success:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[파일 전송 완료] {test_path}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[파일 전송 실패] {test_path}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;~ $&amp;nbsp; &amp;nbsp;터미널 1 &amp;gt;&amp;gt;&amp;nbsp; python chat_server/server.py&lt;br /&gt;~ $&amp;nbsp; &amp;nbsp;터미널 2 &amp;gt;&amp;gt;&amp;nbsp; python test_send_file.py sample.png&lt;br /&gt;※ 서버에 file_info, file_chunk, file_end 처리 코드가 없어서 오류가 발생할 수 있고, 34강에서 서버와 클라이언트의 수신 처리 코드를 추가해서 실습해 본다.&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;33강 소스코드&amp;nbsp;&lt;br /&gt;chat_client / protocol.py&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import json
import base64
import os
import time
 
CHUNK_SIZE = 65536
 
def send_message(sock, message):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data = json.dumps(message).encode(&quot;utf-8&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;length = len(data)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sock.sendall(length.to_bytes(4, &quot;big&quot;) + data)
 
def recv_message(sock):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;raw_length = sock.recv(4)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not raw_length:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;length = int.from_bytes(raw_length, &quot;big&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data = b&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while len(data) &amp;lt; length:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;packet = sock.recv(length - len(data))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not packet:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data += packet
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return json.loads(data.decode(&quot;utf-8&quot;))
 
def send_file(sock, file_path, sender):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filename = os.path.basename(file_path)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;size = os.path.getsize(file_path)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;file_id = f&quot;{filename}_{int(time.time() * 1000)}&quot;
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(sock, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;file_info&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;file_id&quot;: file_id,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filename&quot;: filename,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;size&quot;: size,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sender&quot;: sender
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with open(file_path, &quot;rb&quot;) as file:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while True:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chunk = file.read(CHUNK_SIZE)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not chunk:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encoded = base64.b64encode(chunk).decode(&quot;utf-8&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(sock, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;file_chunk&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;file_id&quot;: file_id,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;data&quot;: encoded
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as error:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(sock, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;error&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;: f&quot;파일 전송 실패: {error}&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;send_message(sock, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;: &quot;file_end&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;file_id&quot;: file_id
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/74</guid>
      <comments>https://goodjop79.tistory.com/74#entry74comment</comments>
      <pubDate>Wed, 24 Jun 2026 17:03:20 +0900</pubDate>
    </item>
    <item>
      <title>D+53[Python] PySide6 GUI 연동하기(22~26강)</title>
      <link>https://goodjop79.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;일&amp;nbsp;자&amp;nbsp;:&amp;nbsp;2026년&amp;nbsp;6월&amp;nbsp;23일(화)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ&amp;nbsp;주&amp;nbsp;제&amp;nbsp;:&amp;nbsp;GUI&amp;nbsp;연동하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22강 채팅 화면 레이아웃 만들기&lt;br /&gt;23강&amp;nbsp;GUI에서&amp;nbsp;서버&amp;nbsp;접속하기&lt;br /&gt;24강&amp;nbsp;GUI에서&amp;nbsp;메세지&amp;nbsp;보내기&lt;br /&gt;25강&amp;nbsp;GUI에서&amp;nbsp;실시간&amp;nbsp;메세지&amp;nbsp;받기&lt;br /&gt;26강&amp;nbsp;GUI&amp;nbsp;상태&amp;nbsp;관리와&amp;nbsp;연결&amp;nbsp;종료&amp;nbsp;처리&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;27강&amp;nbsp;사용자&amp;nbsp;목록&amp;nbsp;동기화&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;28강&amp;nbsp;상대&amp;nbsp;선택&amp;nbsp;UI&amp;nbsp;만들기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;※ 아래 주소는 가상의 실습형 주소임&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;22강. 채팅 화면 레이아웃 만들기&lt;/b&gt;&lt;br /&gt;self.send_button.clicked.connect&amp;nbsp;(self.on_send_clicked)&amp;nbsp;버튼&amp;nbsp;클릭&lt;br /&gt;self.message_input.retrunPressed.connect(sefl.on_send_clicked)&amp;nbsp;엔터&amp;nbsp;입력&lt;br /&gt;def.......&amp;nbsp;(버튼과&amp;nbsp;앤터를&amp;nbsp;입력&amp;nbsp;받은&amp;nbsp;것을&amp;nbsp;실행한다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;22 강 채팅스크린샷 2026-06-23 11-57-25.png&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZPBgd/dJMcadI9Uhn/XcdWwv4XLD6LxBtAvH54V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZPBgd/dJMcadI9Uhn/XcdWwv4XLD6LxBtAvH54V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZPBgd/dJMcadI9Uhn/XcdWwv4XLD6LxBtAvH54V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZPBgd%2FdJMcadI9Uhn%2FXcdWwv4XLD6LxBtAvH54V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1139&quot; height=&quot;940&quot; data-filename=&quot;22 강 채팅스크린샷 2026-06-23 11-57-25.png&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;23강. GUI에서 서버 접속하기&lt;/b&gt;&lt;br /&gt;서버와&amp;nbsp;연결하기,&amp;nbsp;크라이언트가&amp;nbsp;서버에&amp;nbsp;연결을&amp;nbsp;요청한다.&amp;nbsp;&lt;br /&gt;사용자가&amp;nbsp;잘&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;만들어&amp;nbsp;주는&amp;nbsp;것,&amp;nbsp;사용자&amp;nbsp;입장에서&amp;nbsp;봐야&amp;nbsp;한다.&lt;br /&gt;프로젝트&amp;nbsp;단계별&amp;nbsp;나누어서&amp;nbsp;구현하기&amp;nbsp;(각&amp;nbsp;담당자별)&amp;nbsp;:&amp;nbsp;예)&amp;nbsp;테스트-설계-구조&lt;br /&gt;cs팀,&amp;nbsp;ai팀,&amp;nbsp;QT팀&amp;nbsp;:&amp;nbsp;분석과&amp;nbsp;설계가&amp;nbsp;되어서&amp;nbsp;팀별&amp;nbsp;개인임무&amp;nbsp;분담하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;23 강재접속스크린샷 2026-06-23 15-04-31.png&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/myKEv/dJMcaa6SmQ4/AMNeSmDPzuagvooVHQkVoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/myKEv/dJMcaa6SmQ4/AMNeSmDPzuagvooVHQkVoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/myKEv/dJMcaa6SmQ4/AMNeSmDPzuagvooVHQkVoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmyKEv%2FdJMcaa6SmQ4%2FAMNeSmDPzuagvooVHQkVoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;866&quot; height=&quot;822&quot; data-filename=&quot;23 강재접속스크린샷 2026-06-23 15-04-31.png&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;23강 채팅 테스트스크린샷 2026-06-23 12-43-33.png&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;947&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3hh1t/dJMcaicMCCf/lva1TB0sx8GiGLhMRUMCQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3hh1t/dJMcaicMCCf/lva1TB0sx8GiGLhMRUMCQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3hh1t/dJMcaicMCCf/lva1TB0sx8GiGLhMRUMCQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3hh1t%2FdJMcaicMCCf%2Flva1TB0sx8GiGLhMRUMCQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1162&quot; height=&quot;947&quot; data-filename=&quot;23강 채팅 테스트스크린샷 2026-06-23 12-43-33.png&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;947&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLtkJj/dJMcacX6zNL/Jn9KWjxTaNiPjDI7RbhfjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLtkJj/dJMcacX6zNL/Jn9KWjxTaNiPjDI7RbhfjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLtkJj/dJMcacX6zNL/Jn9KWjxTaNiPjDI7RbhfjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLtkJj%2FdJMcacX6zNL%2FJn9KWjxTaNiPjDI7RbhfjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;579&quot; height=&quot;899&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;899&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782206368994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;23강 코드

# GUI 연결하기 실습 (23강)

import socket  # 소켓 모듈 불러오기
import sys

import self
from PySide6.QtWidgets import (
    QApplication,
    QWidget,
    QTextEdit,  # 채팅 표시 영역
    QLineEdit,  # 입력창
    QPushButton,  # 보내기 버튼
    QLabel,  # 서버연결 상태 표시
    QVBoxLayout,  # 가로 레이아웃
    QHBoxLayout,  # 세로 레이아웃
)

# from protocol import send_message    ### 17강에서 만들었던 전송 함수 가져와서 24강에서 사용한다

HOST = &quot;127.0.0.1&quot;  ### 서버 IP (server.py와 주소가 같아야 한다.)
PORT = 5000  #### 서버 포트 (server.py와 포트가 같아야 한다.)


class ChatWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.client_socket = None  ### 연결하기 전이므로 None를 작성하여 시작하기로 한다.

        self.setWindowTitle(&quot;GUI채팅창&quot;)
        self.resize(560, 480)

        self.chat_display = QTextEdit()
        self.chat_display.setReadOnly(True)
        self.chat_display.setPlaceholderText(&quot;채팅 메시지가 여기에 표시됩니다.&quot;)  # 빈 상태 안내문

        self.message_input = QLineEdit()
        self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)
        self.message_input.setEnabled(False)  ### 채팅 서버 연결전에는 입력을 통제한다.

        self.send_button = QPushButton(&quot;보내기&quot;)
        self.send_button.setEnabled(False)  ### 채팅 서버 연결전에는 보내기를 통제한다.

        self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)
        self.connect_button = QPushButton(&quot;서버 접속&quot;)  ### 서버 접속 버튼을 추가함

        top_layout = QHBoxLayout()
        top_layout.addWidget(self.status_label)
        top_layout.addWidget(self.connect_button)

        input_layout = QHBoxLayout()
        input_layout.addWidget(self.message_input)
        input_layout.addWidget(self.send_button)

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)  ### status 라벨을 top 라벨로 변경해 준다.
        main_layout.addWidget(self.chat_display)
        main_layout.addLayout(input_layout)

        self.setLayout(main_layout)

        self.connect_button.clicked.connect(self.connect_to_server)  ### connect 서버 추가해 준다
        self.send_button.clicked.connect(self.on_send_clicked)  # 버튼 클릭 &quot;이벤트&quot; 연결 on_send_clicked
        self.message_input.returnPressed.connect(self.on_send_clicked)  # 앤터 입력 &quot;이벤트&quot; 연결 on_send_clicked

    ## === 클라이언트 - 서버 접속 기능을 연결하기 위해 connect_to_server() 메서드를 추가해 준다.
    def connect_to_server(self):  # 접속 버튼이 호출하는 메서드
        try:
            self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client_socket.connect((HOST, PORT))

            self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)  # 상태 라벨 변경
            self.chat_display.append(&quot;[시스템] 서버에 연결되었습니다.&quot;)  # 시스템 메시지 표시
            self.connect_button.setEnabled(False)  # 중복 접속 방지
            self.message_input.setEnabled(True)  # 입력창 활성화
            self.send_button.setEnabled(True)  # 보내기 버튼 활성화

        except ConnectionRefusedError:  # 서버가 꺼져 있을 때
            self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
            self.chat_display.append(&quot;[오류] 서버가 실행 중인지 확인하세요.&quot;)

        except Exception as error:  # 그 외 예상치 못한 오류
            self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
            self.chat_display.append(f&quot;[오류] 연결 중 문제가 발생했습니다: {error}&quot;)

    def on_send_clicked(self):  # 보내기 처리 함수
        text = self.message_input.text().strip()  # 앞뒤 공백 제거후 입력값을 가져온다
        if not text:  # 빈 메세지는 보내지 않도록 한다.
            return

        self.chat_display.append(f&quot;나: {text}&quot;) # 임시네트워크 전송으로 교체하여 보내는 이가 &quot;나&quot;를 지칭해 출력함
        self.message_input.clear()


app = QApplication(sys.argv)

window = ChatWindow()
window.show()

sys.exit(app.exec())  # 윈도우 창이 바로 꺼지지 않게 계속 유지되도록 하는 역할을 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;24강. GUI에서 메세지 보내기&lt;/b&gt;&lt;br /&gt;메세지&amp;nbsp;전송할&amp;nbsp;때&amp;nbsp;프로토콜에서&amp;nbsp;전송한다.&amp;nbsp;from&amp;nbsp;protocol&amp;nbsp;import&amp;nbsp;send_message&lt;br /&gt;threads&amp;nbsp;:&amp;nbsp;메세지가&amp;nbsp;전송텀이&amp;nbsp;있어서&amp;nbsp;입력&amp;nbsp;될&amp;nbsp;때까지&amp;nbsp;멈추기&amp;nbsp;때문에&amp;nbsp;스레드를&amp;nbsp;사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 주요 내용 실습&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;22 강 채팅스크린샷 2026-06-23 11-57-25.png&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0nyVb/dJMcacKostY/DZT9AEq1rrw8eWJBkLr3N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0nyVb/dJMcacKostY/DZT9AEq1rrw8eWJBkLr3N0/img.png&quot; data-alt=&quot;채팅화면 레이아웃 만들기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0nyVb/dJMcacKostY/DZT9AEq1rrw8eWJBkLr3N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0nyVb%2FdJMcacKostY%2FDZT9AEq1rrw8eWJBkLr3N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1139&quot; height=&quot;940&quot; data-filename=&quot;22 강 채팅스크린샷 2026-06-23 11-57-25.png&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅화면 레이아웃 만들기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;23강 채팅 테스트스크린샷 2026-06-23 12-43-33.png&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;947&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PL6Lg/dJMcahdPLYj/Bt5T5Y7x3XFoSwiWlnRWa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PL6Lg/dJMcahdPLYj/Bt5T5Y7x3XFoSwiWlnRWa1/img.png&quot; data-alt=&quot;GUI에서 서버 접속하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PL6Lg/dJMcahdPLYj/Bt5T5Y7x3XFoSwiWlnRWa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPL6Lg%2FdJMcahdPLYj%2FBt5T5Y7x3XFoSwiWlnRWa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1162&quot; height=&quot;947&quot; data-filename=&quot;23강 채팅 테스트스크린샷 2026-06-23 12-43-33.png&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;947&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GUI에서 서버 접속하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;25강. GUI에서 실시간 메세지 받기&lt;/b&gt;&lt;br /&gt;시그널&amp;nbsp;만들기&amp;nbsp;UI에&amp;nbsp;스레드를&amp;nbsp;사용하기&amp;nbsp;&lt;br /&gt;수신&amp;nbsp;스레드가&amp;nbsp;필요한&amp;nbsp;이유?&amp;nbsp;&lt;br /&gt;recv()가&amp;nbsp;동작할&amp;nbsp;때&amp;nbsp;별도의&amp;nbsp;수신&amp;nbsp;스레드를&amp;nbsp;작성해서&amp;nbsp;실행을&amp;nbsp;한다.&amp;nbsp;즉&amp;nbsp;2개의&amp;nbsp;실행을&amp;nbsp;하므로&amp;nbsp;별도의&amp;nbsp;동작을&amp;nbsp;실행한다.&amp;nbsp;&lt;br /&gt;시그널과&amp;nbsp;슬롯을&amp;nbsp;연결하는&amp;nbsp;것?&amp;nbsp;&lt;br /&gt;메세지를&amp;nbsp;받으면&amp;nbsp;받은&amp;nbsp;메세지를&amp;nbsp;UI에&amp;nbsp;보여지게&amp;nbsp;한다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;25강스크린샷 2026-06-23 17-20-41.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;997&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T58Hr/dJMcabLvKld/z8LKDx7b3ZHxNvuSxomcRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T58Hr/dJMcabLvKld/z8LKDx7b3ZHxNvuSxomcRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T58Hr/dJMcabLvKld/z8LKDx7b3ZHxNvuSxomcRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT58Hr%2FdJMcabLvKld%2Fz8LKDx7b3ZHxNvuSxomcRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1134&quot; height=&quot;997&quot; data-filename=&quot;25강스크린샷 2026-06-23 17-20-41.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;997&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;26강. GUI 상태 관리와 연결 종료 처리&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;26강 exit.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;913&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czphDN/dJMcaijusS6/HwMOx7eAU0x5YFKFrt9VSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czphDN/dJMcaijusS6/HwMOx7eAU0x5YFKFrt9VSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czphDN/dJMcaijusS6/HwMOx7eAU0x5YFKFrt9VSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczphDN%2FdJMcaijusS6%2FHwMOx7eAU0x5YFKFrt9VSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1028&quot; height=&quot;913&quot; data-filename=&quot;26강 exit.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;913&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;lt;&amp;lt;&amp;nbsp; 26강 실습과제 풀이&amp;gt;&amp;gt;&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;과제A,B. 서버에 회원 처리 추가하기 / 로그인 창 만들기&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-30-41.png&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; data-alt=&quot;로그인 화면 추가하기 실행 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmcc4s%2FdJMcaiX1gOU%2FLgFpye6eJqDWon0K2FJB71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;755&quot; data-filename=&quot;스크린샷 2026-06-23 20-30-41.png&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인 화면 추가하기 실행 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;로그인 소스코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782215682697&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 3. 로그인 페이지
class LoginPage(QWidget):
    # 로그인 성공 시 실행할 시그널 (아이디 전달)
    login_success = Signal(str)

    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;로그인&quot;)
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;아이디&quot;)
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.login_btn = QPushButton(&quot;로그인&quot;)
        self.register_btn = QPushButton(&quot;회원가입&quot;)  # 회원가입 이동 버튼 추가

        layout.addWidget(QLabel(&quot;채팅 프로그램 로그인&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.login_btn)
        layout.addWidget(self.register_btn)
        self.setLayout(layout)

        self.login_btn.clicked.connect(self.check_login)
        self.register_btn.clicked.connect(self.open_register_page)

        self.reg_page = None  # 회원가입 창 객체 보유용 변수

    def check_login(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        # 서버 인증 시뮬레이션
        if user_id in USER_DB and USER_DB[user_id] == password:
            self.login_success.emit(user_id)
            self.close()  # 로그인 창 닫기
        else:
            QMessageBox.warning(
                self, &quot;인증 실패&quot;, &quot;아이디 또는 비밀번호가 틀렸습니다.&quot;
            )

    def open_register_page(self):
        self.reg_page = RegisterPage()
        # 회원가입 성공 시 로그인 입력창에 가입한 아이디를 세팅해주면 편리합니다.
        self.reg_page.register_success.connect(
            lambda: self.id_input.setText(self.reg_page.id_input.text())
        )
        self.reg_page.show()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;과제C. 회원가입 처리하기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-47-05.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w2s19/dJMcaasfZ0X/c0v366mdFVfFMmqKfHZVCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w2s19/dJMcaasfZ0X/c0v366mdFVfFMmqKfHZVCK/img.png&quot; data-alt=&quot;가상의 회원가입 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w2s19/dJMcaasfZ0X/c0v366mdFVfFMmqKfHZVCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw2s19%2FdJMcaasfZ0X%2Fc0v366mdFVfFMmqKfHZVCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;885&quot; height=&quot;511&quot; data-filename=&quot;스크린샷 2026-06-23 20-47-05.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 회원가입 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-47-11.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doxYsa/dJMcaasfZ08/WUDZghHLigSqlEVVcl4250/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doxYsa/dJMcaasfZ08/WUDZghHLigSqlEVVcl4250/img.png&quot; data-alt=&quot;가상의 회원가입이 완료된 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doxYsa/dJMcaasfZ08/WUDZghHLigSqlEVVcl4250/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoxYsa%2FdJMcaasfZ08%2FWUDZghHLigSqlEVVcl4250%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;885&quot; height=&quot;511&quot; data-filename=&quot;스크린샷 2026-06-23 20-47-11.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 회원가입이 완료된 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;회원가입 소스코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782215627730&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 2. 회원가입 페이지 (추가됨)
class RegisterPage(QWidget):
    register_success = Signal()  # 회원가입 성공 시 발생할 시그널

    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;회원가입&quot;)
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;새로운 아이디&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;새로운 비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.register_btn = QPushButton(&quot;가입하기&quot;)
        self.cancel_btn = QPushButton(&quot;취소&quot;)

        layout.addWidget(QLabel(&quot;회원가입 정보를 입력하세요&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.register_btn)
        layout.addWidget(self.cancel_btn)
        self.setLayout(layout)

        self.register_btn.clicked.connect(self.check_register)
        self.cancel_btn.clicked.connect(self.close)

    def check_register(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        if not user_id or not password:
            QMessageBox.warning(
                self, &quot;오류&quot;, &quot;아이디와 비밀번호를 모두 입력해주세요.&quot;
            )
            return

        if user_id in USER_DB:
            QMessageBox.warning(self, &quot;오류&quot;, &quot;이미 존재하는 아이디입니다.&quot;)
        else:
            USER_DB[user_id] = password
            QMessageBox.information(
                self, &quot;성공&quot;, &quot;회원가입이 완료되었습니다!&quot;
            )
            self.register_success.emit()
            self.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;과제D.&amp;nbsp;진입점&amp;nbsp;바꾸기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;의미 :&amp;nbsp;&lt;br /&gt;&lt;/b&gt;기능 호출 방식의 변화 : 예를 들어 '플러스 버튼(+)'을 눌러야만 쓸 수 있던 기능을 이제 채팅 입력창에서 @나 특정 명령어를 통해 바로 호출할 수 있게 되는 등, 더 빠르고 편리하게 접근하도록 UI/UX가 개선됨을 뜻이다.&amp;nbsp;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;※ 실행해서 확인 하기 화면 &lt;span style=&quot;color: #8a3db6;&quot;&gt;☞ 실행해서 서버에서 메세지를 받아주는 기능 추가 수정 필요&lt;/span&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-30-41.png&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; data-alt=&quot;로그인 화면 추가하기 실행 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmcc4s/dJMcaiX1gOU/LgFpye6eJqDWon0K2FJB71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcmcc4s%2FdJMcaiX1gOU%2FLgFpye6eJqDWon0K2FJB71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;835&quot; data-filename=&quot;스크린샷 2026-06-23 20-30-41.png&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로그인 화면 추가하기 실행 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-33-13.png&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/davwLs/dJMcahLEaKR/SLnKa0XuVH0owwwRsaTtJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/davwLs/dJMcahLEaKR/SLnKa0XuVH0owwwRsaTtJK/img.png&quot; data-alt=&quot;채팅방 채팅 처리 화면 (서버에 넘기는 부분이 수정 필요함)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/davwLs/dJMcahLEaKR/SLnKa0XuVH0owwwRsaTtJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdavwLs%2FdJMcahLEaKR%2FSLnKa0XuVH0owwwRsaTtJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;838&quot; data-filename=&quot;스크린샷 2026-06-23 20-33-13.png&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅방 채팅 처리 화면 (서버에 넘기는 부분이 수정 필요함)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;☞ 실행해서 서버에서 메세지를 받아주는 기능 추가 수정후 화면&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;26강 추가스크린샷 2026-06-24 11-43-10.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deXzcY/dJMcahdRjJG/b39gWWNGfd5McSmpWxbhh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deXzcY/dJMcahdRjJG/b39gWWNGfd5McSmpWxbhh1/img.png&quot; data-alt=&quot;채팅을 사용자 1이 받아서 출력하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deXzcY/dJMcahdRjJG/b39gWWNGfd5McSmpWxbhh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeXzcY%2FdJMcahdRjJG%2Fb39gWWNGfd5McSmpWxbhh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;916&quot; data-filename=&quot;26강 추가스크린샷 2026-06-24 11-43-10.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅을 사용자 1이 받아서 출력하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;26 강추가2.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqIhMM/dJMcahdRjJS/HVca6lJAhB1lvGHirac5rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqIhMM/dJMcahdRjJS/HVca6lJAhB1lvGHirac5rK/img.png&quot; data-alt=&quot;사용자가 추가된 채팅 서버 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqIhMM/dJMcahdRjJS/HVca6lJAhB1lvGHirac5rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqIhMM%2FdJMcahdRjJS%2FHVca6lJAhB1lvGHirac5rK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;579&quot; data-filename=&quot;26 강추가2.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자가 추가된 채팅 서버 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;26강 추가3.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VqCUy/dJMcacwQaap/sTGKifg02eHvpf3pN2xuK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VqCUy/dJMcacwQaap/sTGKifg02eHvpf3pN2xuK0/img.png&quot; data-alt=&quot;다수의 사용자들과 채팅을 시행한 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VqCUy/dJMcacwQaap/sTGKifg02eHvpf3pN2xuK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVqCUy%2FdJMcacwQaap%2FsTGKifg02eHvpf3pN2xuK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;579&quot; data-filename=&quot;26강 추가3.png&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다수의 사용자들과 채팅을 시행한 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ 파일 구조 &lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-23 20-59-00.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buL6fn/dJMb997Uj0p/4ad6DVGOnNsQ32Fq9sPH40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buL6fn/dJMb997Uj0p/4ad6DVGOnNsQ32Fq9sPH40/img.png&quot; data-alt=&quot;채팅창 구현하기 파일 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buL6fn/dJMb997Uj0p/4ad6DVGOnNsQ32Fq9sPH40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuL6fn%2FdJMb997Uj0p%2F4ad6DVGOnNsQ32Fq9sPH40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;309&quot; height=&quot;380&quot; data-filename=&quot;스크린샷 2026-06-23 20-59-00.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;채팅창 구현하기 파일 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;chat_client / main.py (총괄 코드)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1782215766626&quot; class=&quot;ruby&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# main.py

import sys  # 시스템 관련 파라미터 및 프로그램 종료를 위한 라이브러리
import socket  # 네트워크 통신을 위한 소켓 라이브러리
import threading  # 백그라운드에서 메시지를 수신하기 위한 멀티쓰레딩 라이브러리

# PySide6에서 GUI 이벤트 처리 및 시그널-슬롯 구조를 사용하기 위한 모듈 임포트
from PySide6.QtCore import Signal, Slot, Qt
from PySide6.QtWidgets import (
    QApplication,  # QT 어플리케이션을 제어하는 메인 클래스
    QWidget,  # 기본 윈도우 창 레이아웃의 베이스 클래스
    QTextEdit,  # 멀티라인 텍스트 표시 및 편집 창 (채팅 기록 창)
    QLineEdit,  # 단일라인 텍스트 입력 창 (메시지 입력 창)
    QPushButton,  # 클릭 가능한 버튼 (접속, 전송 버튼)
    QLabel,  # 텍스트를 화면에 표시하는 라벨 (상태 표시용)
    QVBoxLayout,  # 위젯을 수직(세로)으로 배열하는 레이아웃
    QHBoxLayout,  # 위젯을 수평(가로)으로 배열하는 레이아웃
    QMessageBox,  # 경고 및 알림 메시지 창을 띄우기 위한 클래스 (추가됨)
)

# 사용자 정의 프로토콜 모듈에서 메시지 송수신 함수를 임포트
from protocol import receive_messages, send_message

# 접속할 서버의 IP 주소와 포트 번호 설정 (로컬 호스트)
HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 1. 메모리 기반 사용자 데이터 (서버 인증용 딕셔너리)
USER_DB = {&quot;user1&quot;: &quot;1234&quot;, &quot;user2&quot;: &quot;5678&quot;}


# 2. 회원가입 페이지 (추가됨)
class RegisterPage(QWidget):
    register_success = Signal()  # 회원가입 성공 시 발생할 시그널

    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;회원가입&quot;)
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;새로운 아이디&quot;)

        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;새로운 비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.register_btn = QPushButton(&quot;가입하기&quot;)
        self.cancel_btn = QPushButton(&quot;취소&quot;)

        layout.addWidget(QLabel(&quot;회원가입 정보를 입력하세요&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.register_btn)
        layout.addWidget(self.cancel_btn)
        self.setLayout(layout)

        self.register_btn.clicked.connect(self.check_register)
        self.cancel_btn.clicked.connect(self.close)

    def check_register(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        if not user_id or not password:
            QMessageBox.warning(
                self, &quot;오류&quot;, &quot;아이디와 비밀번호를 모두 입력해주세요.&quot;
            )
            return

        if user_id in USER_DB:
            QMessageBox.warning(self, &quot;오류&quot;, &quot;이미 존재하는 아이디입니다.&quot;)
        else:
            USER_DB[user_id] = password
            QMessageBox.information(
                self, &quot;성공&quot;, &quot;회원가입이 완료되었습니다!&quot;
            )
            self.register_success.emit()
            self.close()


# 3. 로그인 페이지
class LoginPage(QWidget):
    # 로그인 성공 시 실행할 시그널 (아이디 전달)
    login_success = Signal(str)

    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;로그인&quot;)
        self.resize(300, 200)

        layout = QVBoxLayout()

        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;아이디&quot;)
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.Password)

        self.login_btn = QPushButton(&quot;로그인&quot;)
        self.register_btn = QPushButton(&quot;회원가입&quot;)  # 회원가입 이동 버튼 추가

        layout.addWidget(QLabel(&quot;채팅 프로그램 로그인&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(self.pw_input)
        layout.addWidget(self.login_btn)
        layout.addWidget(self.register_btn)
        self.setLayout(layout)

        self.login_btn.clicked.connect(self.check_login)
        self.register_btn.clicked.connect(self.open_register_page)

        self.reg_page = None  # 회원가입 창 객체 보유용 변수

    def check_login(self):
        user_id = self.id_input.text().strip()
        password = self.pw_input.text().strip()

        # 서버 인증 시뮬레이션
        if user_id in USER_DB and USER_DB[user_id] == password:
            self.login_success.emit(user_id)
            self.close()  # 로그인 창 닫기
        else:
            QMessageBox.warning(
                self, &quot;인증 실패&quot;, &quot;아이디 또는 비밀번호가 틀렸습니다.&quot;
            )

    def open_register_page(self):
        self.reg_page = RegisterPage()
        # 회원가입 성공 시 로그인 입력창에 가입한 아이디를 세팅해주면 편리합니다.
        self.reg_page.register_success.connect(
            lambda: self.id_input.setText(self.reg_page.id_input.text())
        )
        self.reg_page.show()


# 4. 채팅창 페이지
class ChatWindow(QWidget):
    # 백그라운드 쓰레드에서 수신한 메시지 데이터를 메인 GUI 쓰레드로 안전하게 전달하기 위한 커스텀 시그널 정의
    message_received = Signal(dict)

    def __init__(self):
        super().__init__()  # 부모 클래스(QWidget)의 생성자 호출

        # 초기 변수 및 상태 설정
        self.user_id = &quot;&quot;  # 현재 로그인한 사용자의 아이디 저장 변수
        self.client_socket = None  # 서버와 연결할 소켓 객체 저장 변수
        self.receive_thread = None  # 메시지 수신을 담당할 백그라운드 쓰레드 변수
        self.running = False  # 수신 루프 실행 여부를 제어하는 플래그

        # GUI 창의 기본 속성 설정
        self.setWindowTitle(&quot;채팅방&quot;)  # 창 제목 설정
        self.resize(560, 480)  # 창 크기 설정 (가로 560, 세로 480)

        # 채팅 기록창 설정
        self.chat_display = QTextEdit()
        self.chat_display.setReadOnly(
            True
        )  # 사용자가 직접 수정하지 못하도록 읽기 전용 설정
        self.chat_display.setPlaceholderText(
            &quot;채팅 메시지가 여기에 표시됩니다.&quot;
        )

        # 메시지 입력창 설정
        self.message_input = QLineEdit()
        self.message_input.setPlaceholderText(&quot;메시지를 입력하세요&quot;)
        self.message_input.setEnabled(
            False
        )  # 서버 연결 전까지는 입력 불가능하게 설정

        # 메시지 전송 버튼 설정
        self.send_button = QPushButton(&quot;보내기&quot;)
        self.send_button.setEnabled(
            False
        )  # 서버 연결 전까지는 비활성화 설정

        # 상단 서버 접속 영역 설정
        self.status_label = QLabel(&quot;상태: 서버에 연결되지 않음&quot;)
        self.connect_button = QPushButton(&quot;서버 접속&quot;)

        # 상단 레이아웃 구성 (상태 라벨 | 접속 버튼)
        top_layout = QHBoxLayout()
        top_layout.addWidget(self.status_label)
        top_layout.addWidget(self.connect_button)

        # 하단 레이아웃 구성 (메시지 입력창 | 전송 버튼)
        input_layout = QHBoxLayout()
        input_layout.addWidget(self.message_input)
        input_layout.addWidget(self.send_button)

        # 메인 레이아웃 구성 (상단 -&amp;gt; 채팅창 -&amp;gt; 하단 순으로 세로 배치)
        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addWidget(self.chat_display)
        main_layout.addLayout(input_layout)

        self.setLayout(
            main_layout
        )  # 구성된 메인 레이아웃을 현재 위젯 창에 적용

        # 이벤트 시그널과 핸들러(슬롯) 함수들을 연결 (Connect)
        self.connect_button.clicked.connect(
            self.connect_to_server
        )  # 접속 버튼 클릭 시
        self.send_button.clicked.connect(self.on_send_clicked)  # 전송 버튼 클릭 시
        self.message_input.returnPressed.connect(
            self.on_send_clicked
        )  # 입력창에서 엔터키 입력 시
        self.message_received.connect(
            self.display_message
        )  # 새 메시지 시그널 수신 시 화면 표시 함수 호출

    def set_user(self, user_id):
        &quot;&quot;&quot;로그인 성공 후 유저 아이디를 세팅하고 타이틀을 변경하는 함수&quot;&quot;&quot;
        self.user_id = user_id
        self.setWindowTitle(f&quot;채팅방 - [{self.user_id}]님&quot;)

    def connect_to_server(self):
        &quot;&quot;&quot;서버에 TCP 소켓 연결을 시도하고 수신 쓰레드를 시작하는 함수&quot;&quot;&quot;
        try:
            # IPv4, TCP 프로토콜을 사용하는 소켓 생성 및 서버 연결 시도
            self.client_socket = socket.socket(
                socket.AF_INET, socket.SOCK_STREAM
            )
            self.client_socket.connect((HOST, PORT))

            # 연결 성공 시 UI 상태 업데이트
            self.status_label.setText(&quot;상태: 서버에 연결됨&quot;)
            self.chat_display.append(&quot;[시스템] 서버에 연결되었습니다.&quot;)

            # 버튼 및 입력창 활성화/비활성화 제어
            self.connect_button.setEnabled(
                False
            )  # 재접속 방지를 위해 비활성화
            self.message_input.setEnabled(True)  # 입력 허용
            self.send_button.setEnabled(True)  # 전송 허용

            # 백그라운드 수신 쓰레드 생성 및 시작
            self.running = True
            self.receive_thread = threading.Thread(target=self.receive_loop)
            self.receive_thread.daemon = (
                True  # 메인 프로그램 종료 시 쓰레드도 자동 종료 설정
            )
            self.receive_thread.start()

        except ConnectionRefusedError:
            self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
            self.chat_display.append(
                &quot;[오류] 서버가 실행 중인지 확인하세요.&quot;
            )

        except Exception as error:
            self.status_label.setText(&quot;상태: 서버 연결 실패&quot;)
            self.chat_display.append(
                f&quot;[오류] 연결 중 문제가 발생했습니다: {error}&quot;
            )

    def receive_loop(self):
        &quot;&quot;&quot;백그라운드에서 실행되며 서버로부터 데이터를 지속적으로 수신하는 루프 함수&quot;&quot;&quot;
        buffer = &quot;&quot;  # 데이터가 쪼개져 올 때를 대비한 임시 버퍼 공간

        while self.running:
            try:
                buffer, messages, connected = receive_messages(
                    self.client_socket, buffer
                )

                if not connected:
                    self.message_received.emit(
                        {
                            &quot;type&quot;: &quot;system&quot;,
                            &quot;content&quot;: &quot;서버와의 연결이 끊어졌습니다.&quot;,
                        }
                    )
                    break

                for message in messages:
                    self.message_received.emit(message)

            except Exception as error:
                self.message_received.emit(
                    {
                        &quot;type&quot;: &quot;error&quot;,
                        &quot;content&quot;: f&quot;메시지 수신 중 오류가 발생했습니다: {error}&quot;,
                    }
                )
                break

        self.running = False

    def on_send_clicked(self):
        &quot;&quot;&quot;사용자가 입력한 메시지를 서버로 전송하는 함수&quot;&quot;&quot;
        text = self.message_input.text().strip()

        if not text:
            return

        if self.client_socket is None:
            self.chat_display.append(&quot;[오류] 서버에 먼저 접속하세요.&quot;)
            return

        try:
            # 보낼 때 발신자 정보(sender)도 프로토콜에 실어 보냅니다.
            send_message(
                self.client_socket,
                {&quot;type&quot;: &quot;chat&quot;, &quot;sender&quot;: self.user_id, &quot;content&quot;: text},
            )

            self.chat_display.append(f&quot;나: {text}&quot;)
            self.message_input.clear()

        except Exception as error:
            self.chat_display.append(f&quot;[오류] 메시지 전송 실패: {error}&quot;)

    @Slot(dict)
    def display_message(self, message):
        &quot;&quot;&quot;수신 쓰레드가 보낸 시그널을 받아 화면에 분류 처리하는 슬롯 함수&quot;&quot;&quot;
        message_type = message.get(&quot;type&quot;)

        if message_type == &quot;chat&quot;:
            sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
            content = message.get(&quot;content&quot;, &quot;&quot;)
            # 내가 보낸 메시지가 상대방에게서 다시 브로드캐스트되어 오는 중복 출력을 방지
            if sender != self.user_id:
                self.chat_display.append(f&quot;{sender}: {content}&quot;)

        elif message_type == &quot;system&quot;:
            content = message.get(&quot;content&quot;, &quot;&quot;)
            self.chat_display.append(f&quot;[시스템] {content}&quot;)

        elif message_type == &quot;error&quot;:
            content = message.get(&quot;content&quot;, &quot;&quot;)
            self.chat_display.append(f&quot;[오류] {content}&quot;)

        else:
            self.chat_display.append(f&quot;[알 수 없는 메시지] {message}&quot;)


# 5. 흐름 제어 및 프로그램 실행 진입점
if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)

    login_page = LoginPage()
    chat_window = ChatWindow()

    # 로그인 성공 시그널 연결 로직
    def on_login_success(user_id):
        chat_window.set_user(user_id)  # 채팅창에 로그인된 유저 세팅
        chat_window.show()  # 채팅창 켜기
        chat_window.connect_to_server()  # 로그인 완료와 동시에 서버 접속 자동 실행

    login_page.login_success.connect(on_login_success)
    login_page.show()  # 처음에 로그인 화면 띄우기

    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;chat_server / server.py&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1782215766630&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# server.py

import socket
import threading
from protocol import receive_messages, send_message

HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 현재 연결된 클라이언트 소켓 목록
clients = []


def print_clients():
    print(f&quot;현재 접속자 수: {len(clients)}&quot;)


def broadcast(message, sender_socket):
    &quot;&quot;&quot;메시지를 보낸 클라이언트를 제외한 모든 접속자에게 메시지 전송&quot;&quot;&quot;
    for client in clients[:]:
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:
                if client in clients:
                    clients.remove(client)


def handle_client(client_socket, client_address):
    print(f&quot;클라이언트 처리 시작: {client_address}&quot;)
    buffer = &quot;&quot;

    with client_socket:
        while True:
            try:
                buffer, messages, connected = receive_messages(
                    client_socket, buffer
                )

                if not connected:
                    break

                for message in messages:
                    msg_type = message.get(&quot;type&quot;)

                    # 1. 채팅 처리
                    if msg_type == &quot;chat&quot;:
                        # main.py에서 이미 'sender'와 'content'를 실어서 보냅니다.
                        # 서버는 받은 형태 그대로 다른 클라이언트들에게 브로드캐스트합니다.
                        broadcast(message, client_socket)

                    # 2. 종료 처리
                    elif msg_type == &quot;exit&quot;:
                        return

            except Exception as error:
                print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
                break

    # 소켓 연결 종료 시 목록에서 정리
    if client_socket in clients:
        clients.remove(client_socket)

    print(f&quot;접속자 목록에서 제거: {client_address}&quot;)
    print_clients()


# 서버 실행
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    # 소켓 재사용 설정 (서버 재시작 시 포트 바인딩 에러 방지)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen()
    print(&quot;채팅 서버가 시작되었습니다. 클라이언트 연결을 대기합니다.&quot;)

    while True:
        client_socket, client_address = server_socket.accept()
        clients.append(client_socket)
        print(f&quot;새로운 클라이언트 접속: {client_address}&quot;)
        print_clients()

        threading.Thread(
            target=handle_client,
            args=(client_socket, client_address),
            daemon=True,
        ).start()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;chat_client / client.py&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1782215886721&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket
import threading

from protocol import receive_messages, send_message

HOST = &quot;127.0.0.1&quot;
PORT = 5000


def receive_loop(client_socket):
    buffer = &quot;&quot;

    while True:
        try:
            buffer, messages, connected = receive_messages(client_socket, buffer)

            if not connected:
                print(&quot;서버와의 연결이 끊어졌습니다.&quot;)
                break

            for message in messages:
                if message.get(&quot;type&quot;) == &quot;chat&quot;:
                    sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
                    content = message.get(&quot;content&quot;, &quot;&quot;)
                    print(f&quot;\n{sender}: {content}&quot;)
                else:
                    print(f&quot;\n수신 메시지: {message}&quot;)

        except Exception:
            break


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))

    print(&quot;서버에 연결되었습니다.&quot;)
    print(&quot;전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.&quot;)

    receive_thread = threading.Thread(
        target=receive_loop,
        args=(client_socket,)
    )
    receive_thread.daemon = True
    receive_thread.start()

    while True:
        text = input(&quot;&quot;)

        if text == &quot;exit&quot;:
            send_message(client_socket, {&quot;type&quot;: &quot;exit&quot;})
            print(&quot;서버와의 연결을 종료합니다.&quot;)
            break

        send_message(client_socket, {
            &quot;type&quot;: &quot;chat&quot;,
            &quot;content&quot;: text
        })

print(&quot;클라이언트 소켓을 닫았습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;chat_client / protocol.py&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1782215902930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import json

ENCODING = &quot;utf-8&quot;
DELIMITER = &quot;\n&quot;


def send_message(sock, message):
    json_text = json.dumps(message, ensure_ascii=False)  # 메시지를 JSON 문자열로 변환
    data = json_text + DELIMITER  # 끝에 줄바꿈을 붙여 메시지 경계 표시
    sock.sendall(data.encode(ENCODING))  # 바이트로 바꿔 전부 전송


def receive_messages(sock, buffer):  # 25강에서 사용 (24강에서는 호출 안 함)
    data = sock.recv(1024)

    if not data:
        return buffer, [], False

    buffer += data.decode(ENCODING)
    messages = []

    while DELIMITER in buffer:
        raw_message, buffer = buffer.split(DELIMITER, 1)

        if raw_message.strip():
            message = json.loads(raw_message)
            messages.append(message)

    return buffer, messages, True&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;= 끝 =&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/73</guid>
      <comments>https://goodjop79.tistory.com/73#entry73comment</comments>
      <pubDate>Tue, 23 Jun 2026 21:06:14 +0900</pubDate>
    </item>
    <item>
      <title>D+52[Python] 다중 접속과 브로드캐스팅메시지 &amp;amp; 프로토콜</title>
      <link>https://goodjop79.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 22일(월)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 주 제 : 다중 접속과 브로드캐스팅 메세지 실습하고 이해하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;14, 15강 JSON 기반 메세지 타입 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;16강 패킷 경계란 무엇인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;17강 패킷 경계와 메세지 구분 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;18강 시스템 메세지와 오류 메세지 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※&lt;span&gt;&amp;nbsp; 이 단원은 좀 어렵다. 좀더 이해가 필요하다.&amp;nbsp; 실습간에는 가상의 공간 주소를 활용하였음. 이후 수정됨&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.&amp;nbsp; JSON 기반 메세지 타입 설계&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;type&quot; : &quot;메세지 종류, 서버는 이 값을 보고 처리방식을 결정함&quot;&lt;br /&gt;&quot;sender&quot;&amp;nbsp;:&amp;nbsp;&quot;보낸사람&quot;&lt;br /&gt;&quot;targer&quot;&amp;nbsp;:&amp;nbsp;&quot;받는&amp;nbsp;사람&quot;&lt;br /&gt;&quot;content&quot;&amp;nbsp;:&amp;nbsp;&quot;실제&amp;nbsp;내용&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 문자열 방식 :&lt;/b&gt; 분할된 리스트에서 수신자 아이디를 추출하고, 문자열을 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be1H1D/dJMcabq8mb5/wBPB3cjj9EnapGhyPkyPE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be1H1D/dJMcabq8mb5/wBPB3cjj9EnapGhyPkyPE0/img.png&quot; data-alt=&quot;문자열 방식 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be1H1D/dJMcabq8mb5/wBPB3cjj9EnapGhyPkyPE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe1H1D%2FdJMcabq8mb5%2FwBPB3cjj9EnapGhyPkyPE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;160&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;문자열 방식 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782121074547&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 분석할 귓속말 메시지 정의 (형식: /w 유저명 메시지)
message = &quot;/w user2 안녕하세요&quot;
 
# 메시지가 귓속말 명령어(&quot;/w&quot;)로 시작하는지 확인
if message.startswith(&quot;/w&quot;):
    # 공백(&quot; &quot;)을 기준으로 메시지를 최대 2번만 분할합니다.
    # 결과: ['/w', 'user2', '안녕하세요']
    parts = message.split(&quot; &quot;, 2)
    
    # 분할된 리스트에서 1번 인덱스(두 번째 요소)인 수신자 아이디를 추출
    target = parts[1]
    
    # 분할된 리스트에서 2번 인덱스(세 번째 요소)인 대화 내용을 추출
    content = parts[2]
    
    # 추출한 정보를 바탕으로 귓속말 형태의 문자열을 출력
    print(f&quot;{target}에게 귓속말: {content}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2. json 방식 : &lt;/b&gt;조건문을 줘서 수신자와 내용을 변수에 저장하고 대상과 내용을 화면에 출력한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWvaGG/dJMcabq8mcx/FX2yUTmaQy7IKHeBSJwEBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWvaGG/dJMcabq8mcx/FX2yUTmaQy7IKHeBSJwEBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWvaGG/dJMcabq8mcx/FX2yUTmaQy7IKHeBSJwEBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWvaGG%2FdJMcabq8mcx%2FFX2yUTmaQy7IKHeBSJwEBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;150&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782121129994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 메시지 데이터를 담은 딕셔너리 정의
message = {
    &quot;type&quot;: &quot;whisper&quot;,   # 메시지 종류 (예: whisper = 귓속말)
    &quot;sender&quot;: &quot;user1&quot;,   # 메시지를 보내는 사람
    &quot;target&quot;: &quot;user2&quot;,   # 메시지를 받는 사람 (수신자)
    &quot;content&quot;: &quot;안녕하세요&quot; # 메시지 내용
}

# 2. 메시지 타입이 '귓속말(whisper)'인지 확인하는 조건문
if message[&quot;type&quot;] == &quot;whisper&quot;:
    # 딕셔너리에서 수신자(target)와 내용(content)을 추출하여 변수에 저장
    target = message[&quot;target&quot;]
    content = message[&quot;content&quot;]
    
    # 귓속말 대상과 내용을 화면에 출력 (f-string 포맷팅 사용)
    print(f&quot;{target}에게 귓속말: {content}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 메시지 구조 공통 기본 구조&lt;/p&gt;
&lt;pre id=&quot;code_1782121198212&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 공통 기본 구조
{
    &quot;type&quot;: &quot;&quot;,      # 항상 포함 &amp;mdash; 서버가 처리 방식 결정
    &quot;sender&quot;: &quot;&quot;,    # 보낸 사람
    &quot;target&quot;: &quot;&quot;,    # 받는 사람 (whisper 등 필요한 경우만)
    &quot;content&quot;: &quot;&quot;    # 실제 내용
}
 
# 타입별 설계 정리
{&quot;type&quot;: &quot;chat&quot;,    &quot;sender&quot;: &quot;user1&quot;,                            &quot;content&quot;: &quot;안녕하세요&quot;}
{&quot;type&quot;: &quot;join&quot;,    &quot;sender&quot;: &quot;user1&quot;}
{&quot;type&quot;: &quot;leave&quot;,   &quot;sender&quot;: &quot;user1&quot;}
{&quot;type&quot;: &quot;whisper&quot;, &quot;sender&quot;: &quot;user1&quot;, &quot;target&quot;: &quot;user2&quot;,         &quot;content&quot;: &quot;비밀 메시지&quot;}
{&quot;type&quot;: &quot;error&quot;,                                                  &quot;content&quot;: &quot;존재하지 않는 사용자입니다.&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.&amp;nbsp; &lt;/b&gt;&lt;b&gt;패킷 경계란 무엇인가? 애플리케이션 책임 구간과 소켓에 흐름 이해&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 패킷 경계(Packet Boundary)란 네트워크 상에서 전송되는 데이터 패킷과 패킷 사이의 구분선을 의미한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; 송신 측이 보낸 데이터의 시작과 끝을 수신 측이 정확하게 파악할 수 있느냐에 대한 개념으로, 우리가 사용하는 전송 계층&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 프로토콜(TCP vs UDP)과 애플리케이션의 구현 방식에 따라 이 경계의 의미가 완전히 달라진다.&lt;br /&gt;&lt;b&gt;&lt;br /&gt;2.1. TCP와 UDP에서의 패킷 경계&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZDCHF/dJMcajvVjYc/Ud5bikq8BkKQ3ykoqoQdK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZDCHF/dJMcajvVjYc/Ud5bikq8BkKQ3ykoqoQdK0/img.png&quot; data-alt=&quot;패킷 구분자 실습 (A, B 구분)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZDCHF/dJMcajvVjYc/Ud5bikq8BkKQ3ykoqoQdK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZDCHF%2FdJMcajvVjYc%2FUd5bikq8BkKQ3ykoqoQdK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;505&quot; height=&quot;150&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;패킷 구분자 실습 (A, B 구분)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deBIWY/dJMcacDB7VO/KXOuIUpKo7WPkRVOkkSi60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deBIWY/dJMcacDB7VO/KXOuIUpKo7WPkRVOkkSi60/img.png&quot; data-alt=&quot;예제 실습 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deBIWY/dJMcacDB7VO/KXOuIUpKo7WPkRVOkkSi60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeBIWY%2FdJMcacDB7VO%2FKXOuIUpKo7WPkRVOkkSi60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;151&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예제 실습 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY1jNf/dJMcadoRFMJ/dKRmLh6xOcwuTmdKQUeBKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY1jNf/dJMcadoRFMJ/dKRmLh6xOcwuTmdKQUeBKk/img.png&quot; data-alt=&quot;소과제1. 실습 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY1jNf/dJMcadoRFMJ/dKRmLh6xOcwuTmdKQUeBKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY1jNf%2FdJMcadoRFMJ%2FdKRmLh6xOcwuTmdKQUeBKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;497&quot; height=&quot;150&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소과제1. 실습 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782121355428&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket

server, client = socket.socketpair()  # 네트워크 없이 연결된 두 소켓을 만든다

client.send(b&quot;hello world&quot;)  # 메시지 하나를 한 번에 보낸다 (send는 1번뿐)

part1 = server.recv(4)  # 앞에서 최대 4바이트만 읽는다
part2 = server.recv(4)  # 이어서 최대 4바이트만 더 읽는다
print(part1)  # 첫 번째 조각 확인
print(part2)  # 두 번째 조각 확인

server.close()  # 소켓 닫기
client.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCg2Jh/dJMcagsqdzh/5bCJRUAWkKIKfIqAXmQf20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCg2Jh/dJMcagsqdzh/5bCJRUAWkKIKfIqAXmQf20/img.png&quot; data-alt=&quot;소과제 2. 실습 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCg2Jh/dJMcagsqdzh/5bCJRUAWkKIKfIqAXmQf20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCg2Jh%2FdJMcagsqdzh%2F5bCJRUAWkKIKfIqAXmQf20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;208&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;소과제 2. 실습 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782121424379&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket

# 소켓 쌍(서버와 클라이언트 역할을 동시에 할 수 있는 연결된 소켓 2개)을 생성합니다.
server, client = socket.socketpair()

# 같은 짧은 메시지를 10번 연속으로 보냅니다.
for i in range(10):
    # 메시지의 끝을 식별할 수 있도록 구분자(개행 문자 \n)를 붙여 바이트 형태로 전송합니다.
    client.send(b&quot;hello\n&quot;)

# 수신 버퍼에서 데이터를 최대 1024바이트만큼 읽어옵니다. (단 한 번만 호출)
# TCP 특성상 보낸 메시지 10개가 하나의 덩어리로 뭉쳐서 한 번에 읽힐 수 있습니다.
data = server.recv(1024)

# 10개의 메시지가 어떤 모습으로 (예: b'hello\nhello\n...') 뭉쳐서 도착했는지 콘솔에 출력해 확인합니다.
print(data)

# 받은 바이트(bytes) 데이터를 디코딩하여 일반 문자열(string)로 변환합니다.
text = data.decode(&quot;utf-8&quot;)

# 약속된 구분자인 개행 문자('\n')를 기준으로 전체 문자열을 잘라 리스트로 만듭니다.
# 이 과정을 통해 붙어왔던 메시지들이 개별 메시지로 분리됩니다.
messages = text.split(&quot;\n&quot;)

# 제대로 분리되었는지 확인하기 위한 카운트 변수를 초기화합니다.
count = 0

# 분리된 메시지 리스트를 하나씩 순회합니다.
for message in messages:
    # 문자열의 맨 마지막 '\n' 때문에 split 결과로 생기는 마지막 빈 문자열(&quot;&quot;)은 제외합니다.
    if message:
        count += 1
        # 몇 번째 메시지인지 번호와 분리된 메시지 내용을 함께 출력합니다.
        print(count, message)

# 통신이 끝났으므로 사용한 서버 소켓과 클라이언트 소켓을 모두 닫아 자원을 반납합니다.
server.close()
client.close()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 패킷 경계와 메세지 구분 처리&lt;/b&gt;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;3.1. 문제의 배경: TCP의 스트림 지향 특성&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgFEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;TCP는 경계가 없다 &lt;/b&gt;: TCP는 전송 데이터를 끊이지 않는 하나의 연속된 바이트 스트림(Stream)으로 취급한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgFEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;패킷 단편화와 결합 &lt;/b&gt;: 송신측이 보낸 하나의 큰 메시지가 여러 패킷으로 쪼개지거나(&lt;b&gt;TCP Segmentation&lt;/b&gt;), 반대로 여러 작은 메시지가 하나의 패킷에 합쳐져서 수신될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgFEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;메시지 왜곡&lt;/b&gt;: 수신측이 정확한 기준 없이 읽으면 데이터가 잘리거나 다음 메시지까지 침범하는 현상이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;3.2. 패킷 경계 문제의 발생 유형&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgHEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Splicing (정렬 오류)&lt;/b&gt;: 패킷의 끝과 메시지의 끝이 일치하지 않아 데이터가 밀리는 현상.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgHEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Fragmentation (단편화)&lt;/b&gt;: 하나의 메시지가 여러 패킷에 분할되어 도달하여, 메시지의 일부분만 먼저 읽히는 현상.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgHEAY&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Coalescing (결합)&lt;/b&gt;: 송신 속도가 빠르거나 네트워크 버퍼링에 의해 여러 메시지가 한 번에 뭉쳐서 수신 버퍼에 쌓이는 현상.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;3.3. 메시지 구분 처리 해결 방안&lt;/b&gt; (애플리케이션 프로토콜 설계)&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgJEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;TCP 자체는 경계를 유지해주지 않으므로, 상위 애플리케이션 계층에서 직접 메시지의 시작과 끝을 구별하는 규칙을 정의해야 한다.&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;① 고정 길이 방식 (Fixed Length)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgLEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;: 모든 메시지의 크기를 미리 동일하게 고정(예: 무조건 1024바이트)하여 전송한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgLEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;처리 방법&lt;/b&gt;: 수신측은 버퍼에서 정확히 지정된 바이트 수만큼만 끊어서 읽는다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgLEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;장단점&lt;/b&gt;: 구현이 가장 직관적이고 간단하나, 작은 데이터를 보낼 때도 남은 공간을 패딩(공백)으로 채워야 하므로 &lt;b&gt;대역폭 낭비&lt;/b&gt;가 심하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;② 구분자 기반 방식 (Delimiter-based)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgNEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;: 메시지의 끝에 특정 문자나 바이트 패턴(예: \n, \r\n, \0)을 붙여 경계를 표시한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgNEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;처리 방법&lt;/b&gt;: 수신측은 스트림을 읽다가 해당 구분자가 등장할 때까지를 하나의 메시지로 인식한다. (HTTP 헤더, SMTP 등에서 사용)&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgNEAY&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;장단점&lt;/b&gt;: 가변 길이 메시지를 다루기 편하나, 실제 데이터 내부에 구분자와 동일한 문자가 포함될 경우 &lt;b&gt;이스케이프(Escape) 처리&lt;/b&gt;가 필수적이며 문자열 파싱 오버헤드가 존재한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;③ 길이 필드 명시 방식 (Length-Prefix / Type-Length-Value)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgPEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;개념&lt;/b&gt;: 메시지를 보낼 때, 실제 데이터 앞에 해당 데이터의 길이를 나타내는 고정 크기의 헤더(예: 4바이트 정수)를 먼저 붙여 전송한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgPEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;처리 방법&lt;/b&gt;:&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgPEAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;수신측은 먼저 헤더 크기(예: 4바이트)만큼 읽어 데이터의 전체 길이 \(N\)을 파악한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgPEAY&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;그 다음 정확히 \(N\)바이트만큼 수신 버퍼에서 읽어 전송된 메시지를 완성한다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgPEAo&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;장단점&lt;/b&gt;: 바이너리 데이터 전송에 가장 효율적이며 이스케이프 처리가 필요 없다. 다만 구현 시 수신 버퍼에 데이터가 충분히 쌓였는지 체크하는 &lt;b&gt;상태 관리 뼈대 코드&lt;/b&gt;가 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;3.4. 수신측 가용 데이터 체크 핵심 로직&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgREAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;어떤 방식을 쓰든 네트워크 라이브러리 레벨에서는 다음과 같은 예외 처리가 필수적이다.&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgSEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Loop 구조 필수&lt;/b&gt;: 수신 버퍼(recv_buf)에 쌓인 바이트 수가 처리해야 할 메시지 요구량보다 많다면, 하나의 패킷 안에서 여러 개의 메시지를 복원하기 위해 반복문으로 잘라내야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgSEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;미완성 패킷 보존&lt;/b&gt;: 메시지의 일부만 도착한 경우, 가용 바이트가 채워질 때까지 처리를 미루고 다음 데이터 수신 이벤트를 기다려야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17 강파 일스크린샷 2026-06-22 17-29-07.png&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ee0Okp/dJMcaiw1rmt/8RxvUUDgy8KriR2d1zKCok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ee0Okp/dJMcaiw1rmt/8RxvUUDgy8KriR2d1zKCok/img.png&quot; data-alt=&quot;실습 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ee0Okp/dJMcaiw1rmt/8RxvUUDgy8KriR2d1zKCok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fee0Okp%2FdJMcaiw1rmt%2F8RxvUUDgy8KriR2d1zKCok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;235&quot; height=&quot;341&quot; data-filename=&quot;17 강파 일스크린샷 2026-06-22 17-29-07.png&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실습 파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7LNNA/dJMcaiw1rE0/ai7ZencKNNMF1iasKz9VH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7LNNA/dJMcaiw1rE0/ai7ZencKNNMF1iasKz9VH0/img.png&quot; data-alt=&quot;가상의 주소에 가상의 접속자간의 대화를 위한 서버 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7LNNA/dJMcaiw1rE0/ai7ZencKNNMF1iasKz9VH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7LNNA%2FdJMcaiw1rE0%2Fai7ZencKNNMF1iasKz9VH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;830&quot; height=&quot;385&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 주소에 가상의 접속자간의 대화를 위한 서버 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17강 과2제스크린샷 2026-06-22 17-21-15.png&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FdptV/dJMcaiw1rmu/DmDyjiK7YKux21OtxjfLE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FdptV/dJMcaiw1rmu/DmDyjiK7YKux21OtxjfLE1/img.png&quot; data-alt=&quot;가상의 주소에 가상의 접속자간의 대화를 위한 서버 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FdptV/dJMcaiw1rmu/DmDyjiK7YKux21OtxjfLE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFdptV%2FdJMcaiw1rmu%2FDmDyjiK7YKux21OtxjfLE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;825&quot; height=&quot;348&quot; data-filename=&quot;17강 과2제스크린샷 2026-06-22 17-21-15.png&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 주소에 가상의 접속자간의 대화를 위한 서버 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17과제1 2026-06-22 16-49-06.png&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F7LLO/dJMcah5ZxJu/TmkjKpHTK7u78gIb6Zn1Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F7LLO/dJMcah5ZxJu/TmkjKpHTK7u78gIb6Zn1Kk/img.png&quot; data-alt=&quot;가상의 사용자 (2)에서 채팅이 정상 시행된 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F7LLO/dJMcah5ZxJu/TmkjKpHTK7u78gIb6Zn1Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF7LLO%2FdJMcah5ZxJu%2FTmkjKpHTK7u78gIb6Zn1Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;473&quot; data-filename=&quot;17과제1 2026-06-22 16-49-06.png&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 사용자 (2)에서 채팅이 정상 시행된 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;17강과제1스크린샷 2026-06-22 16-48-27.png&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dATr6O/dJMcah5ZxJv/gy9yYerCpMuKoHVaFoq5Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dATr6O/dJMcah5ZxJv/gy9yYerCpMuKoHVaFoq5Zk/img.png&quot; data-alt=&quot;가상의 대화창을 여러개 열어서 정상 실행 여부 확인 화면 (메세지는 20개까지 구분해서 출력됨)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dATr6O/dJMcah5ZxJv/gy9yYerCpMuKoHVaFoq5Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdATr6O%2FdJMcah5ZxJv%2Fgy9yYerCpMuKoHVaFoq5Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;807&quot; height=&quot;320&quot; data-filename=&quot;17강과제1스크린샷 2026-06-22 16-48-27.png&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 대화창을 여러개 열어서 정상 실행 여부 확인 화면 (메세지는 20개까지 구분해서 출력됨)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size18&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;4.&amp;nbsp; 시스템 메세지와 오류 메세지 처리&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.1. 오류로 수정 보완 검색 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;① 빈 데이터(공백) 수신 시 json.loads() 예외 처리&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트가 연결을 끊은 것이 아니라, 어떤 이유로든 빈 줄(&quot;\n&quot;)이나 공백만 포함된 데이터를 보냈을 때 raw_message.strip()이 거짓(False)이 되어 무시되기도 하지만 만약 공백이 아닌 잘못된 JSON 형식(예: {&quot;action&quot;: 처럼 잘린 데이터)이 들어오면 json.loads()에서 json.JSONDecodeError가 발생해 서버가 위험할 수도 있어서 안전하게 try-except로 감싸는 것이 좋다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;② 성능 최적화 (문자열 더하기 + 연산 배제)&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;Python에서 문자열은 불변(Immutable) 객체,&amp;nbsp; buffer += data.decode(...)를 반복하면 매번 새로운 메모리 공간을 할당하고 복사하므로 성능이 저하되니, 데이터가 바이트 단위로 들어오므로, &lt;b data-index-in-node=&quot;129&quot; data-path-to-node=&quot;7&quot;&gt;버퍼를 bytes 타입으로 유지&lt;/b&gt;하다가 구분자 단위로 쪼갠 뒤에 decode하는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 2. 올바르게 수정된 protocol.py 코드 :&amp;nbsp;&lt;/b&gt; 버퍼를 bytes 기반으로 처리하도록 변경&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;※ chat_client 패키지 파일 설치&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.1. protocol.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782123805284&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import json

ENCODING = &quot;utf-8&quot;
# 네트워크 전송 시 혼선을 줄이기 위해 바이트 단위로 구분자를 정의합니다.
DELIMITER = b&quot;\n&quot;  


def send_message(sock, message):
    &quot;&quot;&quot;메시지를 JSON으로 직렬화하고 구분자를 붙여 전송합니다.&quot;&quot;&quot;
    json_text = json.dumps(message, ensure_ascii=False)
    # 문자열 뒤에 \n을 붙이고 전체를 encode해서 전송
    data = (json_text + &quot;\n&quot;).encode(ENCODING)
    sock.sendall(data)


def receive_messages(sock, buffer: bytes):
    &quot;&quot;&quot;
    소켓에서 데이터를 읽어와 버퍼에 추가한 뒤, 
    완전한 JSON 메시지들을 파싱하여 반환합니다.
    &quot;&quot;&quot;
    try:
        data = sock.recv(1024)
    except ConnectionResetError:
        # 상대방이 강제 종료했을 경우 안전하게 연결 종료 처리
        return b&quot;&quot;, [], False

    if not data:
        # 상대방이 정상적으로 연결을 종료한 경우 (EOF)
        return buffer, [], False

    buffer += data
    messages = []

    # 버퍼에 구분자(\n)가 포함되어 있는 동안 반복
    while DELIMITER in buffer:
        raw_message, buffer = buffer.split(DELIMITER, 1)

        # 빈 바이트가 아니라면 JSON 파싱 시도
        if raw_message.strip():
            try:
                # 파싱 직전에 문자열로 디코딩
                decoded_msg = raw_message.decode(ENCODING)
                message = json.loads(decoded_msg)
                messages.append(message)
            except (json.JSONDecodeError, UnicodeDecodeError) as e:
                # 잘못된 데이터가 들어와도 서버가 죽지 않도록 예외 처리
                print(f&quot;[경고] 잘못된 패킷 수신 및 무시: {e}&quot;)
                continue

    return buffer, messages, True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.2. chat_server/server.py&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782120488434&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket
import threading

# 사용자 정의 프로토콜 모듈에서 메시지 송수신 함수를 가져옵니다.
from protocol import receive_messages, send_message

# 서버가 구동될 호스트 IP와 포트 번호 설정
HOST = &quot;127.0.0.1&quot;
PORT = 5000

# 현재 서버에 연결된 클라이언트 소켓들을 저장하는 리스트
clients = []


def print_clients():
    &quot;&quot;&quot;현재 접속 중인 클라이언트 수를 출력하는 함수&quot;&quot;&quot;
    print(f&quot;현재 접속자 수: {len(clients)}&quot;)


def broadcast(message, sender_socket=None):
    &quot;&quot;&quot;
    연결된 모든 클라이언트에게 메시지를 전송(브로드캐스트)하는 함수
    sender_socket이 지정된 경우, 메시지를 보낸 본인을 제외하고 나머지 사람들에게만 전송합니다.
    &quot;&quot;&quot;
    # 순회 중 리스트가 변경되어 오류가 발생하는 것을 방지하기 위해 복사본(clients[:])을 사용합니다.
    for client in clients[:]:
        if client != sender_socket:
            try:
                send_message(client, message)
            except Exception:
                # 전송 실패 시, 해당 클라이언트와의 연결이 끊어진 것으로 간주하고 목록에서 제거합니다.
                if client in clients:
                    clients.remove(client)


def send_error(client_socket, content):
    &quot;&quot;&quot;특정 클라이언트에게 에러 메시지를 형식을 맞춰 전송하는 함수&quot;&quot;&quot;
    error_message = {&quot;type&quot;: &quot;error&quot;, &quot;content&quot;: content}
    send_message(client_socket, error_message)


def handle_client(client_socket, client_address):
    &quot;&quot;&quot;
    각 클라이언트의 연결을 독립적으로 처리하는 스레드 함수
    메시지 수신 대기 및 타입별(채팅, 종료 등) 처리를 담당합니다.
    &quot;&quot;&quot;
    print(f&quot;클라이언트 처리 시작: {client_address}&quot;)

    # [수정 포인트] protocol.py가 bytes 버퍼를 사용하므로 b&quot;&quot; 로 초기화합니다.
    buffer = b&quot;&quot;
    address_text = str(client_address)

    # 새로운 클라이언트 입장 알림 메시 생성 및 브로드캐스트
    join_message = {
        &quot;type&quot;: &quot;system&quot;,
        &quot;content&quot;: f&quot;{address_text} 님이 입장했습니다.&quot;,
    }
    broadcast(join_message, client_socket)

    # context manager(with)를 사용하여 함수가 끝나거나 오류 발생 시 소켓이 자동으로 닫히도록 합니다.
    with client_socket:
        while True:
            try:
                # 클라이언트로부터 메시지를 수신합니다.
                # receive_messages는 바이트 버퍼, 파싱된 메시지 리스트, 연결 유지 여부를 반환합니다.
                buffer, messages, connected = receive_messages(
                    client_socket, buffer
                )

                # 연결이 끊어졌다면 수신 루프를 빠져나갑니다.
                if not connected:
                    print(f&quot;클라이언트 연결 끊김: {client_address}&quot;)
                    break

                # 수신된 메시지들을 하나씩 처리합니다.
                for message in messages:
                    print(f&quot;{client_address} 메시지: {message}&quot;)

                    message_type = message.get(&quot;type&quot;)

                    # 1. 종료 요청(&quot;exit&quot;) 메시지 처리
                    if message_type == &quot;exit&quot;:
                        print(f&quot;클라이언트 종료 요청: {client_address}&quot;)
                        break

                    # 2. 일반 채팅(&quot;chat&quot;) 메시지 처리
                    if message_type == &quot;chat&quot;:
                        content = message.get(&quot;content&quot;, &quot;&quot;).strip()

                        # 빈 메시지 전송 시 에러 처리
                        if not content:
                            send_error(
                                client_socket, &quot;빈 메시지는 보낼 수 없습니다.&quot;
                            )
                            continue

                        # 다른 클라이언트들에게 채팅 내용 전달
                        broadcast_message = {
                            &quot;type&quot;: &quot;chat&quot;,
                            &quot;sender&quot;: address_text,
                            &quot;content&quot;: content,
                        }
                        broadcast(broadcast_message, client_socket)
                        continue

                    # 3. 정의되지 않은 메시지 타입이 올 경우 에러 처리
                    send_error(
                        client_socket,
                        f&quot;지원하지 않는 메시지 타입입니다: {message_type}&quot;,
                    )

                # 메시지 리스트 중에 &quot;exit&quot; 요청이 포함되어 있었다면 메인 수신 루프도 종료합니다.
                if any(message.get(&quot;type&quot;) == &quot;exit&quot; for message in messages):
                    break

            except Exception as error:
                print(f&quot;클라이언트 처리 오류: {client_address}, {error}&quot;)
                break

    # [연결 종료 프로세스]
    # 접속자 목록에서 해당 클라이언트 제거
    if client_socket in clients:
        clients.remove(client_socket)

    # 다른 사용자들에게 퇴장 알림 전송
    leave_message = {
        &quot;type&quot;: &quot;system&quot;,
        &quot;content&quot;: f&quot;{address_text} 님이 퇴장했습니다.&quot;,
    }
    broadcast(leave_message)

    print(f&quot;접속자 목록에서 제거: {client_address}&quot;)
    print_clients()
    print(f&quot;클라이언트 처리 종료: {client_address}&quot;)


# --- 메인 서버 시작 부 ---
# IPv4(AF_INET), TCP(SOCK_STREAM) 소켓을 생성합니다.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    # 재사용 가능하도록 설정 (서버 재시작 시 포트 바인딩 에러 방지)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))  # 지정한 IP와 포트에 소켓 바인딩
    server_socket.listen()  # 클라이언트의 연결 요청 대기 상태로 전환

    print(&quot;시스템/오류 메시지 처리 서버를 시작합니다.&quot;)
    print(&quot;클라이언트 접속 대기 중...&quot;)

    # 서버 메인 루프: 끊임없이 새로운 클라이언트의 접속을 받아들입니다.
    while True:
        # 클라이언트의 접속 승인 (새로운 소켓과 클라이언트 주소 반환)
        client_socket, client_address = server_socket.accept()
        print(f&quot;클라이언트 접속: {client_address}&quot;)

        # 새로운 클라이언트를 관리 리스트에 추가
        clients.append(client_socket)
        print(f&quot;접속자 목록에 추가: {client_address}&quot;)
        print_clients()

        # 멀티스레딩 적용: 클라이언트별로 독립된 스레드를 생성하여 handle_client 함수를 실행합니다.
        client_thread = threading.Thread(
            target=handle_client, args=(client_socket, client_address)
        )
        client_thread.start()  # 스레드 시작&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.3. chat_client/client.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782120536098&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import socket
import threading

# 사용자 정의 프로토콜 모듈에서 메시지 송수신 함수를 가져옵니다.
from protocol import receive_messages, send_message

# 접속할 서버의 IP 주소와 포트 번호 설정
HOST = &quot;127.0.0.1&quot;
PORT = 5000


def receive_loop(client_socket):
    &quot;&quot;&quot;
    서버로부터 데이터를 실시간으로 수신하는 무한 루프 함수입니다.
    메인 스레드와 별개로 백그라운드(데몬 스레드)에서 작동합니다.
    &quot;&quot;&quot;
    # [수정 포인트] protocol.py가 bytes 버퍼를 사용하므로 b&quot;&quot; 로 초기화합니다.
    buffer = b&quot;&quot;

    while True:
        try:
            # 서버로부터 메시지를 수신합니다.
            # receive_messages는 바이트 버퍼, 파싱된 메시지 리스트, 연결 유지 여부를 반환합니다.
            buffer, messages, connected = receive_messages(
                client_socket, buffer
            )

            # 서버가 연결을 끊었거나 연결이 유실된 경우
            if not connected:
                print(&quot;\n서버와의 연결이 끊어졌습니다.&quot;)
                break

            # 수신된 메시지들을 하나씩 꺼내어 타입별로 처리합니다.
            for message in messages:
                message_type = message.get(&quot;type&quot;)

                # 1. 일반 채팅 메시지 처리
                if message_type == &quot;chat&quot;:
                    sender = message.get(&quot;sender&quot;, &quot;unknown&quot;)
                    content = message.get(&quot;content&quot;, &quot;&quot;)
                    print(f&quot;\n{sender}: {content}&quot;)

                # 2. 시스템 알림 메시지 처리 (예: 누구누구 님이 입장했습니다.)
                elif message_type == &quot;system&quot;:
                    content = message.get(&quot;content&quot;, &quot;&quot;)
                    print(f&quot;\n[시스템] {content}&quot;)

                # 3. 에러 메시지 처리 (예: 빈 메시지 전송 제한 오류 등)
                elif message_type == &quot;error&quot;:
                    content = message.get(&quot;content&quot;, &quot;&quot;)
                    print(f&quot;\n[오류] {content}&quot;)

                # 4. 약속되지 않은 다른 타입의 메시지가 왔을 경우 처리
                else:
                    print(f&quot;\n[알 수 없는 메시지] {message}&quot;)

        except Exception:
            # 수신 중 에러가 발생하면 수신 루프를 종료합니다.
            break


# --- 메인 클라이언트 실행 부 ---
# IPv4(AF_INET), TCP(SOCK_STREAM) 소켓을 생성합니다.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    try:
        # 설정된 IP와 포트를 통해 서버에 연결을 시도합니다.
        client_socket.connect((HOST, PORT))
    except ConnectionRefusedError:
        print(&quot;서버가 켜져 있지 않거나 연결할 수 없습니다.&quot;)
        exit()

    print(&quot;서버에 연결되었습니다.&quot;)
    print(&quot;전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.&quot;)

    # [멀티스레딩] 서버의 메시지를 상시 대기할 서브 스레드를 생성합니다.
    receive_thread = threading.Thread(
        target=receive_loop, args=(client_socket,)
    )
    # 데몬 스레드로 설정: 메인 스레드(사용자 입력 루프)가 종료되면 이 스레드도 자동으로 함께 종료됩니다.
    receive_thread.daemon = True
    receive_thread.start()  # 수신 스레드 시작

    # [메인 루프] 사용자의 키보드 입력을 대기하고 서버로 전송하는 블로킹 구간입니다.
    while True:
        try:
            text = input(&quot;&quot;)  # 사용자 입력 대기

            # 사용자가 'exit'을 입력하면 종료 프로토콜을 수행합니다.
            if text == &quot;exit&quot;:
                send_message(
                    client_socket, {&quot;type&quot;: &quot;exit&quot;}
                )  # 서버에 종료 알림 전송
                print(&quot;서버와의 연결을 종료합니다.&quot;)
                break  # 키보드 입력 루프 탈출

            # 일반 입력값은 채팅 메시지 형태로 포맷팅하여 서버로 전송합니다.
            send_message(client_socket, {&quot;type&quot;: &quot;chat&quot;, &quot;content&quot;: text})
        except (ConnectionResetError, BrokenPipeError):
            print(&quot;\n서버가 비정상 종료되어 메시지를 보낼 수 없습니다.&quot;)
            break

# with 블록을 빠져나가면서 client_socket이 자동으로 close(닫힘) 됩니다.
print(&quot;클라이언트 소켓을 닫았습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-끝-&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/72</guid>
      <comments>https://goodjop79.tistory.com/72#entry72comment</comments>
      <pubDate>Mon, 22 Jun 2026 19:33:36 +0900</pubDate>
    </item>
    <item>
      <title>D+51 [Python] 채팅서버 용어의 이해 및 예시문제 실습</title>
      <link>https://goodjop79.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 19일(금)&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 주 제 : 채팅 서버를 구축함에 있어서 각 용어의 정의 및 간략한 구조에 대한 이해&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 목 표 : 교육자료 9강의 ~ 15강의 까지 자기주도 학습하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;※ 실습 강의 목록&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;9강 : 여러 클라이언트 접속 구조&lt;/li&gt;
&lt;li&gt;10강 : 스레드 개념과 기초 사용법&lt;/li&gt;
&lt;li&gt;11강 : 스레드로 클라이언트 처리하기&lt;/li&gt;
&lt;li&gt;12강 : 접속자 목록 관리&lt;/li&gt;
&lt;li&gt;13&lt;span style=&quot;color: #000000;&quot;&gt;강 : 전체 채팅 브로드캐스팅 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14강 : 왜 메세지 규칙이 필요한가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;15강 : JSON 기반 메세지 타입 설계&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size23&quot;&gt;1. 용어의 정의 및 간단한 구조 설명&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;1.1. 클라이언트 (Client)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,0,0&quot;&gt;용어의 정의 &lt;/b&gt;: 서버에 연결을 요청하고 서비스를 제공받는 프로그램&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0&quot;&gt;구조 및 특징 &lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;4,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;4,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;4,1,1,0,0,1&quot;&gt;&lt;span&gt;서버와 다수의 클라이언트가 연결된 구조를 가진다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;4,1,1,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;4,1,1,1,0,1&quot;&gt;&lt;span&gt;각 클라이언트는 고유한 식별값(번호)을 가지며, 서버의 &lt;/span&gt;&lt;span&gt;clients&lt;/span&gt;&lt;span&gt; 리스트를 통해 관리 및 식별된다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;4,1,1,2,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;4,1,1,2,0,1&quot;&gt;&lt;span&gt;연결 시 생성되는 연결 소켓을 통해 개별 클라이언트를 구분한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;1.2. 스레드 (Thread)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-42&quot; data-path-to-node=&quot;6,0,1&quot;&gt;&lt;span data-path-to-node=&quot;6,0,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,1,0&quot;&gt;&lt;span&gt;용어의 정의&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 프로세스 내에서 실행되는 가장 작은 작업 단위이자 순차적인 실행 흐름이다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;구조 및 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;6,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;6,1,1,0,0,1&quot;&gt;&lt;span&gt;메인 스레드에서 필요한 만큼의 추가 스레드를 생성하여 병렬 작업을 수행한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;6,1,1,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;6,1,1,1,0,1&quot;&gt;&lt;span&gt;데이터 송수신과 같은 실질적인 동작은 별도의 스레드에서 처리하여 효율을 높인다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-45&quot; data-path-to-node=&quot;6,1,1,2,1&quot;&gt;&lt;span data-path-to-node=&quot;6,1,1,2,1,0&quot;&gt;&lt;span&gt;join()&lt;/span&gt;&lt;span&gt; 함수를 통해 메인 스레드가 보조 스레드의 종료를 기다리게 할지 여부를 결정할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size20&quot;&gt;1.3. 다중 접속 서버 (Multi-connection Server)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;용어의 정의&lt;/b&gt;: 여러 클라이언트의 동시 접속을 수용하여 동시에 통신을 처리하는 서버 구조&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;구조 및 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;8,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;8,1,1,0,0,1&quot;&gt;&lt;span&gt;[서버 소켓 생성 &amp;rarr; 주소 바인딩 &amp;rarr; 접속 대기]의 흐름을 가진다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-47&quot; data-path-to-node=&quot;8,1,1,1,1&quot;&gt;&lt;span data-path-to-node=&quot;8,1,1,1,1,0&quot;&gt;&lt;span&gt;while True&lt;/span&gt;&lt;span&gt; 루프와 &lt;/span&gt;&lt;span&gt;accept()&lt;/span&gt;&lt;span&gt; 메서드를 반복 호출하여 끊임없이 새로운 연결을 받아들인다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;8,1,1,2,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;8,1,1,2,0,1&quot;&gt;&lt;span&gt;개발자의 논리적 사고를 의사코드(Pseudocode)로 구조화하는 연습이 중요하다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;1.4. 소켓 (Socket)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-49&quot; data-path-to-node=&quot;10,0,1&quot;&gt;&lt;span data-path-to-node=&quot;10,0,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,1,0&quot;&gt;&lt;span&gt;용어의 정의&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 네트워크상에서 두 프로그램 간 양방향 통신을 가능하게 하는 종착점(Endpoint)이자 데이터 출입문&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;구조 및 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;10,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;10,1,1,0,0,1&quot;&gt;&lt;span&gt;응용 프로그램이 OS의 네트워크 기능을 활용하도록 돕는다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,1,1,0&quot;&gt;데이터 흐름 3단계&lt;/b&gt;:&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;10,1,1,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-51&quot; data-path-to-node=&quot;10,1,1,1,1,0,1&quot;&gt;&lt;span data-path-to-node=&quot;10,1,1,1,1,0,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,1,1,1,0,1,0&quot;&gt;&lt;span&gt;송신&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 애플리케이션 데이터가 커널에서 패킷으로 캡슐화되어 송출&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-52&quot; data-path-to-node=&quot;10,1,1,1,1,1,1&quot;&gt;&lt;span data-path-to-node=&quot;10,1,1,1,1,1,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,1,1,1,1,1,0&quot;&gt;&lt;span&gt;전송&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 라우터와 스위치를 거쳐 목적지로 라우팅&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li id=&quot;p-rc_8d276a41ba752f37-53&quot; data-path-to-node=&quot;10,1,1,1,1,2,1&quot;&gt;&lt;span data-path-to-node=&quot;10,1,1,1,1,2,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,1,1,1,2,1,0&quot;&gt;&lt;span&gt;수신&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 서버가 패킷을 받아 포트 번호를 분석하고, 해당 소켓의 수신 버퍼에 데이터를 적재&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;1.5. 접속자 목록 관리 및 브로드캐스팅&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목록 관리&lt;/b&gt; : 연결된 클라이언트들을 리스트 등에 저장하여 개별적으로 제어하는 과정&lt;span style=&quot;letter-spacing: 0px;&quot; data-path-to-node=&quot;12,0,1,0,1,1&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,1,1,0&quot;&gt;브로드캐스팅&lt;/b&gt;: 서버에 연결된 모든 클라이언트에게 데이터를 일괄 전송하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;구조 및 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;12,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;12,1,1,0,0,1&quot;&gt;&lt;span&gt;개별 클라이언트 식별을 위해 목록을 관리하며, 귓속말 기능 구현 시에는 특정 클라이언트만 타겟팅&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;12,1,1,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;12,1,1,1,0,1&quot;&gt;&lt;span&gt;메시지 전송 시 단순 문자열보다 &lt;/span&gt;&lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;12,1,1,1,0,1&quot;&gt;&lt;span&gt;JSON 형식&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 사용하면 데이터 유지관리가 용이&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size20&quot;&gt;1.6. 데이터 구조 (Data Structure)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;용어의 정의&lt;/b&gt;: 컴퓨터가 데이터를 효율적으로 저장하고 처리하기 위해 사용하는 논리적인 체계&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;구조 및 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;14,1,1,0,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;14,1,1,0,0,1&quot;&gt;&lt;span&gt;데이터는 전기 신호인 0과 1(비트 단위)로 저장된다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span data-path-to-node=&quot;14,1,1,1,0,0&quot;&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;14,1,1,1,0,1&quot;&gt;&lt;span&gt;개발자는 데이터를 16진수(4비트를 2자리로 표현) 형태로 다루며, 이는 효율적인 데이터 관리에 도움을 준다.&lt;/span&gt;&lt;/span&gt;&lt;span data-path-to-node=&quot;14,1,1,1,0,1&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-path-to-node=&quot;2&quot;&gt;2. 예시문제 실습 하기 (9강의 ~ 13강의)&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;※ 가상의 내부망 주소는 실습용임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 1.&amp;nbsp; 7강. 반복 송수신과 종료 처리&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHtrNl/dJMcaiXYT8o/KKJ5zrQBSd6ta9acK8FJ4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHtrNl/dJMcaiXYT8o/KKJ5zrQBSd6ta9acK8FJ4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHtrNl/dJMcaiXYT8o/KKJ5zrQBSd6ta9acK8FJ4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHtrNl%2FdJMcaiXYT8o%2FKKJ5zrQBSd6ta9acK8FJ4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;651&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 2.&lt;span&gt;&amp;nbsp; 8강. 1:1 터미널 채팅 완성하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5bT7a/dJMcah5XSI6/G2MrQDGVxOYLKklmIqg3r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5bT7a/dJMcah5XSI6/G2MrQDGVxOYLKklmIqg3r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5bT7a/dJMcah5XSI6/G2MrQDGVxOYLKklmIqg3r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5bT7a%2FdJMcah5XSI6%2FG2MrQDGVxOYLKklmIqg3r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;604&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 3.&amp;nbsp; 10강. 스레드 개념과 기초 사용법&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;921&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJOU8/dJMcac4Ad1U/UQsls6T0CzvAXixXSpZdLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJOU8/dJMcac4Ad1U/UQsls6T0CzvAXixXSpZdLK/img.png&quot; data-alt=&quot;10강 예시문제 실습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJOU8/dJMcac4Ad1U/UQsls6T0CzvAXixXSpZdLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAJOU8%2FdJMcac4Ad1U%2FUQsls6T0CzvAXixXSpZdLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;667&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;555&quot; data-origin-height=&quot;921&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;10강 예시문제 실습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 4. 11강. 스레드로 클라이언트 처리하기&lt;/b&gt;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgCEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-subtree=&quot;aimfl&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ※&amp;nbsp; 스레드(Thread)&lt;/span&gt;&lt;/b&gt;는 프로세스 내에서 실행되는 &lt;/span&gt;&lt;b&gt;가장 작은 작업 단위&lt;/b&gt;이자 CPU가 처리하는 실제 실행 흐름&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;※ 핵심 개념 요약&lt;/b&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 프로세스 vs 스레드 &lt;/b&gt;: 프로세스는 실행 중인 프로그램(공장)이며, 스레드는 그 안에서 일하는 일꾼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 메모리 공유 &lt;/b&gt;: 한 프로세스 안의 스레드들은 heap 메모리와 자원을 공유하므로 통신이 빠르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 멀티스레딩 &lt;/b&gt;: 하나의 프로그램에서 여러 작업을 동시에 처리(예: 다운로드하면서 화면 표시)하기 위해 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qFAC/dJMcacwMCyy/N8VZR5KEGlE3zCceXnAxy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qFAC/dJMcacwMCyy/N8VZR5KEGlE3zCceXnAxy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qFAC/dJMcacwMCyy/N8VZR5KEGlE3zCceXnAxy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qFAC%2FdJMcacwMCyy%2FN8VZR5KEGlE3zCceXnAxy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;747&quot; height=&quot;460&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;11강 예시문제 실습 화면&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 5.&amp;nbsp; 12강. 접속자 목록 관리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QXKUK/dJMcacQ1Z7R/u9yCpLPErnxd74atEy7080/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QXKUK/dJMcacQ1Z7R/u9yCpLPErnxd74atEy7080/img.png&quot; data-alt=&quot;접속자 처리 화면 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QXKUK/dJMcacQ1Z7R/u9yCpLPErnxd74atEy7080/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQXKUK%2FdJMcacQ1Z7R%2Fu9yCpLPErnxd74atEy7080%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1095&quot; height=&quot;578&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;접속자 처리 화면 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;12_스크린샷 2026-06-19 18-31-13.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s2QFN/dJMcahLBJYf/vKJ7IoPsrlcGbJ7k3MgnnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s2QFN/dJMcahLBJYf/vKJ7IoPsrlcGbJ7k3MgnnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s2QFN/dJMcahLBJYf/vKJ7IoPsrlcGbJ7k3MgnnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs2QFN%2FdJMcahLBJYf%2FvKJ7IoPsrlcGbJ7k3MgnnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;695&quot; data-filename=&quot;12_스크린샷 2026-06-19 18-31-13.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 6. 13강. 전체 채팅 브로드캐스팅 구현&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;793&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brv0P9/dJMcacXPFM9/jUpvP6vu7oJbbynEc94bJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brv0P9/dJMcacXPFM9/jUpvP6vu7oJbbynEc94bJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brv0P9/dJMcacXPFM9/jUpvP6vu7oJbbynEc94bJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrv0P9%2FdJMcacXPFM9%2FjUpvP6vu7oJbbynEc94bJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;832&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;793&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;13_스크린샷 2026-06-19 18-51-59.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pFInn/dJMcabq6LdP/MrrrKSlwuox6FhbzDYHkp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pFInn/dJMcabq6LdP/MrrrKSlwuox6FhbzDYHkp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pFInn/dJMcabq6LdP/MrrrKSlwuox6FhbzDYHkp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpFInn%2FdJMcabq6LdP%2FMrrrKSlwuox6FhbzDYHkp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;484&quot; data-filename=&quot;13_스크린샷 2026-06-19 18-51-59.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2.7. 14강. 왜 메세지 규칙이 필요한가?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgDEAA&quot; data-sfc-root=&quot;ep&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-subtree=&quot;aimfl&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;채팅창에서 메시지 규칙이 필요한 가장 핵심적인 이유는?&lt;/span&gt;&lt;/p&gt;
&lt;p data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgDEAA&quot; data-sfc-root=&quot;ep&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;수많은 이용자가 동시에 소통하는 공간에서 발생할 수 있는 혼란을 막고, 안전하며 효율적인 소통 환경을 조성하기 위함&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;14_스크린샷 2026-06-19 19-02-56.png&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIkw28/dJMcaalnkyW/Vk5QuUQcufXmLDkl75ANw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIkw28/dJMcaalnkyW/Vk5QuUQcufXmLDkl75ANw0/img.png&quot; data-alt=&quot;가상의 환경에서 임의의 채팅창 (실습용)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIkw28/dJMcaalnkyW/Vk5QuUQcufXmLDkl75ANw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIkw28%2FdJMcaalnkyW%2FVk5QuUQcufXmLDkl75ANw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;504&quot; data-filename=&quot;14_스크린샷 2026-06-19 19-02-56.png&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상의 환경에서 임의의 채팅창 (실습용)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;14_귀속말 06-19 19-09-20.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqQ9NW/dJMcaalnky4/pHIKyL5fYgWYOqkDvByCBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqQ9NW/dJMcaalnky4/pHIKyL5fYgWYOqkDvByCBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqQ9NW/dJMcaalnky4/pHIKyL5fYgWYOqkDvByCBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqQ9NW%2FdJMcaalnky4%2FpHIKyL5fYgWYOqkDvByCBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;445&quot; data-filename=&quot;14_귀속말 06-19 19-09-20.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;가상의 환경에서 임의의 채팅창 (실습용)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2.8. 15강. JSON 기반 메세지 타입 설계&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2.8.1.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/71</guid>
      <comments>https://goodjop79.tistory.com/71#entry71comment</comments>
      <pubDate>Fri, 19 Jun 2026 19:43:27 +0900</pubDate>
    </item>
    <item>
      <title>D+50[Python] PySide6 &amp;amp; Server-Client 예제 실습</title>
      <link>https://goodjop79.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 18일(목)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 주 제 : 네트워크 프로그래밍을 이해하고, 서버와 클라이언트의 구조 예제 실습&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-path-to-node=&quot;2&quot;&gt;[네트워크 프로그래밍 기초 및 소켓 통신 이해]&lt;/h1&gt;
&lt;h2 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 프로그래밍의 핵심 개념인 서버-클라이언트 구조, 주요 네트워크 용어, 그리고 소켓(Socket)을 활용한 데이터 통신 흐름을 이해하는 것을 목적으로 함.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size26&quot;&gt;2. 네트워크 프로그래밍의 기본 구조&lt;/h2&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 프로그래밍은 서버와 다수의 클라이언트가 네트워크를 통해 데이터를 상호 교환하는 구조를 설계하는 과정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;상호작용 원리 :&lt;/b&gt; 클라이언트의 요청(Request)에 대해 서버가 응답(Response)하는 구조.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;개발 순서 및 동작 흐름:&lt;/b&gt; 서비스의 시작점은 클라이언트이며, 서버가 항상 먼저 실행되어 대기 상태를 유지해야 한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;7,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,0,0&quot;&gt;서버 :&lt;/b&gt; 실행 및 대기(Listening)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,1,0&quot;&gt;클라이언트 :&lt;/b&gt; 연결 요청(Connection Request)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,2,0&quot;&gt;서버 :&lt;/b&gt; 요청 수락(Accept)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,3,0&quot;&gt;데이터 교환 :&lt;/b&gt; 메시지 및 파일 송&amp;middot;수신&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,1,4,0&quot;&gt;종료 :&lt;/b&gt; 연결 해제&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size26&quot;&gt;3. 핵심 네트워크 용어 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;IP (Internet Protocol) :&lt;/b&gt; 통신 대상을 식별하는 컴퓨터 고유의 주소.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;Port (포트) :&lt;/b&gt; 특정 프로그램에 접근하기 위한 논리적 출입구(번호).&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;TCP (Transmission Control Protocol):&lt;/b&gt; 연결 지향적 방식으로, 연결 수립 후 데이터를 순서대로 신뢰성 있게 전달. (대조군: UDP - 비연결형 방식)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,3,0&quot;&gt;DNS (Domain Name Server):&lt;/b&gt; IP 주소를 사람이 기억하기 쉬운 도메인 이름으로 매핑하여 관리하는 서버.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,4,0&quot;&gt;루프백(Loopback):&lt;/b&gt; 자기 자신의 컴퓨터를 가리키는 주소(127.0.0.1, localhost).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size26&quot;&gt;4. 소켓(Socket) 프로그래밍&lt;/h2&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;소켓은 네트워크 통신의 종단점(Endpoint) 역할을 하며, 소켓 프로그래밍 자체가 곧 네트워크 프로그래밍임.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;동작 로직:&lt;/b&gt; 함수들의 배치와 흐름을 이해하고 로직을 구성하는 것이 핵심.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;주요 함수 및 단계:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;socket(): 소켓 생성 (AF_INET, SOCK_STREAM 등 사용)&lt;/li&gt;
&lt;li&gt;bind(): 소켓에 IP 주소와 포트 번호 결합&lt;/li&gt;
&lt;li&gt;listen(): 연결 대기 상태로 전환&lt;/li&gt;
&lt;li&gt;accept(): &lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;12,1,1,3,0&quot;&gt;[중요]&lt;/b&gt; 접속 요청 수락. 이 함수를 기점으로 '대기용 소켓'과 실제 '데이터 송수신용 소켓'이 분리됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size26&quot;&gt;5. 데이터 활용 및 파일 전송&lt;/h2&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;네트워크를 통한 파일 전송은 파일 I/O(입출력) 개념과 소켓 통신을 결합하여 구현함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 객체 생성(open) 및 쓰기 모드 설정.&lt;/li&gt;
&lt;li&gt;소켓 내부의 식별 번호를 활용하여 데이터를 목적지까지 정확히 전달하는 로직 구현.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[예제 실습]&amp;nbsp; 내부 가상의 host로 실습에서만 활용&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zu2T2/dJMcacQ1PwK/FDtpuHKPlK9Sz944iKfKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zu2T2/dJMcacQ1PwK/FDtpuHKPlK9Sz944iKfKi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zu2T2/dJMcacQ1PwK/FDtpuHKPlK9Sz944iKfKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzu2T2%2FdJMcacQ1PwK%2FFDtpuHKPlK9Sz944iKfKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1379&quot; height=&quot;528&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1379&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;Server-Client 의 예제 실습&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;클라이언트와서버채팅스크린샷 2026-06-18 15-44-41.png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfQexV/dJMcahSkcW9/UfS08r88gqDeBOEnEB5IQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfQexV/dJMcahSkcW9/UfS08r88gqDeBOEnEB5IQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfQexV/dJMcahSkcW9/UfS08r88gqDeBOEnEB5IQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfQexV%2FdJMcahSkcW9%2FUfS08r88gqDeBOEnEB5IQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1434&quot; height=&quot;977&quot; data-filename=&quot;클라이언트와서버채팅스크린샷 2026-06-18 15-44-41.png&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;Server-Client 의 예제 실습&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3nDgA/dJMcaiDKWZf/O2udHhUvhq9KukeoQIFUJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3nDgA/dJMcaiDKWZf/O2udHhUvhq9KukeoQIFUJk/img.png&quot; data-alt=&quot;Server-Client 의 예제 실습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3nDgA/dJMcaiDKWZf/O2udHhUvhq9KukeoQIFUJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3nDgA%2FdJMcaiDKWZf%2FO2udHhUvhq9KukeoQIFUJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1168&quot; height=&quot;183&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Server-Client 의 예제 실습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/70</guid>
      <comments>https://goodjop79.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 19 Jun 2026 16:24:47 +0900</pubDate>
    </item>
    <item>
      <title>[D+50] LLM (Gemini, lovable, gabia) 활용한 개인 웹페이지 제작(실습)</title>
      <link>https://goodjop79.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 18일(목)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 주 제 : LLM을 활용한 바이브코딩 &amp;amp; 개인 웹페이지 제작 실습을 통해 &quot;개인 포트폴리오&quot; 사이트 제작 실습을 해 본다.&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. 개인 포트폴리오 준비에 앞서서 생각해 보기&lt;/b&gt;&lt;br&gt;본인은 회사에 입사 할 준비가 되어 있는가? 일단 시작해야 한다. 취업을 위한 준비를 해 나가야 한다. 현업에서 일, 공부 병행 5년 정도 하면 그 분야에 집중할수 있게 된다. 취업 준비의 출발점은 학생과 개발자 사이의 근본적인 관점 차이가 존재 한다.&lt;br&gt;신입은 생산성 향상을 위해 채용이 되었다면 조직문화에 기여할 수 있는 인력이 되어야 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2. 개인 포트폴리오를 관리 하기 위한 개인만의 웹사이트 개설 절차 실습&lt;/b&gt;&lt;br&gt;web은 브라우저에 올라가는 &quot;문서&quot;이다.&amp;nbsp; 가벼운 문서는 브라우저에 웹으로 구성하고, 게임 등 무거운 경우에는 그러하지 아니한다&lt;br&gt;예를 들어 출입증 시스템에 출입증을 찍으면 web로 관리한다. 대부분 기업에서는 서버를 구현하고 관리하는 장치가 번거로우니 Cloud, AWS를 대여해서 사용한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;3. 웹사이트 생성 절차&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3.1. 생성 절차&amp;nbsp;&lt;/b&gt;&lt;br&gt;Gemini Canvas 접속하기, 브라우저(Chrome 권장) --&amp;gt; 접속 후 프롬프트 입력창 아래 [Canvas, 로그인상태] 버튼을 클릭 --&amp;gt;&amp;nbsp;&lt;br&gt;채팅창이 2분할(좌측 채팅, 우측 편집영역) 확인 --&amp;gt; 포트폴리오 정보 미리 메모장에 입력해 두기 (코드생성간 활용) --&amp;gt; 화면에 구상하고 싶은 내용과 항목들을 자유롭게 포함하여 프롬프트를 작성한다 --&amp;gt; 기획단계(페이지 구조 만들기, 섹션을 채팅을 통해 기획 작성) --&amp;gt; 코딩단계(프롬프트 활용) --&amp;gt; 디자인 단계 --&amp;gt; &lt;b&gt;html 코드 파일(index.html)를 웹사이트 링크 받아 크롬 주소창에서 열기&lt;/b&gt;&lt;br&gt;&lt;i&gt;※ 코딩후 코드를 자유롭게 수정할 수 있다면 기존의 사이트를 샘플로 활용하여 링크를 주고 변경하고 싶고 구현하고 싶은 부분을 명확하게 프롬프트로 명시해서 웹사이트를 수정 생성할 수도 있다&lt;/i&gt;.&amp;nbsp; (단, 저작권에 유의)&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt;예시, 구상에 포함할 요소&amp;gt;&lt;/b&gt;&lt;br&gt;이름 나늑구&lt;br&gt;한&amp;nbsp;줄&amp;nbsp;소개 &quot;사용자&amp;nbsp;경험을&amp;nbsp;고민하는&amp;nbsp;프론트엔드&amp;nbsp;개발자&amp;nbsp;지망생&quot; &amp;nbsp;&lt;br&gt;기술&amp;nbsp;스택 HTML,&amp;nbsp;CSS,&amp;nbsp;JavaScript,&amp;nbsp;Python,&amp;nbsp;Git &amp;nbsp;&lt;br&gt;프로젝트&amp;nbsp;1 투두리스트&amp;nbsp;앱&amp;nbsp;—&amp;nbsp;HTML/CSS/JS,&amp;nbsp;GitHub&amp;nbsp;링크 &amp;nbsp;&lt;br&gt;프로젝트&amp;nbsp;2 날씨&amp;nbsp;앱&amp;nbsp;—&amp;nbsp;OpenWeather&amp;nbsp;API&amp;nbsp;활용,&amp;nbsp;배포&amp;nbsp;링크 &amp;nbsp;&lt;br&gt;학력·교육 ○○대학교 000공학과 / ○○ 부트캠프 수료 &amp;nbsp;&lt;br&gt;목표 사용자&amp;nbsp;친화적인&amp;nbsp;UI를&amp;nbsp;만드는&amp;nbsp;프론트엔드&amp;nbsp;개발자 &amp;nbsp;&lt;br&gt;GitHub&amp;nbsp;&lt;a href=&quot;https://github.com/username&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://github.com/username&lt;/span&gt;&lt;/a&gt;&lt;br&gt;이메일 email@example.com &amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp;&amp;lt;기획단계&amp;gt;&lt;/b&gt;&lt;br&gt;코딩해줘 가 아니라 &quot;기획해 줘&quot;. 기획 단계에서 먼저 섹션 목록을 받아보면, 내가 원하는 섹션이 빠져 있을 때 코딩 전에 추가 요청을 할 수 있다. 포함구성 요소들을 한번에 모두 포함하여 요청해도 바이브코딩은 되지만, 단계별로 코딩 요청을 해야한다. 섹션을 나중에 추가할 때 구조가 얽혀 수정이 어려워 질 수도 있다.&lt;br&gt;* 섹션 구성안을 텍스트로 제시, 누락된 섹션이 있으면 &quot;○○ 섹션도 추가해줘&quot;로 보완 요청한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt;코딩 단계&amp;gt;&lt;/b&gt;&lt;br&gt;내 정보를 입력해 HTML·CSS·JS가 하나로 합쳐진 포트폴리오 파일을 만든다.&lt;br&gt;※ &lt;b&gt;HTML, CSS, JS(JavaScript)는 웹페이지를 만드는 세 가지 핵심 기술,&lt;/b&gt;&amp;nbsp;쉽게 비유하자면 HTML은 건물의 &lt;b&gt;뼈대&lt;/b&gt;, CSS는 건물의 &lt;b&gt;인테리어(디자인)&lt;/b&gt;, JS는 건물 내의 엘리베이터나 자동문 같은 &lt;b&gt;동적 기능&lt;/b&gt;을 담당한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt; 프롬프트 : 원코드 포트폴리오 생성&amp;gt;&lt;/b&gt;&lt;br&gt;예시 포함요소들과 추가로 생각한 요소들, 특별히 강조할 부분을 &quot; &quot;로 묶어서 프롬프트를 생성해서 소스코드를 요청한다.&lt;br&gt;(예시)&lt;br&gt;프롬프트 : 아래 가상의 개인 정보를 기반으로 개발자 포트폴리오 원코드 홈페이지를 코딩해줘.&lt;br&gt;[이름] (가상의 이름 : 골든김)&lt;br&gt;[한&amp;nbsp;줄&amp;nbsp;소개]&amp;nbsp;(본인&amp;nbsp;한&amp;nbsp;줄&amp;nbsp;소개)&lt;br&gt;[기술&amp;nbsp;스택]&amp;nbsp;(언어,&amp;nbsp;프레임워크,&amp;nbsp;툴&amp;nbsp;목록)&lt;br&gt;[프로젝트1]&amp;nbsp;제목:&amp;nbsp;(프로젝트명)&amp;nbsp;/&amp;nbsp;설명:&amp;nbsp;(한&amp;nbsp;줄&amp;nbsp;설명)&amp;nbsp;/&amp;nbsp;기술:&amp;nbsp;(사용&amp;nbsp;기술)&amp;nbsp;/&amp;nbsp;링크:&amp;nbsp;(GitHub&amp;nbsp;또는&amp;nbsp;배포&amp;nbsp;URL)&lt;br&gt;[프로젝트2]&amp;nbsp;제목:&amp;nbsp;(프로젝트명)&amp;nbsp;/&amp;nbsp;설명:&amp;nbsp;(한&amp;nbsp;줄&amp;nbsp;설명)&amp;nbsp;/&amp;nbsp;기술:&amp;nbsp;(사용&amp;nbsp;기술)&amp;nbsp;/&amp;nbsp;링크:&amp;nbsp;(GitHub&amp;nbsp;또는&amp;nbsp;배포&amp;nbsp;URL)&lt;br&gt;[학력·교육]&amp;nbsp;(학교명&amp;nbsp;전공&amp;nbsp;/&amp;nbsp;수료한&amp;nbsp;강의나&amp;nbsp;부트캠프)&lt;br&gt;[목표]&amp;nbsp;(어떤&amp;nbsp;개발자가&amp;nbsp;되고&amp;nbsp;싶은지&amp;nbsp;1~2문장)&lt;br&gt;[GitHub]&amp;nbsp;(GitHub&amp;nbsp;프로필&amp;nbsp;URL)&lt;br&gt;[이메일]&amp;nbsp;(이메일&amp;nbsp;주소)&lt;br&gt;HTML,&amp;nbsp;CSS,&amp;nbsp;JS를&amp;nbsp;한&amp;nbsp;파일에&amp;nbsp;담아서&amp;nbsp;만들어줘.&lt;br&gt;개발자 포트폴리오답게 &quot;다크 테마&quot;로 만들어줘. --&amp;gt; 이유 : 개발자 포트폴리오는 다크 배경이 기술적인 인상을 주고, 기술 스택 태그나 코드 스니펫이 더 잘 어울린다. 라이트 테마를 원하면 이 줄을 빼거나 &quot;밝고 깔끔한 흰색 테마로 만들어줘&quot;로 바꾸면 된다. 전체적인 구조를 영어가 아닌 한글로 하고 싶다면 &quot;모든 텍스트를 한글로 변경해&quot; 라고 하면 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt; 디자인 단계&amp;gt;&lt;/b&gt;&amp;nbsp; 폰트·아이콘·프로필 사진 적용할 사항을 요청하기, 그러나 완성후에도 코드에서 수정해 주어도 된다.&lt;br&gt;* 사전 작업: &lt;a href=&quot;https://fonts.google.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://fonts.google.com&lt;/span&gt;&lt;/a&gt; 접속 → 좌측 필터에서 Language: Korean 클릭해서 폰트를 바꿔줄 수도 있다.&lt;br&gt;※ 폰트명 분위기 추천&amp;nbsp;상황&lt;br&gt;IBM&amp;nbsp;Plex&amp;nbsp;Sans&amp;nbsp;KR 모던하고&amp;nbsp;기술적 백엔드·풀스택&amp;nbsp;지향&lt;br&gt;Noto&amp;nbsp;Sans&amp;nbsp;KR 깔끔하고&amp;nbsp;전문적 범용,&amp;nbsp;어디서나&amp;nbsp;무난&lt;br&gt;Gowun&amp;nbsp;Dodum 친근하고&amp;nbsp;읽기&amp;nbsp;쉬움 프론트엔드·디자인&amp;nbsp;지향&lt;br&gt;폰트 이름은 반드시 영문으로 입력해야 한다. &quot;IBM 플렉스 산스&quot;가 아니라 Google Fonts에 표시된 영문 이름 복사, 코드 영역에는 &quot;코드 영역은 monospace 폰트로 유지해 줘&quot;를 추가하면 더 개발자다운 느낌이 난다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt; 프롬프트: 아이콘 변경&amp;gt;&lt;/b&gt;&lt;br&gt;사전&amp;nbsp;작업:&amp;nbsp;&lt;a href=&quot;https://fonts.google.com/icons에서&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://fonts.google.com/icons에서&lt;/span&gt;&lt;/a&gt;&amp;nbsp;원하는&amp;nbsp;아이콘을&amp;nbsp;영문으로&amp;nbsp;검색&amp;nbsp;→&amp;nbsp;이름&amp;nbsp;복사&lt;br&gt;아이콘 이름 의미 추천 섹션&lt;br&gt;code 코드·개발 기술&amp;nbsp;스택&lt;br&gt;terminal 터미널 소개·히어로&lt;br&gt;folder_open 프로젝트&amp;nbsp;폴더 프로젝트&lt;br&gt;school 학력·교육 학력&amp;nbsp;섹션&lt;br&gt;rocket_launch 목표·성장 목표&amp;nbsp;섹션&lt;br&gt;alternate_email 연락처 연락처&amp;nbsp;섹션&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt;프롬프트: 프로필 사진 삽입&amp;gt;&lt;/b&gt;&lt;br&gt;사전&amp;nbsp;작업:&amp;nbsp;&lt;a href=&quot;https://imgbb.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://imgbb.com&lt;/span&gt;&lt;/a&gt; 접속 → 사진 업로드 → Direct link(i.ibb.co/...로 시작하는 URL) 복사. Canvas는 파일을 직접 저장할 수 없으므로 외부 이미지 URL이 필요하다.&lt;br&gt;(이미지&amp;nbsp;URL)&amp;nbsp;이&amp;nbsp;사진을&amp;nbsp;히어로&amp;nbsp;섹션에&amp;nbsp;프로필&amp;nbsp;사진으로&amp;nbsp;넣어줘.&lt;br&gt;원형으로 잘라서 표시해줘. &amp;nbsp;&quot;원형으로 잘라서&quot;를 명시하면 CSS border-radius: 50%가 자동으로 적용된다. 이미지가 X 표시로 나오면 imgbb에서 Direct link(i.ibb.co로 시작하는 URL)를 복사했는지 확인. 'Viewer link'나 'HTML code'를 복사하면 이미지가 깨짐&lt;br&gt;* URL이 i.ibb.co로 시작하는지, Direct link를 복사했는지 확인&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt;시각 효과 단계 — 히어로 섹션 꾸미기&amp;gt;&lt;/b&gt;&lt;br&gt;히어로 섹션에 시각적 효과를 추가해 포트폴리오의 첫인상을 높인다.&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt;프롬프트: 시각 효과 추천받기&amp;gt;&lt;/b&gt;&lt;br&gt;바로 적용하지 말고 먼저 여러 옵션을 받아 비교해서 선택할 수도 있다. 개발자 포트폴리오답게 멋진 효과가 없을까? 5개 정도 추천해줘. 개발자 포트폴리오에 잘 어울리는 효과가 따로 있다.&lt;br&gt;※ 파티클 네트워크(연결된 점들이 움직이는 효과), 타이핑 효과(직함이 한 글자씩 타이핑되는 효과), 코드 빗줄기(매트릭스처럼 코드가 떨어지는 효과)는 개발자 포트폴리오에서 자주 보이는 패턴. &quot;개발자 포트폴리오답게&quot;라는 표현을 프롬프트에 넣으면 이런 방향으로 추천해 준다.&amp;nbsp;&lt;br&gt;그러나 개인이 창의적인 화면 구성과 디자인을 할 수도 있다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt;프롬프트: 효과 1개 적용, 조합, 교체하기&amp;gt;&lt;/b&gt;&lt;br&gt;다른 효과로 바꿀 수 있고, 의도가 분명하게 전달되도록 &quot;유지&quot;, &quot;추가&quot;, &quot;빼고&quot;를 프롬프트에 포함한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt; 인터랙션 단계 — 문의 폼 연결하기&amp;gt;&lt;/b&gt;&lt;br&gt;→ 방문자(채용 담당자·동료 개발자)가 실제로 메시지를 남길 수 있는 구글 폼을 만들고 버튼에 연결한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;lt;프롬프트: Canvas 안에 연락 양식 추가&amp;gt;&lt;/b&gt;&lt;br&gt;방문자가&amp;nbsp;채용&amp;nbsp;제안이나&amp;nbsp;협업&amp;nbsp;요청을&amp;nbsp;남길&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;연락&amp;nbsp;양식을&amp;nbsp;만들어줘.&lt;br&gt;이름, 소속(회사명 또는 학교), 이메일, 문의 유형(채용 제안 / 프로젝트 협업 / 기타), 메시지를 받을수 있게 해.&lt;br&gt;이 단계에서 만들어진 폼은 화면에만 존재하는 디자인 모형, 실제 응답은 수집되지 않는다. 보기 좋은 입력 칸을 갖추는 것이 목적이고, 실제로 메시지를 받는 일은 구글 폼이 담당함. 이 화면의 '연락하기' 버튼을 구글 폼과 연결하면, 모형 폼은 디자인 요소로 남고 실제 접수는 구글 폼으로 처리하게 된다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;lt;Google Forms 만들기 (Canvas 밖 작업)&amp;gt;&lt;br&gt;이 단계는 구글폼에 빈양식을 사용해도 되지만 &lt;b&gt;별도로 갖고 있는 양식이 있다면 구글독스가 있다면 모두 가능&lt;/b&gt;하다.&lt;br&gt;&lt;a href=&quot;https://forms.google.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://forms.google.com&lt;/span&gt;&lt;/a&gt; 접속 '빈 양식' 선택후 내용 작성, 게시 클릭&lt;br&gt;구글 폼을 만든 뒤 반드시 본인이 직접 한 번 제출(게시)해서 응답이 저장되는지 확인. 채용 담당자가 연락을 보냈는데 알림을 못 받으면 기회를 놓칠 수 있다. 응답 탭에서 '이메일 알림 받기'를 꼭 체크해 두기 ( 복사한 구글 폼 URL을&amp;nbsp; 프롬프트 요청시 사용)&lt;br&gt;&lt;br&gt;&lt;b&gt;&amp;lt;프롬프트: 버튼에 구글 폼 링크 연결&amp;gt;&lt;/b&gt;&lt;br&gt;히어로섹션에 있는 '연락하기' 버튼에 구글 폼 링크를 걸어줘. 링크는&amp;nbsp;&lt;a href=&quot;https://forms.gle/(복사한&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://forms.gle/(복사한&lt;/span&gt;&lt;/a&gt;&amp;nbsp;주소)&amp;nbsp;이걸로&amp;nbsp;새&amp;nbsp;창에서&amp;nbsp;열리게&amp;nbsp;해줘.&lt;br&gt;&amp;nbsp;&quot;새 창에서 열리게&quot;를 꼭 명시하. 이 한 마디로 버튼 클릭 시 포트폴리오 사이트에서 벗어나지 않도록(target=&quot;_blank&quot;) 처리됨&lt;br&gt;채용 담당자가 폼을 작성하다 뒤로 가기를 눌러도 포트폴리오로 돌아올 수 있어야 하기 때문이다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;★ &amp;lt;html 코드를 웹사이트 주소 연결하기&amp;gt; ★&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;a href=&quot;https://www.app.netlify.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://www.app.netlify.com&lt;u&gt;&amp;nbsp;꼭 &quot;&quot;로그인&quot;&quot; 해서 &lt;/u&gt;&lt;/span&gt;&lt;/a&gt;생성한 웹페이지를 구글 사이트에 게시, &lt;b&gt;윈도우든 리눅스든 &quot;로그인&quot;만 되어 있다면 파일을&amp;nbsp; &quot;끌어다 놓기&quot;를 하면 웹사이트 공유링크 주소를 받을 수 있다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orvHk/dJMcacKlfIP/MWjWSx9elPZtNvgkGjWSUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orvHk/dJMcacKlfIP/MWjWSx9elPZtNvgkGjWSUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orvHk/dJMcacKlfIP/MWjWSx9elPZtNvgkGjWSUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForvHk%2FdJMcacKlfIP%2FMWjWSx9elPZtNvgkGjWSUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;774&quot; height=&quot;378&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;넷리파이에 로그인 하여 시작에서 배포 &quot;마법사&quot;를 시작한 화면&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5YsOo/dJMcacwLFHO/hnAHH0gokQ2UDnCZxaoAB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5YsOo/dJMcacwLFHO/hnAHH0gokQ2UDnCZxaoAB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5YsOo/dJMcacwLFHO/hnAHH0gokQ2UDnCZxaoAB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5YsOo%2FdJMcacwLFHO%2FhnAHH0gokQ2UDnCZxaoAB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;372&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;834&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;저장한 인덱트.html 파일을 끌어다 놓기 화면&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpgW9e/dJMcacwLFHH/7Kmg3NejSk541hGo0QBBDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpgW9e/dJMcacwLFHH/7Kmg3NejSk541hGo0QBBDk/img.png&quot; data-alt=&quot;파일 끌어다 놓기 후 배포가 끝나면 초록색으로 링크 주소를 표시한 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpgW9e/dJMcacwLFHH/7Kmg3NejSk541hGo0QBBDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpgW9e%2FdJMcacwLFHH%2F7Kmg3NejSk541hGo0QBBDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;732&quot; height=&quot;150&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일 끌어다 놓기 후 배포가 끝나면 초록색으로 링크 주소를 표시한 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;※ 위에서 배포한 웹사이트 링크주소를 복사해서 크롬 주소창에 붙이면, 웹사이트가 열린다. 또는 링크 주소를 클릭해도 된다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;figure data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;011&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/011.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/011.gif&quot; width=&quot;150&quot;&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;◆ &lt;/b&gt;&lt;b&gt;◆&lt;/b&gt;&lt;b&gt;&amp;nbsp; 이것마져도 번거롭고 어렵다고 생각한다면 러버블(&lt;a href=&quot;https://lovable.dev/ko&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://lovable.dev/ko)에서 바이브코딩 후 배포와 웹사이트 주소까지 한번에 받을 수 있으며, &lt;span style=&quot;color: #000000;&quot;&gt;가비아&lt;/span&gt; (&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://www.gabia.com/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://www.gabia.com/&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://lovable.dev/ko&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;) 에서 도메인 주소를 구입 (1년씩 사용 가능)하여 웹사이트를 운영할 수도 있다. &lt;/span&gt;&lt;/a&gt; &lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biM4aW/dJMcabxOwzl/RPTSX8JOLr8pWKIKGYrTS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biM4aW/dJMcabxOwzl/RPTSX8JOLr8pWKIKGYrTS1/img.png&quot; data-alt=&quot;생성형 이미지를 활용한 화면 (예시)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biM4aW/dJMcabxOwzl/RPTSX8JOLr8pWKIKGYrTS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiM4aW%2FdJMcabxOwzl%2FRPTSX8JOLr8pWKIKGYrTS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;451&quot; data-origin-width=&quot;1271&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;생성형 이미지를 활용한 화면 (예시)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;u&gt;&amp;lt; 웹사이트 바이브코딩 코드 &amp;gt;&amp;nbsp; 실습단계에서는 실명이 아닌 &quot;&quot;가명&quot;&quot; 사용&amp;nbsp;&lt;/u&gt;&lt;/b&gt;&lt;br&gt;** 작성한 소스코드를 메모장에 저장하고 &lt;u&gt;파일명은 index.html&amp;nbsp;&lt;/u&gt; --&amp;gt; 넷리파이에 파일을 업로드해서 주소링크를 받는다&amp;nbsp; --&amp;gt;&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp;--&amp;gt; 주소를 크롬주소창에 붙이기 하면 웹사이트가 열린다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;※&amp;nbsp; API 키 발급 받는 방법&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;OpenAI (ChatGPT) :&lt;/b&gt;&amp;nbsp;&amp;nbsp;&quot;OpenAI 플랫폼&quot; 로그인 후 우측 상단의 프로필 메뉴에서&amp;nbsp;&lt;b&gt;Dashboard &amp;gt; API Keys&lt;/b&gt;로 이동하여 'Create new secret key'를 눌러 발급받을 수 있다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Google Gemini :&lt;/b&gt;&amp;nbsp;&quot;Google AI Studio&quot;에 접속하여 구글 계정으로 로그인한 뒤, 좌측 메뉴의&amp;nbsp;&lt;b&gt;API Keys&lt;/b&gt;&amp;nbsp;페이지에서 새 키를 생성할 수 있다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Google Cloud (일반 API, YouTube 등) :&lt;/b&gt;&amp;nbsp;Google Cloud 콘솔에 접속하여&amp;nbsp;&lt;b&gt;사용자 인증 정보 &amp;gt; 사용자 인증 정보 만들기 &amp;gt; API 키&lt;/b&gt;를 선택해 발급받을 수 있다.&lt;/li&gt;&lt;/ul&gt;&lt;div data-bfc=&quot;&quot; data-ved=&quot;2ahUKEwiFmcnz3LGVAxVKr1YBHUbQOrAQi4wTegoIAggACAAIBhAV&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt; 
 &lt;div data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACAYQFg&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
  &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;※ 보안 주의사항&lt;/b&gt;&lt;/span&gt;
  &lt;br&gt;&lt;b&gt;API 키는 서비스 이용 요금이 청구되거나 개인정보와 연결될 수 있는 강력한 비밀번호 역할을 하므로, 절대로 GitHub 등에 공개적으로 업로드하지 말고, 외부에 유출되지 않도록 안전하게 관리해야 한다.&lt;/b&gt;
 &lt;/div&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;※ &lt;span style=&quot;color: #006DD7;&quot;&gt;&quot;가명&quot;을 사용하여 실습&lt;/span&gt;하였고,&amp;nbsp; 키 미기재 (번호는 비밀보안 유지 필수) 채팅 구현내용 예시 실습&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;title&amp;gt;골든김(GoldenKim) - 개발팀 PM 포트폴리오&amp;lt;/title&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Tailwind CSS CDN --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;script src=&quot;https://cdn.tailwindcss.com&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Font Awesome for Icons --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Google Fonts --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;link href=&quot;https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&amp;amp;display=swap&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;style&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;body {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;font-family: 'Noto Sans KR', sans-serif;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;scroll-behavior: smooth;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;/* Custom scrollbar matching Golden theme */
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;::-webkit-scrollbar {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;width: 8px;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;::-webkit-scrollbar-track {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;background: #111827;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;::-webkit-scrollbar-thumb {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;background: #d97706;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;border-radius: 4px;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;::-webkit-scrollbar-thumb:hover {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;background: #f59e0b;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body class=&quot;bg-gray-950 text-gray-100 min-h-screen flex flex-col justify-between&quot;&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Navigation Bar --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;nav class=&quot;sticky top-0 z-50 bg-gray-950/90 backdrop-blur-md border-b border-amber-500/20 transition-all duration-300&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-6xl mx-auto px-4 sm:px-6 lg:px-8&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex items-center justify-between h-16&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex items-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#home&quot; class=&quot;text-xl font-black tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-yellow-500&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GOLDEN_KIM
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;hidden md:block&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;ml-10 flex items-baseline space-x-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#about&quot; class=&quot;text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-sm font-medium transition-colors&quot;&amp;gt;About Me&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#skills&quot; class=&quot;text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-sm font-medium transition-colors&quot;&amp;gt;Skills&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#projects&quot; class=&quot;text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-sm font-medium transition-colors&quot;&amp;gt;Projects&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#guestbook&quot; class=&quot;text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-sm font-medium transition-colors&quot;&amp;gt;Guestbook&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#ai-mentor&quot; class=&quot;bg-gradient-to-r from-amber-500 to-yellow-600 text-gray-950 px-4 py-2 rounded-full text-sm font-bold hover:opacity-90 transition-all shadow-md shadow-amber-500/10&quot;&amp;gt;AI Mentor Chat&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Mobile menu button --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;md:hidden&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button id=&quot;mobile-menu-btn&quot; class=&quot;text-amber-400 hover:text-amber-300 focus:outline-none p-2&quot; aria-label=&quot;메뉴 열기&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-bars text-xl&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Mobile Menu --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div id=&quot;mobile-menu&quot; class=&quot;hidden md:hidden bg-gray-950 border-b border-amber-500/20 px-4 pt-2 pb-4 space-y-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#about&quot; class=&quot;block text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-base font-medium&quot;&amp;gt;About Me&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#skills&quot; class=&quot;block text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-base font-medium&quot;&amp;gt;Skills&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#projects&quot; class=&quot;block text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-base font-medium&quot;&amp;gt;Projects&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#guestbook&quot; class=&quot;block text-gray-300 hover:text-amber-400 px-3 py-2 rounded-md text-base font-medium&quot;&amp;gt;Guestbook&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#ai-mentor&quot; class=&quot;block bg-gradient-to-r from-amber-500 to-yellow-600 text-gray-950 text-center px-4 py-2 rounded-md text-base font-bold&quot;&amp;gt;AI Mentor Chat&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/nav&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;main class=&quot;flex-grow&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Hero Section with Custom Background Image (17479.jpg) --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;home&quot; class=&quot;relative overflow-hidden py-24 sm:py-32 flex items-center justify-center min-h-[90vh] bg-cover bg-center&quot; style=&quot;background-image: url('17479.jpg');&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Dark Gradient Overlay for optimal readability --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;absolute inset-0 bg-gradient-to-b from-black/80 via-black/60 to-gray-950&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-4xl mx-auto px-4 text-center relative z-10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-400 border border-amber-500/30 mb-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-crown mr-1.5&quot;&amp;gt;&amp;lt;/i&amp;gt; 개발팀 PM 지망생 |&amp;nbsp;&amp;nbsp;name
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Matching the Text on the Uploaded Image --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h1 class=&quot;text-4xl sm:text-6xl font-black tracking-tight mb-6 leading-tight&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 via-amber-300 to-yellow-500&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;골든김 포트폴리오
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-2xl sm:text-4xl text-gray-200 font-bold block mt-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;기술과 사람을 연결하여 비즈니스 가치를&amp;nbsp;&amp;nbsp;창출하는 준비된 개발자PM 입니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/h1&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-base sm:text-lg text-gray-300 max-w-2xl mx-auto mb-10 leading-relaxed&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;교육기관 강사님의 탄탄한 실습 과정을 토대로 개발 역량을 내재화했습니다.&amp;lt;br class=&quot;hidden sm:inline&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;개발 흐름을 완벽히 이해하는 &amp;lt;strong&amp;gt;'개발팀 PM'&amp;lt;/strong&amp;gt;으로서, 프로젝트의 성공을 이끌 준비가 되었습니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex flex-col sm:flex-row justify-center items-center gap-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#projects&quot; class=&quot;w-full sm:w-auto px-8 py-3.5 bg-gradient-to-r from-amber-500 to-yellow-600 hover:from-amber-600 hover:to-yellow-700 text-gray-950 rounded-xl font-bold transition-all shadow-lg shadow-amber-500/20 text-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;프로젝트 분석 보기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#ai-mentor&quot; class=&quot;w-full sm:w-auto px-8 py-3.5 bg-gray-900/80 hover:bg-gray-800 text-amber-400 border border-amber-500/30 rounded-xl font-medium transition-colors text-center backdrop-blur-sm&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AI PM 역량 검증하기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- About Me Section --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;about&quot; class=&quot;py-20 border-t border-amber-500/10 bg-gray-950/70&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-5xl mx-auto px-4 sm:px-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-center mb-16&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h2 class=&quot;text-3xl font-bold tracking-tight text-white&quot;&amp;gt;About Me&amp;lt;/h2&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;mt-2 text-gray-400&quot;&amp;gt;개발 프로세스를 리드하는 예비 PM 골든김입니다.&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;grid grid-cols-1 md:grid-cols-12 gap-10 items-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;md:col-span-4 flex justify-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;relative group&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;absolute -inset-1 bg-gradient-to-r from-amber-500 to-yellow-500 rounded-2xl blur opacity-30 group-hover:opacity-50 transition duration-1000&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;relative w-64 h-64 bg-gray-900 rounded-2xl overflow-hidden border border-amber-500/20 flex flex-col items-center justify-center p-6 text-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-20 h-20 rounded-full bg-gradient-to-tr from-amber-400 to-yellow-600 flex items-center justify-center text-gray-950 text-3xl font-black mb-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SG
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-xl font-bold text-white&quot;&amp;gt;골든김 (goodgob)&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-sm text-amber-400 font-semibold mt-1&quot;&amp;gt;개발팀 PM 지망생&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex space-x-3 mt-4 text-gray-400&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;https://github.com/goodgob&quot; target=&quot;_blank&quot; class=&quot;hover:text-amber-400 transition-colors&quot; aria-label=&quot;GitHub&quot;&amp;gt;&amp;lt;i class=&quot;fa-brands fa-github text-xl&quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;#&quot; class=&quot;hover:text-amber-400 transition-colors&quot; aria-label=&quot;Blog&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-blog text-xl&quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;mailto:goodgob@gmail.com&quot; class=&quot;hover:text-amber-400 transition-colors&quot; aria-label=&quot;Email&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-envelope text-xl&quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;md:col-span-8 space-y-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-300 to-yellow-400&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;코드의 메커니즘을 완벽히 이해하는 프로젝트 매니저&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-gray-300 leading-relaxed&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;단순히 일정을 조율하는 PM을 넘어, 개발 팀원들과 편안하게한 의견을 나누고 최선의 기술 의사결정을 보조할 수 있는 '테크니컬 PM'으로 나아가고 있습니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-4 rounded-xl bg-gray-900/50 border border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-amber-400 font-bold mb-1&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-network-wired mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt;Tech PM 역량&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-sm text-gray-400&quot;&amp;gt;API 아키텍처 이해, UI/UX 설계, JSON 데이터 조작 능력 수립&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-4 rounded-xl bg-gray-900/50 border border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-yellow-400 font-bold mb-1&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-chess-knight mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt;핵심 가치&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-sm text-gray-400&quot;&amp;gt;명확한 문서화, 기획의 실현 가능성 검토, 신속한 트러블슈팅&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Skills Section --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;skills&quot; class=&quot;py-20 border-t border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-5xl mx-auto px-4 sm:px-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-center mb-16&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h2 class=&quot;text-3xl font-bold tracking-tight text-white&quot;&amp;gt;Skills Matrix&amp;lt;/h2&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;mt-2 text-gray-400&quot;&amp;gt;PM 지망생으로서 보유한 기술 및 협업 스택입니다.&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;grid grid-cols-1 md:grid-cols-3 gap-8&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- PM &amp;amp; Management --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-6 rounded-2xl bg-gray-900/30 border border-amber-500/10 hover:border-amber-500/30 transition-all duration-300&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center text-amber-400 mb-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-list-check text-2xl&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-xl font-bold mb-4&quot;&amp;gt;Project Management&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;ul class=&quot;space-y-3 text-gray-400&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; 요구사항 정의서(PRD) 작성&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; 애자일/스크럼 프레임워크 이해&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; 프로그램 연동 UI/UX 설계 리드&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/ul&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Development (Technical) --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-6 rounded-2xl bg-gray-900/30 border border-amber-500/10 hover:border-yellow-500/30 transition-all duration-300&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-12 h-12 rounded-xl bg-yellow-500/10 flex items-center justify-center text-yellow-400 mb-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-code text-2xl&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-xl font-bold mb-4&quot;&amp;gt;Tech Specs&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;ul class=&quot;space-y-3 text-gray-400&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-yellow-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; JS ES6+ 및 비동기 처리(Fetch)&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-yellow-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; HTML5/CSS3 (Tailwind 반응형)&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-yellow-500 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; Firebase &amp;amp; LocalStorage 설계&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/ul&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Collaboration --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-6 rounded-2xl bg-gray-900/30 border border-amber-500/10 hover:border-amber-400/30 transition-all duration-300&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-12 h-12 rounded-xl bg-amber-400/10 flex items-center justify-center text-amber-300 mb-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-comments text-2xl&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-xl font-bold mb-4&quot;&amp;gt;Collaboration&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;ul class=&quot;space-y-3 text-gray-400&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-400 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; Git / GitHub 일정 브랜치 관리&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-400 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; Slack / Notion 연동 워크스페이스&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;li class=&quot;flex items-center&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-circle-check text-amber-400 mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; 개발 병목 해결 커뮤니케이션&amp;lt;/li&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/ul&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Projects Section (개발실습 응용 및 관리 관점) --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;projects&quot; class=&quot;py-20 border-t border-amber-500/10 bg-gray-950/70&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-5xl mx-auto px-4 sm:px-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-center mb-16&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h2 class=&quot;text-3xl font-bold tracking-tight text-white&quot;&amp;gt;Projects&amp;lt;/h2&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;mt-2 text-gray-400&quot;&amp;gt;실습을 바탕으로 PM의 눈으로 확장/고도화한 핵심 프로젝트입니다.&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-8&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Project 1 --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;group bg-gray-900/40 rounded-2xl overflow-hidden border border-amber-500/10 hover:border-amber-500/30 transition-all flex flex-col justify-between&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-xs font-semibold text-amber-400 uppercase tracking-wider&quot;&amp;gt;PM &amp;amp; 개발 리포트 포함&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-2xl font-bold text-white mt-2 mb-3&quot;&amp;gt; 업무 관리 시스템 내부용 프로그램 &amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-gray-400 text-sm leading-relaxed mb-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;수업 시간에 학습한 &quot;&quot;&quot;수정중&quot;&quot;&quot;&quot;&amp;nbsp;&amp;nbsp; 전 단계의 일정을 직접 조율했습니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex flex-wrap gap-2 mb-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-amber-400 rounded-md&quot;&amp;gt;JS Fetch API&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-amber-400 rounded-md&quot;&amp;gt;Responsive UI&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-amber-400 rounded-md&quot;&amp;gt;Schedule Control&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;px-6 pb-6 pt-2 border-t border-amber-500/10 flex justify-between items-center bg-gray-950/40&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-xs text-gray-500&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-calendar mr-1&quot;&amp;gt;&amp;lt;/i&amp;gt; PM 겸 개발 (2주)&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;https://github.com/goodgob&quot; target=&quot;_blank&quot; class=&quot;text-sm font-medium text-amber-400 hover:text-amber-300 flex items-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;View PRD &amp;lt;i class=&quot;fa-solid fa-arrow-right ml-1&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Project 2 --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;group bg-gray-900/40 rounded-2xl overflow-hidden border border-amber-500/10 hover:border-amber-500/30 transition-all flex flex-col justify-between&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;p-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-xs font-semibold text-yellow-400 uppercase tracking-wider&quot;&amp;gt;기획 및 관리&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-2xl font-bold text-white mt-2 mb-3&quot;&amp;gt;&quot;&quot; 수정중 &quot;&quot;&quot;&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-gray-400 text-sm leading-relaxed mb-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot; 수정중&quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ....... 증대시킬 수 있는 시나리오를 설계했습니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex flex-wrap gap-2 mb-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-yellow-400 rounded-md&quot;&amp;gt;Vanilla JS&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-yellow-400 rounded-md&quot;&amp;gt;Local Storage&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;px-2.5 py-1 text-xs bg-gray-800 text-yellow-400 rounded-md&quot;&amp;gt;Kanban UX&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;px-6 pb-6 pt-2 border-t border-amber-500/10 flex justify-between items-center bg-gray-950/40&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-xs text-gray-500&quot;&amp;gt;&amp;lt;i class=&quot;fa-solid fa-calendar mr-1&quot;&amp;gt;&amp;lt;/i&amp;gt; PM 기획 (1주)&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;https://github.com/goodgob&quot; target=&quot;_blank&quot; class=&quot;text-sm font-medium text-yellow-400 hover:text-yellow-300 flex items-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;View PRD &amp;lt;i class=&quot;fa-solid fa-arrow-right ml-1&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/a&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Interactive Guestbook Section --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;guestbook&quot; class=&quot;py-20 border-t border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-4xl mx-auto px-4 sm:px-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-center mb-16&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h2 class=&quot;text-3xl font-bold tracking-tight text-white&quot;&amp;gt;Guestbook&amp;lt;/h2&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;mt-2 text-gray-400&quot;&amp;gt;방문 소감 또는 PM 골든김에게 제안하고 싶은 내용을 남겨주세요!&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/40 rounded-2xl p-6 border border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;form id=&quot;guestbook-form&quot; class=&quot;space-y-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;grid grid-cols-1 sm:grid-cols-2 gap-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;label class=&quot;block text-xs font-semibold text-amber-400 uppercase mb-2&quot;&amp;gt;닉네임&amp;lt;/label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;input type=&quot;text&quot; id=&quot;guestbook-name&quot; required placeholder=&quot;예: 채용 담당자&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class=&quot;w-full bg-gray-950 border border-gray-800 rounded-lg px-4 py-2.5 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;label class=&quot;block text-xs font-semibold text-amber-400 uppercase mb-2&quot;&amp;gt;소속 (직위)&amp;lt;/label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;input type=&quot;text&quot; id=&quot;guestbook-title&quot; placeholder=&quot;예: OO 테크 기업 개발 본부장&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class=&quot;w-full bg-gray-950 border border-gray-800 rounded-lg px-4 py-2.5 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;label class=&quot;block text-xs font-semibold text-amber-400 uppercase mb-2&quot;&amp;gt;남기고 싶은 조언 / 응원&amp;lt;/label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;textarea id=&quot;guestbook-msg&quot; required rows=&quot;3&quot; placeholder=&quot;예비 PM 골든김 개발자에게 피드백을 전달해 주세요.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class=&quot;w-full bg-gray-950 border border-gray-800 rounded-lg px-4 py-2.5 text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex justify-end&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button type=&quot;submit&quot; class=&quot;px-6 py-2.5 bg-gradient-to-r from-amber-500 to-yellow-600 hover:from-amber-600 hover:to-yellow-700 text-gray-950 font-bold rounded-lg transition-colors flex items-center shadow-lg shadow-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-paper-plane mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt; 방명록 남기기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/form&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Messages Container --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;mt-10 border-t border-amber-500/10 pt-8&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-lg font-bold mb-4 text-amber-400&quot;&amp;gt;Recent Messages&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div id=&quot;guestbook-list&quot; class=&quot;space-y-4 max-h-96 overflow-y-auto pr-2&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- JS를 통해 렌더링 --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- AI Mentor Chatbot (Gemini API Integration) --&amp;gt;&amp;nbsp;&amp;nbsp;// 멘토 AI채봇 제미나이 API 통합
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;section id=&quot;ai-mentor&quot; class=&quot;py-20 border-t border-amber-500/10 bg-gray-950/70&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-4xl mx-auto px-4 sm:px-6&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;text-center mb-10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-400 border border-amber-500/20 mb-3&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Gemini 2.5 Flash Engine&amp;nbsp;&amp;nbsp;// 제미나이 2.5 플래시 엔진 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h2 class=&quot;text-3xl font-bold tracking-tight text-white&quot;&amp;gt;AI 채용 면접 멘토링 Chatbot&amp;lt;/h2&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;mt-2 text-gray-400&quot;&amp;gt;골든김(goodgob) 예비 PM의 기술력과 관리 역량에 대해 AI와 실시간으로 이야기 나눠보세요.&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/30 rounded-2xl border border-amber-500/10 flex flex-col h-[520px] overflow-hidden shadow-2xl&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Chat Header --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;px-6 py-4 border-b border-amber-500/10 bg-gray-950/60 flex items-center justify-between&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex items-center space-x-3&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-10 h-10 rounded-full bg-gradient-to-tr from-amber-400 to-yellow-600 flex items-center justify-center text-gray-950 font-black&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-crown text-gray-950&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h4 class=&quot;font-bold text-sm text-white&quot;&amp;gt;AI PM 채용 가이드&amp;lt;/h4&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-xs text-amber-400 flex items-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;w-1.5 h-1.5 bg-amber-400 rounded-full mr-1 animate-pulse&quot;&amp;gt;&amp;lt;/span&amp;gt; Online (Gemini Powered)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button id=&quot;clear-chat-btn&quot; class=&quot;text-xs text-gray-500 hover:text-amber-400 transition-colors&quot; title=&quot;대화 초기화&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-arrow-rotate-left&quot;&amp;gt;&amp;lt;/i&amp;gt; 리셋
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Chat Messages Area --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div id=&quot;chat-messages&quot; class=&quot;flex-grow overflow-y-auto p-6 space-y-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Bot Greeting --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex items-start space-x-3&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-8 h-8 rounded-full bg-amber-950/50 flex items-center justify-center text-amber-400 flex-shrink-0 mt-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-robot text-xs&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/60 rounded-2xl rounded-tl-none px-4 py-2.5 text-sm text-gray-200 max-w-[80%] leading-relaxed border border-amber-500/5&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;안녕하세요! &amp;lt;strong&amp;gt;개발팀 PM 지망생 골든김(goodjob)&amp;lt;/strong&amp;gt; 님의 채용 안내를 도와드리는 AI 비서입니다.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;골든김 개발자가 이수한 실습, 다룰 줄 아는 기술, 프로젝트 일정 조율 및 기획 역량이 회사에 어떻게 기여할 수 있는지 물어보세요!  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Quick Suggestions --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;px-6 py-2 bg-gray-950/40 border-t border-amber-500/5 flex flex-wrap gap-2 overflow-x-auto whitespace-nowrap&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button class=&quot;chat-suggest-btn text-xs bg-gray-900 hover:bg-gray-800 text-gray-300 px-3 py-1.5 rounded-full border border-gray-800 transition-colors&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;  개발자 골든김이 'PM'으로서 가지는 최고의 강점은?
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button class=&quot;chat-suggest-btn text-xs bg-gray-900 hover:bg-gray-800 text-gray-300 px-3 py-1.5 rounded-full border border-gray-800 transition-colors&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 0000 Todo 앱은 PM 관점에서 어떤 가치가 있나요?
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button class=&quot;chat-suggest-btn text-xs bg-gray-900 hover:bg-gray-800 text-gray-300 px-3 py-1.5 rounded-full border border-gray-800 transition-colors&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ️ 비동기 통신 및 데이터 바인딩 실습 수준이 어느 정도인가요?
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Input Box --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;form id=&quot;chat-form&quot; class=&quot;p-4 bg-gray-950/50 border-t border-amber-500/10 flex items-center space-x-3&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;input type=&quot;text&quot; id=&quot;chat-input&quot; placeholder=&quot;골든김 예비 PM의 역량에 대해 질문해 보세요...&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class=&quot;flex-grow bg-gray-900 border border-gray-800 rounded-xl px-4 py-3 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;button type=&quot;submit&quot; id=&quot;chat-submit-btn&quot; class=&quot;w-11 h-11 bg-gradient-to-r from-amber-500 to-yellow-600 hover:opacity-90 text-gray-950 rounded-xl flex items-center justify-center transition-opacity&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-paper-plane text-sm&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/button&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/form&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/section&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/main&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- Footer --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;footer class=&quot;bg-gray-950 border-t border-amber-500/10 py-12&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;max-w-6xl mx-auto px-4 text-center&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;h3 class=&quot;text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-yellow-500 mb-4&quot;&amp;gt;GOLDEN_KIM PM Portfolio&amp;lt;/h3&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-sm text-gray-500&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp;copy; 2026. All rights reserved. Designed &amp;amp; Coded by 골든김(goodgob). // 모든 권리보유 골든김
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;mt-4 text-xs text-gray-600 flex justify-center space-x-4&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span&amp;gt;Made for Instructor Practice Extension&amp;lt;/span&amp;gt; // 강사 실습 확장용으로 제작됨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span&amp;gt;•&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span&amp;gt;Powered by Gemini 2.5 Flash API&amp;lt;/span&amp;gt; // 제미나이 2.5 플래쉬 API로 구동됨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/footer&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;!-- JavaScript logic --&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;script&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// DOM Elements
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const mobileMenuBtn = document.getElementById('mobile-menu-btn');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const mobileMenu = document.getElementById('mobile-menu');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const guestbookForm = document.getElementById('guestbook-form');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const guestbookList = document.getElementById('guestbook-list');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const chatForm = document.getElementById('chat-form');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const chatInput = document.getElementById('chat-input');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const chatMessages = document.getElementById('chat-messages');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const clearChatBtn = document.getElementById('clear-chat-btn');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const suggestBtns = document.querySelectorAll('.chat-suggest-btn');

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Gemini API Configuration
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const apiKey = &quot;&quot;; // API key is provided by runtime environment&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// API키 런타임 환경에서 제공 활용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const modelName = &quot;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// System instruction template tailored to 골든김 (goodgob)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const portfolioSystemInstruction = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[개발자 골든김(goodgob) 정보]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 이름: 골든김 (goodgob)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 역할: 개발팀 PM 지망생 (Technical Project Manager / Product Owner 지망)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 핵심 스택: 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 교육: 강사 주도 웹개발 종합 부트캠프 준비 (JSON 데이터 파싱, 비동기 통신 Fetch API, 브라우저 클라이언트 데이터 스토리지 활용 등을 탄탄하게 이수하며 개발 프로세스의 원천 이해) &quot;&quot; 학습중&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[주요 강점]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1. 뛰어난 의사소통 능력: 개발자와 기획자 사이의 언어적 병목을 해결할 수 있는 기술적 이해도 확보.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2. 문서화 역량: 비동기 처리 실습을 확장하여 기능 요구사항 정의서(PRD) 및 QA 명세서를 꼼꼼하게 도출할 수 있음.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[프로젝트 성과]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1. 업무관리 시스템 내부망 사용 (수업 시간의 API/JSON 비동기 실습 확장)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 기술: 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 내용: 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2. 인터랙티브 웹 Todo 오거나이저 (수업 시간의 로컬스토리지 실습 확장)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 기술: 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 내용: 

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[답변 규칙]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1. &quot;골든김 님은 개발팀 PM으로서 기술에 대한 탄탄한 이해를 기반으로 협업을 이끕니다.&quot; 와 같은 비서의 조근조근하면서도 핵심을 찌르는 톤을 유지해줘.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2. 답변은 항상 친근한 한국어로 작성하며, 필요시 이모티콘( ,  ,  ️)을 적절히 활용해줘.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;3. 없는 내용을 과장하지 말고, 신입 PM이지만 개발 전반에 대한 높은 이해와 철저한 성실성(골든김)을 장점으로 호소해줘.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Mobile Menu Toggle
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mobileMenuBtn.addEventListener('click', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mobileMenu.classList.toggle('hidden');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Mock Firestore/DB Virtual Guestbook Storage (Safe In-Memory / Local Storage Fallback)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let guestbookData = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{ id: 1, name: &quot;강사님&quot;, title: &quot;부트캠프 대표 멘토&quot;, msg: &quot;골든김 님의 실습 이해도와 프로젝트 일정 관리 능력은 타의 추종을 불허했습니다. 개발자들과 매끄럽게 호흡하며 프로젝트를 무조건 성공시킬 명품 PM감입니다!&quot;, date: &quot;2026-06-18 10:11&quot; },
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{ id: 2, name: &quot;정태우&quot;, title: &quot;수석 개발 팀장&quot;, msg: &quot;배경 화면의 화려하고 견고한 황금빛 트리처럼, 팀의 기둥 역할을 든든하게 해 주실 것 같습니다. 기획서 완성도가 높네요. 조만간 면접에서 뵙길 바랍니다.&quot;, date: &quot;2026-06-18 15:30&quot; }
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;];

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Load guestbook items
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;function renderGuestbook() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const saved = localStorage.getItem('goodgob_guestbook');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (saved) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookData = JSON.parse(saved);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookList.innerHTML = '';
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookData.slice().reverse().forEach(item =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const div = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;div.className = &quot;bg-gray-950/80 border border-amber-500/10 p-4 rounded-xl flex flex-col space-y-2 transition-transform hover:scale-[1.01]&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;div.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex justify-between items-start&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;font-bold text-amber-100 text-sm&quot;&amp;gt;${escapeHTML(item.name)}&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;${item.title ? `&amp;lt;span class=&quot;ml-2 text-xs text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full&quot;&amp;gt;${escapeHTML(item.title)}&amp;lt;/span&amp;gt;` : ''}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;text-xs text-gray-500&quot;&amp;gt;${item.date}&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;p class=&quot;text-sm text-gray-400 leading-relaxed&quot;&amp;gt;${escapeHTML(item.msg)}&amp;lt;/p&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookList.appendChild(div);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// HTML Escape Helper
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;function escapeHTML(str) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return str.replace(/[&amp;amp;&amp;lt;&amp;gt;'&quot;]/g,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tag =&amp;gt; ({ '&amp;amp;': '&amp;amp;amp;', '&amp;lt;': '&amp;amp;lt;', '&amp;gt;': '&amp;amp;gt;', &quot;'&quot;: '&amp;amp;#39;', '&quot;': '&amp;amp;quot;' }[tag] || tag)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Guestbook Submit
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookForm.addEventListener('submit', (e) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;e.preventDefault();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const name = document.getElementById('guestbook-name').value.trim();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const title = document.getElementById('guestbook-title').value.trim();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const msg = document.getElementById('guestbook-msg').value.trim();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!name || !msg) return;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const now = new Date();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const dateStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const newItem = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;id: Date.now(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;msg,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;date: dateStr
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookData.push(newItem);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;localStorage.setItem('goodgob_guestbook', JSON.stringify(guestbookData));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderGuestbook();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;guestbookForm.reset();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;showToast(&quot;  골든김 개발자에게 전달할 방명록이 정상적으로 등록되었습니다!&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Friendly Custom Toast Message
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;function showToast(message) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const toast = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;toast.className = &quot;fixed bottom-5 right-5 z-50 bg-amber-600 text-gray-950 px-5 py-3 rounded-xl shadow-2xl flex items-center space-x-2 font-bold animate-bounce&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;toast.innerHTML = `&amp;lt;i class=&quot;fa-solid fa-circle-check&quot;&amp;gt;&amp;lt;/i&amp;gt; &amp;lt;span class=&quot;text-sm&quot;&amp;gt;${message}&amp;lt;/span&amp;gt;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;document.body.appendChild(toast);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setTimeout(() =&amp;gt; toast.remove(), 3000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// AI Chat System with Gemini API&amp;nbsp;&amp;nbsp; // 제미나이 API를 사용한 채팅 시스템
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;async function fetchGeminiAIResponse(userMessage) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const payload = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;contents: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parts: [{ text: userMessage }]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;systemInstruction: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parts: [{ text: portfolioSystemInstruction }]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;let delay = 1000;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (let i = 0; i &amp;lt; 5; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const response = await fetch(endpoint, {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;method: 'POST',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;'Content-Type': 'application/json'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;body: JSON.stringify(payload)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (response.ok) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const data = await response.json();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return data.candidates?.[0]?.content?.parts?.[0]?.text || &quot;죄송합니다, 답변을 가져오는 과정에 지연이 발생했습니다. 다시 눌러주세요.&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (err) {}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;await new Promise(resolve =&amp;gt; setTimeout(resolve, delay));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay *= 2;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new Error(&quot;API Exception&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Handle sending message
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;async function sendMessage(text) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!text.trim()) return;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1. Render User Message
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const userMsgDiv = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;userMsgDiv.className = &quot;flex items-start justify-end space-x-3&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;userMsgDiv.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-amber-500 text-gray-950 rounded-2xl rounded-tr-none px-4 py-2.5 text-sm max-w-[80%] font-medium leading-relaxed shadow-md shadow-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;${escapeHTML(text)}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.appendChild(userMsgDiv);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.scrollTop = chatMessages.scrollHeight;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 2. Render Loading/Typing Indicator
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const loadingDiv = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;loadingDiv.className = &quot;flex items-start space-x-3&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;loadingDiv.id = &quot;chat-loading-indicator&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;loadingDiv.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-8 h-8 rounded-full bg-amber-950/50 flex items-center justify-center text-amber-400 flex-shrink-0 mt-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-robot text-xs animate-spin&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/60 rounded-2xl rounded-tl-none px-4 py-2.5 text-sm text-gray-400 max-w-[80%] flex items-center space-x-1 border border-amber-500/5&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span&amp;gt;답변을 열심히 정제하고 있어요&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;animate-bounce&quot;&amp;gt;.&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;animate-bounce&quot; style=&quot;animation-delay: 0.2s&quot;&amp;gt;.&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;span class=&quot;animate-bounce&quot; style=&quot;animation-delay: 0.4s&quot;&amp;gt;.&amp;lt;/span&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.appendChild(loadingDiv);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.scrollTop = chatMessages.scrollHeight;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 3. Request API
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const aiResponse = await fetchGeminiAIResponse(text);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;document.getElementById('chat-loading-indicator').remove();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Render AI Response
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const botMsgDiv = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;botMsgDiv.className = &quot;flex items-start space-x-3&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;botMsgDiv.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-8 h-8 rounded-full bg-amber-950/50 flex items-center justify-center text-amber-400 flex-shrink-0 mt-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-robot text-xs&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/60 rounded-2xl rounded-tl-none px-4 py-2.5 text-sm text-gray-200 max-w-[80%] leading-relaxed border border-amber-500/10&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;${aiResponse.replace(/\n/g, '&amp;lt;br&amp;gt;')}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.appendChild(botMsgDiv);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.scrollTop = chatMessages.scrollHeight;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (error) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (document.getElementById('chat-loading-indicator')) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;document.getElementById('chat-loading-indicator').remove();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const errorDiv = document.createElement('div');
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;errorDiv.className = &quot;flex items-start space-x-3&quot;;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;errorDiv.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-8 h-8 rounded-full bg-red-950/50 flex items-center justify-center text-red-400 flex-shrink-0 mt-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-triangle-exclamation text-xs&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-red-950/10 border border-red-900/30 rounded-2xl rounded-tl-none px-4 py-2.5 text-sm text-red-300 max-w-[80%]&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;네트워크 연결이 지연되고 있습니다. 잠시 후 질문을 다시 보내 주시면 성실히 답변 드리겠습니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.appendChild(errorDiv);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.scrollTop = chatMessages.scrollHeight;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Send Chat Submit Event
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatForm.addEventListener('submit', (e) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;e.preventDefault();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const text = chatInput.value.trim();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!text) return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatInput.value = '';
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendMessage(text);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Click suggestions
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;suggestBtns.forEach(btn =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;btn.addEventListener('click', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const queryText = btn.textContent.replace(' ', '').replace(' ', '').replace(' ️', '').trim();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendMessage(queryText);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Clear Chat History
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clearChatBtn.addEventListener('click', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chatMessages.innerHTML = `
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;flex items-start space-x-3&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;w-8 h-8 rounded-full bg-amber-950/50 flex items-center justify-center text-amber-400 flex-shrink-0 mt-1&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;i class=&quot;fa-solid fa-robot text-xs&quot;&amp;gt;&amp;lt;/i&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div class=&quot;bg-gray-900/60 rounded-2xl rounded-tl-none px-4 py-2.5 text-sm text-gray-200 max-w-[80%] leading-relaxed border border-amber-500/5&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;대화 내용이 깔끔하게 초기화되었습니다. 골든김 개발자님에 대한 새로운 궁금증을 편하게 말씀해 주세요!
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;`;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Initial setup
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;window.addEventListener('DOMContentLoaded', () =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;renderGuestbook();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;});
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/69</guid>
      <comments>https://goodjop79.tistory.com/69#entry69comment</comments>
      <pubDate>Thu, 18 Jun 2026 23:53:22 +0900</pubDate>
    </item>
    <item>
      <title>D+49[Python] 메모 요약 앱 만들기 예제 실습</title>
      <link>https://goodjop79.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 17(수)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메모 요약앱 만들때 참고사항&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Qt 리소스(.qrc) 관리 및 변환&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgEEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;경로 설정&lt;/b&gt;: 리소스 파일에서 prefix=&quot;/icons&quot;와 file=&quot;icons/note.png&quot;를 조합하면 실제 참조 경로는 :/icons/icons/note.png&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgEEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;파이썬 변환&lt;/b&gt;: 터미널에서 아래 명령어를 실행하여 .qrc 파일을 파이썬 파일(.py)로 변환해야 소스코드에서 import하여 사용할 수 있다.&amp;nbsp; &amp;nbsp; &amp;nbsp;예) &lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;pyside6-rcc resources/resources.qrc -o resources/resources_rc.py&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2. 환경 설정 (settings.json)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgGEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;: 앱 실행 시 settings.json 파일 존재 여부와 형식을 확인하며, 오류가 있으면 기본값(DEFAULT_SETTINGS) 적용&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgGEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 설정 정보&lt;/b&gt;: 앱 제목, Ollama API 주소, 로컬 LLM 모델명(llama3.2:3b), 요약 항목 개수(3개)를 정의&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAIIAAgGEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;문법 주의&lt;/b&gt;: JSON 파일 내부 문자열은 반드시 큰따옴표(&quot;)를 사용해야 하며, 마지막 항목 뒤에는 쉼표(,)를 붙이지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;3. 앱 배포 (PyInstaller)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgIEAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;리소스 파일&lt;/b&gt;: 변환된 resources_rc.py는 파이썬 코드이므로 배포 시 자동으로 함께 빌드 됨&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgIEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;설정 파일&lt;/b&gt;: settings.json은 외부 파일이므로 배포할 때 파이썬 코드와 별도로 패키징에 포함해야 함.&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;4. 기본 배포 명령어&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;터미널에서 아래 명령어를 실행하면 앱이 하나의 폴더 형태로 빌드된다.&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;lnqSze_10,lnqSze_z&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;pre class=&quot;ada&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;code&gt;pyinstaller --noconsole main.py
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;--noconsole (또는 -w): 앱 실행 시 검은색 터미널(콘솔) 창이 뜨지 않도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAIBhAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;main.py: 실행 파일명 (실제 작성하신 메인 파일명으로 변경 가능)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;5. 외부 설정 파일(settings.json) 포함하기&lt;/b&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;resources_rc.py는 파이썬 코드라 자동으로 묶이지만, &lt;b&gt;settings.json은 외부 파일이므로 배포 폴더에 강제로 포함&lt;/b&gt;시켜야 함&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;※ 방법 A : 명령어로 한 번에 빌드하기 (--add-data 옵션)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Windows&lt;/b&gt;: pyinstaller --noconsole --add-data &quot;settings.json;.&quot; main.py&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAIChAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Mac / Linux&lt;/b&gt;: pyinstaller --noconsole --add-data &quot;settings.json:.&quot; main.py&lt;br /&gt;(주의: 윈도우는 세미콜론;, 맥/리눅스는 콜론:을 구분자로 사용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;※ 방법 B: .spec 파일 수정하기 (추천)&lt;/b&gt;&lt;br /&gt;한 번 빌드하면 생기는 main.spec 파일을 열어 datas 항목을 아래와 같이 수정하고 pyinstaller main.spec을 실행하기&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDBAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;lnqSze_2x,lnqSze_2w&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;pre class=&quot;ini&quot; data-copy-service-computed-style=&quot;font-family: monospace; font-size: 14px; font-weight: 400; margin: 14px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;&lt;code&gt;datas=[('settings.json', '.')],
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 3px 0px 0px; text-decoration: none; border-bottom: 1.25px solid rgb(240, 242, 245);&quot; data-sae=&quot;&quot; data-animation-atomic=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-ved=&quot;2ahUKEwiRxuWc1ZCVAxVNmq8BHWt4G-UQh9gSegYIAAgMEAI&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;6. 빌드 완료 후 확인 및 주의사항&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;결과물 위치&lt;/b&gt;: 빌드가 끝나면 dist/main/ 폴더가 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAIDhAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;실행 조건&lt;/b&gt;: 실행 파일(main.exe 등)과 settings.json&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt; 파일이 &lt;b&gt;같은 위치(동일 폴더)&lt;/b&gt;에 있어야 프로그램이 설정을 읽어온다.&lt;/span&gt;&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-18 20-30-52.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ta6Ed/dJMb997QS7o/vkeG45dhC7k5ZKo2ks7400/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ta6Ed/dJMb997QS7o/vkeG45dhC7k5ZKo2ks7400/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ta6Ed/dJMb997QS7o/vkeG45dhC7k5ZKo2ks7400/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fta6Ed%2FdJMb997QS7o%2FvkeG45dhC7k5ZKo2ks7400%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;834&quot; height=&quot;298&quot; data-filename=&quot;스크린샷 2026-06-18 20-30-52.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/68</guid>
      <comments>https://goodjop79.tistory.com/68#entry68comment</comments>
      <pubDate>Thu, 18 Jun 2026 20:28:32 +0900</pubDate>
    </item>
    <item>
      <title>D+49 [Project] 개인 프로젝트를 팀 통합파일로 만들기 실습</title>
      <link>https://goodjop79.tistory.com/66</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 17일(수)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 목 적&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 개인 프로젝트 패키지 파일들을 한데 모아 팀 통합파일로 만들어 실행해 보고, 개발 순서 역주행에 대한 문제점을 스스로 찾아보고 프로그램 개발 로직을 이해하기 위함이다.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 후기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 교육 49일째이다. 시간이 너무 빠르게 흘러 가는듯 하다. 파이썬에서 코드의 자동완성 기능이 Tab를 활용하여 많이 익숙해져 가고 있지만 아직도 개발자의 길은 갈길이 멀기도 하다. 그러나 흥미로움은 계속되고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 개인 프로젝트를 한 파일들을 가지고 하나로 통합 해서 팀 통합파일을 만드는 실습을 하였다. 어떠한 애로사항과 제한사항이 있는지 확인해 보고, 전체적인 프로그램 흐름 로직을 타인원들의 파일들도 확인하면서 토의하는 시간을 가졌다. 매우 유익한 시간이었다. 이에 사전 협의할 사항을 도출하면서 공부가 많이 되었다. 우선 생소한 용어들을 이해하게 되었고,&amp;nbsp; 프로그램 파일들을 하나로 통합하였을때 애러를 최소화 하려면 파일구조, 서버, 코드작성 부분에서 지켜야 할 약속 같은것, 통신규약, DB항목, 각종 산출물에 대한 공통사항 등 다양한 토의가 이뤄졌다. 특히 개발산출물을 인공지능 응용해서 생성하고자 할때 개발자의 생각과 로직이해가 선행되어야 한다는 것이다.&amp;nbsp;인공지능이 많이 보편화 되어 가고 있는 시점이지만 아직도 비용으로 인한 제한사항은 당장 해소하지 못하는 것이 현실이다. 이제는 개발자들에게도 인공지능 응용에 대한 수요가 증가할 수 있을 것이다. &quot;도전은 계속된다&quot;.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 팀 프로젝트 시행전 사전 조치 및 협의할 사항&lt;/b&gt;&lt;br /&gt;&lt;b&gt;1. 1. 사전 협의할 사항&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;① 파일 구조 : 파일경로 설정, 프로젝트명, 뷰(V), 모듈(M), 모델뷰(MV), 리소스(R)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;② 서버 : 서버는 어떤것 활용할 것인지&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;③ 코드 작성 : 사용 함수명이 중복되지 않게 하기, 수정금지 부분 코드 사용 여부, JSON, KEY, 함수로 묶음부분 등&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;④ 통신 : IP, Port, TCP 지정, &amp;nbsp;패키지 버젼&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;⑥ DB : 자료에 포함되어야 할 중요 핵심 요소나 항목들 사전 토의 합의&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;⑦ 각종 프로그램 개발 산출물 작성시 사전 협의해 할 사항들 확인 (예, 요구사항 명세서, API명세서 등)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;* 요구사항 명세서(동작 로직들),&amp;nbsp; API 명세서(주고받는 규칙들)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.&amp;nbsp;파일&amp;nbsp;구조&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;1.1. 다양한 통합파일 실행 방법이 있지만 아래 StackModelView를 활용하여 통합 실행 화면을 구성한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;각각의 패키지 파일들을 다운로드 해서 파이참 환경에서 새프로젝트를 구성한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.2. 통합 폴더명 : my_total_project /&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- StackModelView.py&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 통합해서 새로 생성한 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- StackModelViewMain.py&amp;nbsp; # 통합해서 새로 생성한 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- requirements.txt&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# 통합해서 새로 생성한 텍스트 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- Package1.&amp;nbsp; &amp;nbsp;# 개발자 1번의 패키지 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- ManagerReservation /&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- __init__.py&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- ManagerMain.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- ManagerNetWorkModel.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- ManagerUI (6개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- ManagerViewModel (3개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- __pycache__ (2개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |---&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;CsharpSocektNetWork /&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- CsharpSocektNetWork.csproj&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- CsharpSocektNetWork.http&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- Program.cs&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- Properties (1개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- appsettings.Development.json&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- appsettings.json&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---obj (6개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- Package2.&amp;nbsp; &amp;nbsp;# 개발자 2번의 패키지 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |---danawa_car_ver&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- __init__.py&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---__pycache__ (3개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---db (3개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---image_rc.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---main.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---model (7개 파일)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---repository(7개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---res(6개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---res.qrc&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---utils(5개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---view(13개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|---widget.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; |--- Package3.&amp;nbsp; &amp;nbsp;# 개발자 3번의 패키지 파일&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |---project_manager&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|--- __init__.py&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---main.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---PySide6_SQLite.txt&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---requirements.txt&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---__pycache__ (1개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---core (3개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---data(1개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---modules(5개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---ui (4개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;|---utils(3개 파일)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.3. 예시 파일 구조&lt;/b&gt;&lt;br /&gt;폴더명 :&amp;nbsp; my_total_project /&lt;br /&gt;&amp;nbsp;&lt;b&gt; | --- main.py&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; | --- Package1/&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| --- __init__.py&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| --- sub_app1.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;b&gt;&amp;nbsp; | ---&lt;/b&gt;&amp;nbsp;&lt;b&gt;&amp;nbsp;Package2/&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;---__init__.py&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;| ---sub_app2.py&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;b&gt;&amp;nbsp; | ---&lt;/b&gt;&amp;nbsp;&amp;nbsp;Package3/&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; | --- __init__.py&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; | --- sub_app3.py&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.4. my_total_project /&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- StackModelView.py&amp;nbsp; &amp;nbsp; &amp;nbsp;# 통합해서 새로 생성한 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1781684236528&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QStackedWidget, QApplication
from PySide6.scripts.pyside_tool import project
import sys

# 다나와 차 버전 관련 커스텀 데이터베이스 및 레포지토리, 모델 임포트
from danawa_car_ver.db import DataBase
from danawa_car_ver.repository import UserRepo, CarRepo, QuoteRepo, FavoriteRepo, LogRepo
from danawa_car_ver.model import UserModel, CarModel, QuoteModel, FavoriteModel, AdminModel
from danawa_car_ver.widget import MainWidget

# 프로젝트 관리자(Project Manager) 관련 UI 및 데이터베이스 초기화 모듈 임포트
from project_manager.core.database import init_database
from project_manager.ui.login_window import LoginWindow
from project_manager.ui.main_window import MainWindow

# 예약 관리자(Manager Reservation) 뷰모델 임포트
from ManagerReservation.ManagerViewModel.ManagerViewModel import ManagerViewModel

# 프로젝트 관리자 모듈의 메타데이터 임포트
from project_manager.modules.module_panel import MODULE_META


class StackModelView(QStackedWidget):
    &quot;&quot;&quot;
    QStackedWidget을 상속받아 여러 개의 화면(위젯)을 쌓아두고
    인덱스를 통해 화면을 전환하는 컨테이너 클래스입니다.
    &quot;&quot;&quot;
    def __init__(self):
        super().__init__()

        # [주석 처리된 코드] 다나와 자동차 관련 DB, 레포지토리, 모델 초기화 로직
        # db = DataBase()

        # user_repo = UserRepo(db)
        # car_repo = CarRepo(db)
        '''quote_repo = QuoteRepo(db)
        favorite_repo = FavoriteRepo(db)
        log_repo = LogRepo(db)

        user_model = UserModel(user_repo)
        car_model = CarModel(car_repo)
        quote_model = QuoteModel(quote_repo)
        favorite_model = FavoriteModel(favorite_repo)
        admin_model = AdminModel(car_repo, quote_repo, log_repo)
        '''

        # 프로젝트 관리자 시스템의 데이터베이스를 초기화합니다.
        init_database()

        # [주석 처리된 코드] 다나와 자동차 메인 위젯 생성
        # self.a = MainWidget(user_model, car_model, quote_model, favorite_model, admin_model)
        
        # 예약 관리자 뷰모델 인스턴스를 생성합니다.
        self.b = ManagerViewModel()
        
        # 모듈 메타데이터를 전달하여 메인 관리자 창 인스턴스를 생성합니다.
        self.c = MainWindow(MODULE_META)
        
        # [주석 처리된 코드] 다나와 자동차 스택 위젯을 컨테이너에 추가
        # self.addWidget(self.a.stack)
        
        # 메인 관리자 창(MainWindow)을 첫 번째 스택(인덱스 0)으로 추가합니다.
        self.addWidget(self.c)
        
        # 예약 관리자 뷰(View)를 두 번째 스택(인덱스 1)으로 추가합니다.
        self.addWidget(self.b.View)
        
        # 프로그램 시작 시 첫 번째 화면(MainWindow, 인덱스 0)이 보이도록 설정합니다.
        self.setCurrentIndex(0)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; |--- StackModelViewMain.py # 통합해서 새로 생성한 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1781684256519&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
from PySide6.QtWidgets import QApplication
from ManagerReservation.ManagerViewModel.ManagerViewModel import ManagerViewModel
from StackModelView import StackModelView

# 해당 스크립트가 직접 실행될 때만 아래 블록을 수행합니다. (모듈로 import될 때는 실행 안 됨)
if __name__ == &quot;__main__&quot;:
    
    # GUI 애플리케이션의 제어 흐름과 주요 설정을 관리하는 QApplication 인스턴스를 생성합니다.
    # sys.argv는 명령행 인자(Arguments)를 프로그램 내부로 전달합니다.
    app = QApplication(sys.argv)
    
    # 앞서 정의한 화면 전환 컨테이너(QStackedWidget)인 StackModelView 인스턴스를 생성합니다.
    widget = StackModelView()
    
    # 생성한 위젯(윈도우 창)을 화면에 표시합니다.
    widget.show()
    
    # 애플리케이션의 메인 이벤트 루프를 시작합니다. (app.exec() 또는 app.exec_())
    # 사용자가 창을 닫아 루프가 종료되면 시스템에 종료 상태 코드를 반환하며 프로그램을 완전히 종료합니다.
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;|--- requirements.txt&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# 통합해서 새로 생성한 텍스트 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1781684390464&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;certifi==2026.5.20
charset-normalizer==3.4.7
idna==3.18
PySide6==6.11.1
PySide6_Addons==6.11.1
PySide6_Essentials==6.11.1
requests==2.34.2
shiboken6==6.11.1
urllib3==2.7.0

openpyxl 3.1.5&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.5. 예시 파일 통합후 실행 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1781684737512&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication, QMainWindow, QTabWidget
# ... (위젯 import 부분은 동일)

class TabbedApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;탭 통합 프로그램&quot;)
        self.resize(1000, 700)

        # 탭 위젯 생성
        self.tabs = QTabWidget()

        # 각 프로그램 위젯을 탭에 추가 (탭 이름 지정)
        self.tabs.addTab(AppOneWidget(), &quot;첫 번째 프로그램&quot;)
        self.tabs.addTab(AppTwoWidget(), &quot;두 번째 프로그램&quot;)
        self.tabs.addTab(AppThreeWidget(), &quot;세 번째 프로그램&quot;)

        self.setCentralWidget(self.tabs)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;※ 프로젝트 파일은 별첨 유지&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/66</guid>
      <comments>https://goodjop79.tistory.com/66#entry66comment</comments>
      <pubDate>Wed, 17 Jun 2026 17:46:42 +0900</pubDate>
    </item>
    <item>
      <title>D+46 [Project]  프로젝트에서 WBS 무엇일까?</title>
      <link>https://goodjop79.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 14일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 개인 학습&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ WBS&amp;nbsp;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(Work Breakdown Structure, 작업 분류 구조)&lt;/span&gt;&lt;/b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WBS(Work Breakdown Structure, 작업 분류 구조) 핵심 가이드&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WBS는 프로젝트의 성공을 위한 '성과물 중심의 계층적 구조도' 전체 프로젝트를 관리 가능한 수준의 작은 단위(Work Package)로 분해하여 체계적으로 정의한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. WBS의 핵심 목적&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;범위 정의 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 프로젝트에서 수행해야 할 모든 작업을 명확히 파악하여 범위 이탈(Scope Creep)을 방지&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;책임 명확화 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &quot;누가 무엇을 해야 하는지&quot;(WBS와 RACI 매트릭스 연결) 명확해 진다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일정 및 비용 산정 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 작업 단위가 작아지므로 정확한 공수 산정과 예산 수립이 가능해 진다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의사소통 도구 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 이해관계자들에게 프로젝트 진행 상황과 구조를 시각적으로 공유한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. WBS 작성의 3대 원칙&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100% 법칙 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 상위 레벨의 모든 작업은 하위 레벨 작업들의 합과 정확히 일치해야 한다. (누락도, 중복도 없어야 함).&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성과물(Deliverable) 중심 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 기능이나 수행 절차가 아닌, '산출물'을 기준으로 구조화 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상호 배타적 계층 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 하위 요소들은 서로 겹치지 않게 독립적으로 구분 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. WBS 구조 예시 (계층적 모델)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Level 1 (프로젝트 명) :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 전체 프로젝트 목표&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Level 2 (주요 단계/Phase) :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 생명주기 또는 주요 산출물 그룹 (예: 설계, 개발, 테스트)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Level 3 (하위 작업/Task) :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 관리 가능한 수준으로 나눈 세부 작업&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Level 4 (작업 패키지 / Work Package) :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 실질적인 일정과 비용이 배정되는 최하위 단위&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 성공적인 WBS 작성을 위한 팁&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;팀과 함께 작성:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 실무자가 참여해야 현실적이고 누락 없는 WBS가 생성&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;'8/80 법칙' 적용:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 작업 단위가 너무 작으면(8시간 미만) 관리가 비효율적이고, 너무 크면(80시간 초과) 위험 관리가 어렵다&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사전(WBS Dictionary) 활용 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 각 작업 패키지에 대한 상세 설명, 담당자, 마일스톤, 선행 조건 등을 기록하여 공유하기&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지속적 관리 :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 프로젝트 변경 사항 발생 시 WBS를 즉시 업데이트하여 최신 상태를 유지해야 한다.&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 활용 방법&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;엑셀이나 전문 프로젝트 관리 도구를 사용해 표나 간트차트 형태로 만든다. 성공적인 관리를 위해서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;WBS 엑셀 템플릿&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt; 등을 활용해 업무를 최소 단위(일주일 내 완료 가능한 수준)로 쪼개고 종속 관계(A 작업이 끝나야 B 작업 시작)를 파악하는 것이 중요&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;팀원과 함께 작성 :&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;실무자가 참여해야 현실적이고 누락 없는 WBS가 생성된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 결&amp;nbsp; 론&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WBS는 단순히 리스트를 나열하는 것이 아니라,&lt;b&gt; &quot;무엇을 만들 것인가?&lt;/b&gt;&quot;에 집중하여 프로젝트의 복잡성을 관리하는 핵심설계&lt;/span&gt;&lt;/p&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/64</guid>
      <comments>https://goodjop79.tistory.com/64#entry64comment</comments>
      <pubDate>Sun, 14 Jun 2026 19:36:21 +0900</pubDate>
    </item>
    <item>
      <title>D+44 [Project] 프로그램 개발 산출물  작성 방법</title>
      <link>https://goodjop79.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 10일(수)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 목 표 : 프로그램 산출물에 대한 이해와 작성 방법&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;프로그램 산출물 &amp;amp; 작성방법&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Project&amp;nbsp; 산출물&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 개발 산출물은?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트는 사용자 중심에서 생각하고 사용자가 사용 가능한 프로그램을 개발해야 한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프로그램 개발 산출물은 프로젝트의 '설계도'이자 '설명서'이므로, 제3자가 보아도 이해할 수 있도록 명확하고 구체적으로 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;c&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2.&lt;span&gt; 개발 산출물 및 작성방법&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div data-bfc=&quot;&quot; data-ved=&quot;2ahUKEwiSnvi2roSVAxWie_UHHflFDZsQi4wTegoIAggACAAIBxAA&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;
&lt;div data-animation-nesting=&quot;&quot; data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(238, 240, 255);&quot;&gt;&lt;b&gt;2.1. 프로그램 개발 계획서&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div data-bfc=&quot;&quot; data-ved=&quot;2ahUKEwiSnvi2roSVAxWie_UHHflFDZsQi4wTegoIAggACAAIDhAA&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QAQ&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;프로젝트 개요 &lt;/b&gt;: 개발 목적과 기대 효과를 한 페이지로 요약&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QAg&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;개발 범위 &lt;/b&gt;: 개발할 프로그램의 기능과 포함되지 않는 범위 명확화&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QAw&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;요구사항 정의 &lt;/b&gt;: 사용자가 원하는 기능과 시스템 성능 요건을 구체화&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QBA&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;시스템 설계 &lt;/b&gt;: 아키텍처, 데이터베이스 모델링&lt;b&gt;(ERD)&lt;/b&gt;, UI/UX &lt;b&gt;화면 설계서&lt;/b&gt; 포함&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QBQ&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;일정 계획 &lt;/b&gt;: &lt;b&gt;작업 분할 구조도(WBS)&lt;/b&gt;를 활용해 세부 마일스톤 작성&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QBg&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;인력 및 예산 &lt;/b&gt;: 투입 인력의 역할과 개발 비용을 산정&lt;/span&gt;&lt;/li&gt;
&lt;li data-sfc-cp=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgACA4QBw&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;span data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;&lt;b&gt;위험 관리 &lt;/b&gt;: 예상되는 기술적 문제와 일정 지연에 대한 대응책 수립&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.2. 요구사항 정의서 (Requirement Specification) : &lt;/b&gt;프로젝트의 출발점으로, &quot;무엇을 만들 것인가&quot;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법 &lt;/b&gt;: 고객의 모호한 요구를 기술적인 언어로 명확히 바꾸어 작성, 모호한 표현(예: &quot;빠르게 처리된다&quot;, &quot;예쁘게 디자인한다&quot;) 대신&lt;b&gt; 정량적인 수치와 명확한 동작을 적어야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBxAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목 &lt;/b&gt;: 요구사항 ID, 요구사항 명칭, 분류(기능/비기능), 상세 설명, 우선순위(상/중/하), 요청자&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.3. 화면 설계서 / 스토리보드 (Storyboard) : &lt;/b&gt;UI/UX 디자이너와 개발자가 화면을 어떻게 구현할지 합의하는 문서&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법&lt;/b&gt;: 화면 디자인 영역(와이어프레임)과 설명 영역(Description)을 분리, 버튼을 눌렀을 때의 이동 경로, 예외 처리(데이터가 없을 때 등)에 대한 동작을 빠짐없이 적어야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목&lt;/b&gt;: 화면 ID, 화면명, UI 와이어프레임, 기능 및 디스크립션(인터랙션 정의), 이동할 화면(링크)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.4. 데이터베이스 설계서 및 ERD (Database Design) : &lt;/b&gt;데이터가 어떻게 저장되고 관리되는지 구조를 보여주는 문서&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법&lt;/b&gt;: 개념적 설계에서 시작해 물리적 설계로 발전시킨다. 테이블 간의 관계(1:1, 1:N)를 명확히 선으로 연결하고, 데이터 타입(문자, 숫자 등)과 길이를 정확히 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDRAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목&lt;/b&gt;: 엔티티(테이블)명, 속성(컬럼)명, 데이터 타입 및 길이, PK(기본키)/FK(외래키) 여부, Null 허용 여부&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.5. API 명세서 (API Specification) : &lt;/b&gt;프론트엔드와 백엔드가 데이터를 주고받는 규칙을 정한 문서&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIEBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법&lt;/b&gt;: 주소(URL)와 요청 방식을 명시하고, 보낼 데이터(Request)와 받을 데이터(Response)의 형식을 JSON 형태로 예시를 들어 작성, 성공할 때와 실패할 때의 응답 코드를 모두 적어주어야 혼선이 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIEBAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목&lt;/b&gt;: API ID/이름, HTTP Method(GET, POST 등), URL 경로, 요청 파라미터, 응답 데이터(성공/실패 예시), HTTP 상태 코드.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.6. 테스트 시나리오 및 결과서 (Test Case) : &lt;/b&gt;개발된 프로그램이 기획대로 잘 작동하는지 확인하는 문서&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIExAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법&lt;/b&gt;: '누가, 어떤 화면에서, 무엇을 했을 때, 어떤 결과가 나와야 한다'를 하나의 행으로 작성, 정상적인 입력뿐만 아니라 잘못된 값을 넣었을 때 오류 메시지가 잘 뜨는지(비정상 케이스)도 반드시 테스트해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIExAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목&lt;/b&gt;: 테스트 ID, 테스트 경로(화면), 테스트 조건/절차, 기대 결과, 실제 결과, 통과 여부(Pass/Fail), 결함 내용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;b&gt;2.7. 사용자 및 관리자 매뉴얼 (Manual) : &lt;/b&gt;시스템을 처음 쓰는 사람이 보고 따라 할 수 있는 안내서&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIFhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;작성 방법&lt;/b&gt;: 전문 용어는 최대한 줄이고 구어체로 쉽게 작성한다. 모든 과정은 &lt;b&gt;실제 화면 캡처 이미지&lt;/b&gt;를 첨부하고, 중요 버튼이나 입력창은 빨간색 네모나 화살표로 강조 표시를 해주는 것이 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIFhAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;필수 항목&lt;/b&gt;: 시스템 개요, 로그인 방법, 주요 기능별 이용 프로세스(화면 캡처 포함), 자주 묻는 질문(FAQ) 및 문제 해결 가이드&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 프로그램 소스코드 작성시 참고사항&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. CRUD : 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 소프트웨어와 데이터베이스 관리 시스템에서 데이터를 처리하는 네 가지 기본 기능인 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)의 약어, 거의 모든 웹 애플리케이션과 REST API 설계에서 데이터를 효율적으로 관리하기 위한 핵심 뼈대로 활용된다. CRUD 기능의 핵심 개념각 글자는 시스템에서 데이터가 수행하는 역할을 명확히 나타낸다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C (Create, 생성): 데이터를 새로 만드는 기능&lt;/li&gt;
&lt;li&gt;R (Read, 읽기): 기존 데이터를 조회하거나 검색하는 기능&lt;/li&gt;
&lt;li&gt;U (Update, 수정): 기존 데이터의 내용을 변경하는 기능&lt;/li&gt;
&lt;li&gt;D (Delete, 삭제): 불필요한 데이터를 제거하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예 시)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시판 (게시글 관리)Create : 새 글 작성하기&lt;/li&gt;
&lt;li&gt;Read : 작성된 글 목록 보기, 상세 내용 보기&lt;/li&gt;
&lt;li&gt;Update : 작성한 글 내용 수정하기&lt;/li&gt;
&lt;li&gt;Delete : 불필요한 글 지우기&lt;/li&gt;
&lt;li&gt;쇼핑몰 (장바구니 시스템)Create : 장바구니에 상품 담기&lt;/li&gt;
&lt;li&gt;Read : 장바구니에 담긴 상품 목록 확인하기&lt;/li&gt;
&lt;li&gt;Update : 상품의 수량 변경하기&lt;/li&gt;
&lt;li&gt;Delete : 장바구니에서 상품 빼기&lt;/li&gt;
&lt;li&gt;SNS (프로필 관리)Create : 회원가입, 새 프로필 등록&lt;/li&gt;
&lt;li&gt;Read : 내 프로필 페이지 보기&lt;/li&gt;
&lt;li&gt;Update : 프로필 사진 및 상태 메시지 변경&lt;/li&gt;
&lt;li&gt;Delete : 회원탈퇴 개발 환경에서의 CRUD 구현프로그래밍에서 CRUD는 데이터베이스의 영구 저장소와 상호작용하기 위한 F-Lab 개발 인사이트의 중요한 설계 지침&lt;/li&gt;
&lt;li&gt;SQL (데이터베이스) : INSERT, SELECT, UPDATE, DELETE 명령어를 사용&lt;/li&gt;
&lt;li&gt;HTTP (웹 API) : POST, GET, PUT/PATCH, DELETE 메서드를 사용, 게시판과 데이터베이스를 활용하여 CRUD 기능을 실제로 구현하는 과정.&amp;nbsp; 끝.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/63</guid>
      <comments>https://goodjop79.tistory.com/63#entry63comment</comments>
      <pubDate>Sat, 13 Jun 2026 23:10:59 +0900</pubDate>
    </item>
    <item>
      <title>D+43 [Project] 프로젝트 기획자(PM)에 대한 이해</title>
      <link>https://goodjop79.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 9일(화)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 목 표 : 프로젝트 관련 직종을 이해하고 개인 학습한다.&amp;nbsp;&lt;br&gt;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJH2yj/dJMcagsoPI2/0jKT0NyZakY11oeYIdWfCK/tfile.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJH2yj/dJMcagsoPI2/0jKT0NyZakY11oeYIdWfCK/tfile.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJH2yj/dJMcagsoPI2/0jKT0NyZakY11oeYIdWfCK/tfile.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJH2yj%2FdJMcagsoPI2%2F0jKT0NyZakY11oeYIdWfCK%2Ftfile.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;br&gt;1. 프로그램 기획자(Product Manager,&amp;nbsp; Project Manager)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. PM 이란?&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.1.&amp;nbsp;&lt;/b&gt;&lt;b&gt;프로덕트 매니저(PM)&lt;/b&gt;와 &lt;b&gt;프로젝트 매니저(PM)&lt;/b&gt;는 모두 제품이나 업무의 성공을 이끈다는 공통점이 있지만, 매니징의 &lt;b&gt;대상&lt;/b&gt;과 &lt;b&gt;목표&lt;/b&gt;에서 명확한 차이가 있다.&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 4px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sfc-inited=&quot;2&quot; data-complete=&quot;true&quot; data-ved=&quot;2ahUKEwij0_rwmoSVAxXQklYBHQy8KlIQ-q4QegoIAggACAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; &lt;b&gt;1.2. 프로덕트 매니저 (Product Manager)프로젝트 매니저 (Project Manager) 구분&lt;/b&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 4px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sfc-inited=&quot;2&quot; data-complete=&quot;true&quot; data-ved=&quot;2ahUKEwij0_rwmoSVAxXQklYBHQy8KlIQ-q4QegoIAggACAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
 &lt;table style=&quot;border-collapse: collapse; width: 96.2791%; height: 188px;&quot; border=&quot;1&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-animation-nesting=&quot;&quot; data-ke-align=&quot;alignLeft&quot;&gt; 
  &lt;tbody data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt; 
   &lt;tr data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;관리 대상&lt;/b&gt;&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;제품 (Product) 자체&lt;/b&gt;&lt;/td&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;일감 및 과제 (Project)&lt;/b&gt;&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 질문&lt;/b&gt;&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;무엇(What)&lt;/b&gt;을, &lt;/span&gt;&lt;b&gt;왜(Why)&lt;/b&gt; 만들어야 하는가?&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;언제(When)&lt;/b&gt;, &lt;b&gt;어떻게(How)&lt;/b&gt; 완료할 것인가?&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 목표&lt;/b&gt;&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;제품의 시장 가치 극대화 및 성장&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;정해진 예산과 일정 내 프로젝트 완수&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 업무&lt;/b&gt;&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;유저 리서치, 데이터 분석, 제품 비전 수립&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.8px solid rgb(68, 71, 70);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;일정 계획, 리소스 분배, 리스크 및 병목 제거&lt;/td&gt; 
   &lt;/tr&gt; 
   &lt;tr data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 
    &lt;td style=&quot;text-align: center;&quot; colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;성공 지표&lt;/b&gt;&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;고객 만족도, 매출, 잔존율 등 (비즈니스 성과)&lt;/td&gt; 
    &lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;온타임(On-Time) 출시, 예산 준수 (프로세스 효율)&lt;/td&gt; 
   &lt;/tr&gt; 
  &lt;/tbody&gt; 
 &lt;/table&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.3. 프로덕트 매니저 (Product Manager)&lt;/b&gt;&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-ved=&quot;2ahUKEwij0_rwmoSVAxXQklYBHQy8KlIQi4wTegoIAggACAAIDRAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACA0QAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;제품의 생애주기&lt;/b&gt;(기획부터 출시, 개선까지)를 총괄&lt;/span&gt;&lt;/li&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACA0QAg&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;고객의 &lt;b&gt;문제&lt;/b&gt;를 파악하고 해결책을 제시&lt;/span&gt;&lt;/li&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACA0QAw&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;제품이 시장에서 &lt;b&gt;성공&lt;/b&gt;하는 데 집중&lt;/span&gt;&lt;/li&gt; 
 &lt;/ul&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.4. 프로젝트 매니저 (Project Manager)&lt;/b&gt;&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-ved=&quot;2ahUKEwij0_rwmoSVAxXQklYBHQy8KlIQi4wTegoIAggACAAIERAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBEQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;시작과 끝&lt;/b&gt;이 명확한 과업을 관리&lt;/span&gt;&lt;/li&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBEQAg&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;정해진 &lt;b&gt;기간&lt;/b&gt;과 &lt;b&gt;예산&lt;/b&gt;을 넘기지 않도록 통제&lt;/span&gt;&lt;/li&gt; 
  &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBEQAw&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;팀이 원활하게 협업하고 &lt;b&gt;일정&lt;/b&gt;대로 산출물을 내는 데 집중&lt;/span&gt;&lt;/li&gt; 
 &lt;/ul&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt; &lt;b&gt;1.5. 신입 프로젝트 메니저&lt;/b&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
  신입은 프로젝트 전체를 설계하진 못하더라도, 과정에서 갑작스레 발견된 일을 &amp;nbsp;조치 예방한 경험이 중요,&amp;nbsp;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-subtree=&quot;aimfl&quot;&gt;신입 프로젝트 매니저(Project Manager, PM)&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;는 경력이 부족한 상태에서 여러 이해관계자와 일정을 조율해야 하므로, '직무 전문성'&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;보다는 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 500; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;'기본적인 소통과 실행력'이 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;1.6. 신입 PM의 핵심 요구 역량&lt;/b&gt; 
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;구조적 커뮤니케이션 &lt;/b&gt;: 메일 작성, 회의록 정리, 업무 지시를 명확하고 간결하게 하는 능력&lt;/li&gt;&lt;li&gt;&lt;b&gt;일정 및 리스크 관리 &lt;/b&gt;: 일의 우선순위를 정하고, 문제가 생겼을 때 빠르게 공유하는 감각&lt;/li&gt;&lt;li&gt;&lt;b&gt;도구(Tools) 활용력 &lt;/b&gt;: Jira, Confluence, Notion, Slack 등 협업 툴을 능숙하게 다루는 능력&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;※ 참고사항&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;br&gt; &amp;nbsp; * 그외 각 회사,기관에서 사용하는 툴을 사용하면 된다.&amp;nbsp;&lt;br&gt;&amp;nbsp; * 크레딧을 활용하므로&lt;b&gt; 비용이 발생&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt; 
 &lt;tbody&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 100%;&quot;&gt; 
    &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;⚙️ Jira (지라)&lt;/b&gt; 
    &lt;/div&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;: 프로젝트 관리 및 이슈(작업) 추적 툴&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 용도&lt;/b&gt;: 개발 프로젝트의 일정 관리(애자일, 스크럼, 칸반 보드)에 특화됨&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBBAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;: &quot;누가, 언제까지, 무슨 일&quot;을 하는지 상태(할 일, 진행 중, 완료)를 칸반 보드 형태로 한눈에 모니터링&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; 
    &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;  Confluence (컨플루언스)&lt;/b&gt; 
    &lt;/div&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;: 사내 정보 공유를 위한 팀 협업 문서(위키) 툴&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 용도&lt;/b&gt;: 회의록, 개발 가이드, 기술 문서, 기획서 등 축적되는 지식을 기록&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBhAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;: Jira와 같은 회사(Atlassian) 제품으로, Jira의 작업 티켓을 문서 내에 연동하여 실시간으로 개발 진행 상황을 문서화하기 좋음&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; 
    &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;  Notion (노션)&lt;/b&gt; 
    &lt;/div&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;: 문서 작성, 데이터베이스 관리, 메모를 하나로 합친 올인원 노트 툴&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 용도&lt;/b&gt;: 개인 메모부터 스타트업의 기업 홈페이지(소개 페이지), 업무 칸반 보드까지 다양하게 활용&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;: 디자인이 깔끔하고 템플릿 제작이 자유로우며, 텍스트·표·이미지를 자유롭게 배치할 수 있어 접근성이 높다&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; 
    &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;  Slack (슬랙)&lt;/b&gt; 
    &lt;/div&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;: 업무 전용 실시간 팀 메신저&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 용도&lt;/b&gt;: 프로젝트별, 팀별로 '채널'을 개설하여 빠르게 소통하고 실시간 피드백을 주고 받는다&lt;/span&gt;&lt;/li&gt; 
     &lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;: Jira, GitHub, Google Drive 등 수많은 외부 툴과 연동되어, 시스템 알림(예: 코드 배포 완료, 에러 발생)을 한곳에서 받아본다&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
 &lt;/tbody&gt; 
&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;※ 국내 기업에서는 PM과 PO를 혼용하거나 동일하게 다루는 경우가 많다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt; 1.7. PM이 되기 위한 준비 과정&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;PM은 주니어 단계에서 바로 시작하기 까다로운 직무이므로, 다음과 같은 역량을 단계적으로 증명해야 한다.&lt;/li&gt;&lt;li&gt;직무 이해도 넓히기 : IT 서비스가 만들어지는 전체 프로세스(기획-디자인-개발-데이터 분석)의 기초 지식을 쌓아야 한다&lt;/li&gt;&lt;li&gt;역량 증명 포트폴리오 제작 : 기존 서비스의 역기획(구조 분석 및 개선안 제안) 서류를 작성하거나, 사이드 프로젝트를 통해 실제 제품을 기획·출시해 본 경험을 만든다.&lt;/li&gt;&lt;li&gt;데이터 분석 툴 학습 : SQL, GA4(구글 애널리틱스) 같은 데이터 추출 및 분석 툴의 기본적인 사용법을 익힌다.&lt;/li&gt;&lt;li&gt;PM 부트캠프 활용 : 비전공자나 신입의 경우, 기업 연계 프로젝트를 수행할 수 있는 국비지원 교육이나 사설 PM 부트캠프를 통해 실무 경험을 빠르게 채우는 것도 좋은 방법이다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2. 기획자를 위한 현실적인 Tip (노코드 + AI)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;요즘은 아이디어가 떠오르면 개발자를 찾기 전에 노코드(No-Code) 플랫폼과 AI 코드 생성 툴을 결합해 몇 시간 만에 프로토타입(시제품)을 만들어 시장의 반응을 먼저 검증(MVP)하는 것이 정석이다.&amp;nbsp;완벽한 앱을 한 번에 만들려 하기보다, 핵심 기능 하나만 빠르게 검증하는 트렌드를 따라서 만들어 본다.&lt;br&gt;2026~2030년의 프로그램 개발 시장은 &lt;b&gt;&quot;코딩 실력보다 설계 능력과 비즈니스 감각이 주 무기가 되는 시대&lt;/b&gt;&quot;가 될 것이다.&amp;nbsp;&lt;br&gt;AI가 쏟아내는 수많은 코드 속에서 인간만의 독창적인 기획력, 시행착오에서 얻은 경험, 그리고 시스템의 뼈대를 잡는 아키텍처 설계 능력을 갖춘 개발자가 시장에서 살아남을 수 있을 것이다.끝.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <category>PM #프로젝트메니저</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/62</guid>
      <comments>https://goodjop79.tistory.com/62#entry62comment</comments>
      <pubDate>Sat, 13 Jun 2026 22:10:50 +0900</pubDate>
    </item>
    <item>
      <title>D+43 [Python] PySide6 &amp;amp; 가상의 LLM 대화앱 예제 실습</title>
      <link>https://goodjop79.tistory.com/60</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 9일(화)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 내 용 :&amp;nbsp; 로컬호스트 개념, 멀티스레드 기반 GUI 비동기 처리, LLM연동 및 리소스 관리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; ※ 실습을 위한 가상환경임&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;1. 로컬호스트(Localhost) 및 개발 환경 이해&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;개념 정의: 컴퓨터 자신을 지칭하는 기본 루프백 주소 (외부 네트워크에서 접근 불가)&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;접속 방법: 서버 활성화 상태에서 브라우저 주소창에 http://127.0.0.1 또는 http://localhost 입력&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;포트 지정: 개발 서버가 특정 포트를 사용할 경우 http://localhost:8080 형태로 접속&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICBAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;DB 관리: MySQL / MariaDB 환경은 http://localhost/phpmyadmin을 통해 데이터베이스 제어 (환경에 따라 다를 수 있음)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;2. 워커 및 멀티스레드(Multi-Thread)&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;스레드(Thread): 코드가 순차적으로 실행되는 단일 흐름의 단위&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;멀티스레드 필요성: OS 기반 응용 소프트웨어 및 GUI 개발의 핵심 필수 개념&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAG&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;동기(Synchronous): 하나의 작업이 완전히 끝난 후 다음 줄의 코드를 실행하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAJ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;비동기(Asynchronous): 앞선 작업의 종료 여부와 상관없이 다음 코드를 실행하는 방식&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt; (네트워크 통신은 대기 방지를 위해 비동기 처리 권장)&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;GUI 화면 멈춤 방지: 프로그램 실행 중 UI가 먹통이 되는 현상을 방지하기 위해 대기 시간이 긴 로직 연산은 별도 스레드로 분리 (GUI 프로그램은 기본적으로 1개의 UI 스레드로 구동되며, 필요에 따라 로직 스레드를 추가)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;3. 소스코드 보안 및 데이터 분리 원칙&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDBAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;하드코딩 금지: 소스코드 내부에 고정 데이터를 직접 저장하는 행위 철저히 배제&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDBAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;리소스 관리: 이미지나 아이콘 등은 리소스 파일(.qrc)을 활용하여 프로그램 코드와 자원을 독립적으로 분리&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDBAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;개인정보 보호: AI 모델 정보, 회원 정보, API 키 등 민감 데이터는 소스코드 및 리소스 파일과 완벽히 분리하여 환경변수 파일(.env) 등으로 관리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIDRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;4. 실습 및 기능 구현 핵심 요약&lt;/b&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;응답 표시: GUI 화면에 로컬 LLM의 답변을 매끄럽게 출력하는 기능 구현&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;QThread 도입: LLM 답변 대기 시간 동안 UI가 멈추거나 응답 없음 상태가 되는 현상 해결&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;앱 기능 전환: 일반 대화형 앱에서 효율적인 '메모 요약 앱'으로 구조 변경&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;기록 시스템 구축: 사용자가 요약한 텍스트 히스토리를 한눈에 볼 수 있는 '요약 기록 목록' 구현&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;환경 설정 최적화: 앱 세팅 값과 리소스 파일 정리를 통한 프로젝트 구조 안정화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. 예시 실습 및 이해하기&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5.1. GUI에서 응답 표시하기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 12-04-26.png&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D5OyP/dJMcab5y9YX/DootBvwkCYtM5k2eAfzuc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D5OyP/dJMcab5y9YX/DootBvwkCYtM5k2eAfzuc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D5OyP/dJMcab5y9YX/DootBvwkCYtM5k2eAfzuc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD5OyP%2FdJMcab5y9YX%2FDootBvwkCYtM5k2eAfzuc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;872&quot; height=&quot;988&quot; data-filename=&quot;스크린샷 2026-06-09 12-04-26.png&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 2. QThread로 응답 대기중 화면 멈춤 방지&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 12-25-05.png&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;1031&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0WqQk/dJMcaiQ8JKs/vVczbbtnqhF9xPjYXcHvyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0WqQk/dJMcaiQ8JKs/vVczbbtnqhF9xPjYXcHvyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0WqQk/dJMcaiQ8JKs/vVczbbtnqhF9xPjYXcHvyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0WqQk%2FdJMcaiQ8JKs%2FvVczbbtnqhF9xPjYXcHvyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;873&quot; height=&quot;1031&quot; data-filename=&quot;스크린샷 2026-06-09 12-25-05.png&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;1031&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5.3. 메모 요약 앱으로 바꾸기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 18-00-02.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFz4JG/dJMcaar5bkI/EPWdY2JF9Xa1yJk1q0DtK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFz4JG/dJMcaar5bkI/EPWdY2JF9Xa1yJk1q0DtK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFz4JG/dJMcaar5bkI/EPWdY2JF9Xa1yJk1q0DtK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFz4JG%2FdJMcaar5bkI%2FEPWdY2JF9Xa1yJk1q0DtK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;1033&quot; data-filename=&quot;스크린샷 2026-06-09 18-00-02.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 17-57-08.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0l8PX/dJMcabxHBMx/HkkEK7PHuyrhLQhbRGt730/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0l8PX/dJMcabxHBMx/HkkEK7PHuyrhLQhbRGt730/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0l8PX/dJMcabxHBMx/HkkEK7PHuyrhLQhbRGt730/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0l8PX%2FdJMcabxHBMx%2FHkkEK7PHuyrhLQhbRGt730%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;832&quot; height=&quot;1011&quot; data-filename=&quot;스크린샷 2026-06-09 17-57-08.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5.4.&amp;nbsp;요약&amp;nbsp;기록&amp;nbsp;목록&amp;nbsp;만들기&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 18-23-56.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bywZDS/dJMcagy1mpU/MtpR1gxK96QkIa1lN4FznK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bywZDS/dJMcagy1mpU/MtpR1gxK96QkIa1lN4FznK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bywZDS/dJMcagy1mpU/MtpR1gxK96QkIa1lN4FznK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbywZDS%2FdJMcagy1mpU%2FMtpR1gxK96QkIa1lN4FznK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;835&quot; height=&quot;1015&quot; data-filename=&quot;스크린샷 2026-06-09 18-23-56.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5.5.&amp;nbsp;세팅과&amp;nbsp;qrc&amp;nbsp;리소스&amp;nbsp;정리&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-09 20-42-23.png&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;1037&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfZEs1/dJMcadCcUkb/HBrE8LAK228xaMfrx97Q6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfZEs1/dJMcadCcUkb/HBrE8LAK228xaMfrx97Q6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfZEs1/dJMcadCcUkb/HBrE8LAK228xaMfrx97Q6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfZEs1%2FdJMcadCcUkb%2FHBrE8LAK228xaMfrx97Q6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1262&quot; height=&quot;1037&quot; data-filename=&quot;스크린샷 2026-06-09 20-42-23.png&quot; data-origin-width=&quot;1262&quot; data-origin-height=&quot;1037&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;끝.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/60</guid>
      <comments>https://goodjop79.tistory.com/60#entry60comment</comments>
      <pubDate>Tue, 9 Jun 2026 21:37:16 +0900</pubDate>
    </item>
    <item>
      <title>D+42 [Python] File 작성, JSON 활용 예제 실습Project</title>
      <link>https://goodjop79.tistory.com/59</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년&amp;nbsp; 6월 8일(월)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 주 제 : 파일 작성을 통한 JSON 활용 및 로직 이해하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; * 가상환경에서 가상의 DB를 활용한 로직 이해 실습임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 패키지 파일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;__init__.py, main.py, member_model.py, member_storage.py, password_utils.py, tab_window.py&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 실행 화면&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-08 20-05-59.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu8Zrm/dJMcah5OxTa/8j1NNT9lMDOHqyDToB2iE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu8Zrm/dJMcah5OxTa/8j1NNT9lMDOHqyDToB2iE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu8Zrm/dJMcah5OxTa/8j1NNT9lMDOHqyDToB2iE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu8Zrm%2FdJMcah5OxTa%2F8j1NNT9lMDOHqyDToB2iE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;851&quot; height=&quot;619&quot; data-filename=&quot;스크린샷 2026-06-08 20-05-59.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-08 20-07-24.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qK0OM/dJMcah5OxTl/Lt7nuaifLhcKT9uZVMo5N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qK0OM/dJMcah5OxTl/Lt7nuaifLhcKT9uZVMo5N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qK0OM/dJMcah5OxTl/Lt7nuaifLhcKT9uZVMo5N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqK0OM%2FdJMcah5OxTl%2FLt7nuaifLhcKT9uZVMo5N1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;851&quot; height=&quot;619&quot; data-filename=&quot;스크린샷 2026-06-08 20-07-24.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-08 20-06-13.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4pLRE/dJMcaccjKIR/T3kgbIR3UEVvuBkyax3lyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4pLRE/dJMcaccjKIR/T3kgbIR3UEVvuBkyax3lyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4pLRE/dJMcaccjKIR/T3kgbIR3UEVvuBkyax3lyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4pLRE%2FdJMcaccjKIR%2FT3kgbIR3UEVvuBkyax3lyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;851&quot; height=&quot;619&quot; data-filename=&quot;스크린샷 2026-06-08 20-06-13.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-08 20-07-40.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EWbVI/dJMcah5OxTo/BhTtYVKtCrTakwgK4qqSc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EWbVI/dJMcah5OxTo/BhTtYVKtCrTakwgK4qqSc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EWbVI/dJMcah5OxTo/BhTtYVKtCrTakwgK4qqSc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEWbVI%2FdJMcah5OxTo%2FBhTtYVKtCrTakwgK4qqSc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;851&quot; height=&quot;619&quot; data-filename=&quot;스크린샷 2026-06-08 20-07-40.png&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-08 20-08-16.png&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB0o0c/dJMcabdmm4k/DybP3MVDYODKTMFnCKgou0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB0o0c/dJMcabdmm4k/DybP3MVDYODKTMFnCKgou0/img.png&quot; data-alt=&quot;JSON 자료 (예시 입력자료)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB0o0c/dJMcabdmm4k/DybP3MVDYODKTMFnCKgou0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB0o0c%2FdJMcabdmm4k%2FDybP3MVDYODKTMFnCKgou0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;695&quot; height=&quot;738&quot; data-filename=&quot;스크린샷 2026-06-08 20-08-16.png&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JSON 자료 (예시 입력자료)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ .py, .json 파일&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① __init__.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918234990&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot; 회원모델에서 회원가입 기능 확장하는 실습 Project (signup_login_hash_project)&quot;&quot;&quot;
&quot;&quot;&quot; 회원가입 과제 구현하기 실습 &quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② main.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918307582&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys

from PySide6.QtWidgets import QApplication

from member_model import MemberModel
from tab_window import TabWindow

# 프로그램 실행을 위해 QApplication 객체를 생성합니다. (기본 설정)
app = QApplication(sys.argv)

# 회원 데이터를 관리할 모델 객체를 만듭니다.
member_model = MemberModel()

# 화면 창을 생성하고, 회원 모델 데이터를 창에 전달합니다.
window = TabWindow(member_model)

# 생성한 화면 창을 컴퓨터 모니터에 띄웁니다.
window.show()

# 프로그램이 안전하게 닫힐 때까지 대기하고 종료합니다.
sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ member_model.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918408510&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from member_storage import MemberStorage
from password_utils import hash_password

class MemberModel:
    &quot;&quot;&quot;회원가입, 로그인, 프로필 수정을 담당하는 Model&quot;&quot;&quot;

    def __init__(self):
        # 회원 데이터를 저장하고 불러오는 저장소 객체를 만듭니다.
        self.storage = MemberStorage()
        # 저장소에서 기존 회원 목록 데이터를 읽어옵니다.
        self.data = self.storage.load_data()
        # 현재 로그인한 회원의 아이디를 기억하는 변수입니다. (기본값은 로그인 안 됨)
        self.current_member_id = None

    def is_duplicate_id(self, user_id):
        # 회원 목록에서 한 명씩 꺼내어 비교합니다.
        for member in self.data[&quot;members&quot;]:
            # 입력한 아이디와 똑같은 아이디가 이미 있다면 True를 돌려줍니다.
            if member[&quot;id&quot;] == user_id:
                return True

        # 중복된 아이디가 없다면 False를 돌려줍니다.
        return False

    # 회원 가입 버튼을 눌렸을 때 호출한다.
    def register_member(self, user_id, password, password_confirm, name, email, phone):
        # 1단계: 빈 칸이 하나라도 있는지 확인합니다.
        if not user_id or not password or not password_confirm or not name or not email or not phone:
            return False, &quot;모든 값을 입력하세요.&quot;

        # 2단계: 입력한 두 비밀번호가 서로 똑같은지 확인합니다.
        if password != password_confirm:
            return False, &quot;비밀번호가 서로 다릅니다.&quot;

        # 3단계: 이미 가입된 아이디인지 확인합니다.
        if self.is_duplicate_id(user_id):
            return False, &quot;이미 사용 중인 아이디입니다.&quot;

    # 새 회원을 만들 때 비밀번호는 password_hash 저장
        # 4단계: 비밀번호를 암호화하여 새 회원 정보를 딕셔너리로 만듭니다.
        new_member = {
            &quot;id&quot;: user_id,
            &quot;password_hash&quot;: hash_password(password),
            &quot;name&quot;: name,
            &quot;email&quot;: email,
            &quot;phone&quot;: phone,
        }

        # 5단계: 회원 목록에 새 회원을 추가하고 파일로 저장합니다.
        self.data[&quot;members&quot;].append(new_member)
        self.storage.save_data(self.data)

        # 회원가입이 안전하게 끝났음을 알려줍니다.
        return True, &quot;회원가입이 완료되었습니다.&quot;

    def check_login(self, user_id, password):
        # 입력한 비밀번호를 암호화하여 비교할 준비를 합니다.
        input_password_hash = hash_password(password)

        # 등록된 회원들을 한 명씩 찾아봅니다.
        for member in self.data[&quot;members&quot;]:
            # 아이디와 암호화된 비밀번호가 모두 일치하는지 확인합니다.
            if member[&quot;id&quot;] == user_id and member[&quot;password_hash&quot;] == input_password_hash:
                # 일치하면 현재 로그인한 회원 아이디로 저장합니다.
                self.current_member_id = user_id
                return True

        # 일치하는 회원이 없다면 로그인을 거절합니다.
        return False

    def get_current_member(self):
        # 로그인 상태가 아니라면 아무것도 돌려주지 않습니다.
        if self.current_member_id is None:
            return None

        # 회원 목록에서 현재 로그인한 회원의 전체 정보를 찾아 돌려줍니다.
        for member in self.data[&quot;members&quot;]:
            if member[&quot;id&quot;] == self.current_member_id:
                return member

        return None

    def get_member_info(self):
        # 현재 로그인한 회원의 정보를 가져옵니다.
        member = self.get_current_member()

        # 로그인 정보가 없다면 아무것도 돌려주지 않습니다.
        if member is None:
            return None

        # 비밀번호를 제외한 개인 정보(아이디, 이름, 이메일, 전화번호)만 골라서 돌려줍니다.
        return {
            &quot;id&quot;: member[&quot;id&quot;],
            &quot;name&quot;: member[&quot;name&quot;],
            &quot;email&quot;: member[&quot;email&quot;],
            &quot;phone&quot;: member[&quot;phone&quot;],
        }

    def update_profile(self, name, email, phone):
        # 프로필을 수정할 회원의 정보를 가져옵니다.
        member = self.get_current_member()

        # 로그인되어 있지 않다면 수정을 실패 처리합니다.
        if member is None:
            return False

        # 회원 정보를 새 내용으로 교체합니다.
        member[&quot;name&quot;] = name
        member[&quot;email&quot;] = email
        member[&quot;phone&quot;] = phone

        # 변경된 내용을 회원 저장소 파일에 완전히 저장합니다.
        self.storage.save_data(self.data)

        # 프로필 수정이 성공했음을 알려줍니다.
        return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ member_storage.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918443813&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 회원 데이터 저장소 (파일이 없거나 깨진 경우, 특정 키가 없을 때 초기화)

import json  # JSON 데이터 처리를 위한 모듈 가져오기
from pathlib import Path  # 파일 경로를 객체로 다루기 위한 모듈 가져오기


class MemberStorage:
    &quot;&quot;&quot;회원 정보를 member.json 파일에 저장하고 관리하는 클래스&quot;&quot;&quot;

    def __init__(self, file_path=&quot;member.json&quot;):
        # 저장할 JSON 파일의 경로를 Path 객체로 설정
        self.file_path = Path(file_path)

    def default_data(self):
        # 파일이 없거나 오류가 났을 때 사용할 기본 데이터 구조 생성
        return {&quot;members&quot;: []}

    def load_data(self):
        # 1. 파일이 존재하지 않는 경우 처리
        if not self.file_path.exists():
            data = self.default_data()  # 기본 데이터 생성
            self.save_data(data)  # 새 파일로 저장
            return data  # 데이터 반환

        # 2. 파일이 있는 경우 읽어오기 시도
        try:
            with self.file_path.open(&quot;r&quot;, encoding=&quot;utf-8&quot;) as file:
                data = json.load(file)  # JSON 파일 내용 읽기
        except json.JSONDecodeError:
            # 파일이 깨져서 읽을 수 없는 오류 발생 시 처리
            data = self.default_data()  # 기본 데이터로 초기화
            self.save_data(data)  # 초기화된 데이터 저장
            return data  # 데이터 반환

        # 3. 파일은 올바르나 &quot;members&quot; 키가 없는 경우 처리
        if &quot;members&quot; not in data:
            data = self.default_data()  # 기본 데이터로 초기화
            self.save_data(data)  # 초기화된 데이터 저장

        return data  # 최종 데이터 반환

    def save_data(self, data):
        # 데이터를 JSON 파일로 저장 (한글 깨짐 방지 및 들여쓰기 4칸 설정)
        with self.file_path.open(&quot;w&quot;, encoding=&quot;utf-8&quot;) as file:
            json.dump(data, file, ensure_ascii=False, indent=4)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑤ password_utils.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918480415&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; 회원가입시 비밀번호 해시 함수 구현
# 회원가입을 하면 새 회원 딕셔너리가 members 리스트에 추가 되어야 함

import hashlib


def hash_password(password):
    &quot;&quot;&quot;입력한 비밀번호를 SHA-256 해시 문자열로 변환합니다.&quot;&quot;&quot;
    return hashlib.sha256(password.encode(&quot;utf-8&quot;)).hexdigest()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑥ tab_window.py&lt;/p&gt;
&lt;pre id=&quot;code_1780918518174&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# PySide6에서 UI 구성을 위해 필요한 위젯 및 레이아웃 클래스들을 임포트합니다.
from PySide6.QtWidgets import (
    QWidget,
    QTabWidget,
    QLabel,
    QLineEdit,
    QPushButton,
    QVBoxLayout,
    QHBoxLayout,
)


# [로그인 화면 컴포넌트] 사용자의 인증을 처리하고 UI 상태를 동기화하는 클래스입니다.
class LoginView(QWidget):
    def __init__(self, member_model, tab_widget, main_view, profile_view):
        super().__init__()

        # 의존성 주입: 회원 데이터 관리 모델 및 화면 전환을 위한 컨트롤러들을 저장합니다.
        self.member_model = member_model
        self.tab_widget = tab_widget
        self.main_view = main_view
        self.profile_view = profile_view

        # 아이디 입력 창을 생성하고 안내 문구를 설정합니다.
        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;아이디를 입력하세요&quot;)

        # 비밀번호 입력 창을 생성하고 입력 값이 마스킹(●)되도록 설정합니다.
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호를 입력하세요&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        # 로그인 버튼을 생성하고 클릭 시 login 메서드가 실행되도록 연결합니다.
        self.login_button = QPushButton(&quot; 로그인&quot;)
        self.login_button.clicked.connect(self.login)

        # 로그인 진행 상태 및 결과 메시지를 보여줄 라벨을 생성합니다.
        self.message_label = QLabel(&quot;로그인 정보를 입력하세요.&quot;)

        # 위젯들을 세로 방향으로 순서대로 배치하기 위한 레이아웃을 생성하고 설정합니다.
        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot; 로그인&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        layout.addWidget(self.pw_input)
        layout.addWidget(self.login_button)
        layout.addWidget(self.message_label)

        # 구성된 레이아웃을 현재 로그인 뷰 위젯에 적용합니다.
        self.setLayout(layout)

    # 로그인 버튼 클릭 시 호출되는 이벤트 처리 메서드입니다.
    def login(self):
        # 사용자가 입력한 아이디와 비밀번호 문자열을 가져옵니다.
        user_id = self.id_input.text()
        password = self.pw_input.text()

        # 회원 데이터 모델을 통해 계정 정보의 유효성을 검증합니다.
        if self.member_model.check_login(user_id, password):
            self.message_label.setText(&quot;로그인 성공!&quot;)

            # 로그인이 성공하면 메인 화면과 프로필 화면의 데이터를 최신 상태로 갱신합니다.
            self.main_view.refresh()
            self.profile_view.refresh()

            # 로그인 성공 후 탭 위젯의 인덱스를 변경하여 3번째 탭(인덱스 2: 메인화면)으로 자동 이동합니다.
            self.tab_widget.setCurrentIndex(2)
        else:
            # 인증 실패 시 안내 메시지를 붉은 경고 형태로 라벨에 표시합니다.
            self.message_label.setText(&quot;❌로그인 실패! 아이디 또는 비밀번호를 확인하세요.&quot;)


# [회원가입 화면 컴포넌트] 신규 회원의 정보를 입력받아 등록을 처리하는 클래스입니다.
class SignupView(QWidget):
    def __init__(self, member_model, tab_widget):
        super().__init__()

        # 의존성 주입: 회원 등록 처리를 위한 모델과 화면 전환용 탭 위젯을 저장합니다.
        self.member_model = member_model
        self.tab_widget = tab_widget

        # 아이디 입력 필드를 생성합니다.
        self.id_input = QLineEdit()
        self.id_input.setPlaceholderText(&quot;아이디&quot;)

        # 보안을 위해 마스킹 처리된 비밀번호 입력 필드를 생성합니다.
        self.pw_input = QLineEdit()
        self.pw_input.setPlaceholderText(&quot;비밀번호&quot;)
        self.pw_input.setEchoMode(QLineEdit.EchoMode.Password)

        # 비밀번호 오입력 방지를 위한 비밀번호 확인 필드를 생성합니다.
        self.pw_confirm_input = QLineEdit()
        self.pw_confirm_input.setPlaceholderText(&quot;비밀번호 확인&quot;)
        self.pw_confirm_input.setEchoMode(QLineEdit.EchoMode.Password)

        # 이름, 이메일, 전화번호를 입력받기 위한 각각의 필드를 생성합니다.
        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText(&quot;이름&quot;)

        self.email_input = QLineEdit()
        self.email_input.setPlaceholderText(&quot;이메일&quot;)

        self.phone_input = QLineEdit()
        self.phone_input.setPlaceholderText(&quot;전화번호&quot;)

        # 회원가입 처리 버튼을 생성하고 클릭 이벤트를 signup 메서드에 연결합니다.
        self.signup_button = QPushButton(&quot; 회원가입&quot;)
        self.signup_button.clicked.connect(self.signup)

        # 가입 검증 결과 및 안내를 출력할 메시지 라벨을 생성합니다.
        self.message_label = QLabel(&quot;회원가입 정보를 입력하세요.&quot;)

        # 회원가입에 필요한 모든 입력 양식과 버튼을 세로 레이아웃에 순서대로 추가합니다.
        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot;  회원가입&quot;))
        layout.addWidget(QLabel(&quot;아이디&quot;))
        layout.addWidget(self.id_input)
        layout.addWidget(QLabel(&quot;비밀번호&quot;))
        layout.addWidget(self.pw_input)
        layout.addWidget(QLabel(&quot;비밀번호 확인&quot;))
        layout.addWidget(self.pw_confirm_input)
        layout.addWidget(QLabel(&quot;이름&quot;))
        layout.addWidget(self.name_input)
        layout.addWidget(QLabel(&quot;이메일&quot;))
        layout.addWidget(self.email_input)
        layout.addWidget(QLabel(&quot;전화번호&quot;))
        layout.addWidget(self.phone_input)
        layout.addWidget(self.signup_button)
        layout.addWidget(self.message_label)

        # 구성된 가입 레이아웃을 현재 위젯의 메인 레이아웃으로 설정합니다.
        self.setLayout(layout)

    # 회원가입 버튼 클릭 시 가입 조건 유효성을 검사하고 등록하는 메서드입니다.
    def signup(self):
        # 모든 입력 폼의 텍스트 데이터를 수집합니다.
        user_id = self.id_input.text()
        password = self.pw_input.text()
        password_confirm = self.pw_confirm_input.text()
        name = self.name_input.text()
        email = self.email_input.text()
        phone = self.phone_input.text()

        # 데이터 모델의 register_member 함수에 입력 값을 전달하여 검증 결과(성공 여부, 메시지)를 리턴받습니다.
        success, message = self.member_model.register_member(
            user_id,
            password,
            password_confirm,
            name,
            email,
            phone,
        )

        # 모델이 리턴한 처리 결과 메시지(예: 비밀번호 불일치, 중복 아이디 등)를 화면에 표시합니다.
        self.message_label.setText(message)

        # 회원가입이 최종 성공했을 때 수행할 분기 처리문입니다.
        if success:
            # 가입 입력창에 남아있는 보안 및 텍스트 데이터들을 모두 지워 초기화합니다.
            self.clear_inputs()
            # 사용자가 바로 로그인할 수 있도록 1번째 탭(인덱스 0: 로그인 화면)으로 이동시킵니다.
            self.tab_widget.setCurrentIndex(0)

    # 회원가입 성공 후 또는 폼 초기화가 필요할 때 모든 입력 필드를 비우는 헬퍼 메서드입니다.
    def clear_inputs(self):
        self.id_input.clear()
        self.pw_input.clear()
        self.pw_confirm_input.clear()
        self.name_input.clear()
        self.email_input.clear()
        self.phone_input.clear()


# [메인 화면 컴포넌트] 로그인 성공 후 회원의 상세 요약 정보를 열람하는 클래스입니다.
class MainView(QWidget):
    def __init__(self, member_model):
        super().__init__()

        # 의존성 주입: 세션 정보를 조회할 때 사용할 회원 데이터 모델을 저장합니다.
        self.member_model = member_model

        # 정보를 시각적으로 출력하기 위한 고정 타이틀 및 데이터 출력용 텍스트 라벨들을 정의합니다.
        self.title_label = QLabel(&quot; [메인화면] 로그인한 회원님 정보가 맞는지 확인해 주세요&quot;)
        self.id_label = QLabel(&quot;아이디:&quot;)
        self.name_label = QLabel(&quot;이름:&quot;)
        self.email_label = QLabel(&quot;이메일:&quot;)
        self.phone_label = QLabel(&quot;전화번호:&quot;)

        # 정보를 위아래 일렬로 깔끔하게 정렬하기 위해 세로 레이아웃에 추가합니다.
        layout = QVBoxLayout()
        layout.addWidget(self.title_label)
        layout.addWidget(self.id_label)
        layout.addWidget(self.name_label)
        layout.addWidget(self.email_label)
        layout.addWidget(self.phone_label)

        # 레이아웃 설정을 메인 뷰 위젯에 적용합니다.
        self.setLayout(layout)

    # 로그인 성공 시 또는 프로필 수정 시 최신 유저 세션 정보를 동적으로 반영하는 갱신 메서드입니다.
    def refresh(self):
        # 모델로부터 현재 세션이 유지되고 있는 회원의 정보를 딕셔너리 형태로 가져옵니다.
        member = self.member_model.get_member_info()

        # 로그인된 상태가 아니거나 정보가 비어있다면 텍스트 갱신을 생략하고 메서드를 종료합니다.
        if member is None:
            return

        # 가져온 회원 데이터 구조(Key-Value)를 바탕으로 각 라벨의 텍스트 인터페이스를 동적 업데이트합니다.
        self.id_label.setText(f&quot; ️아이디 : {member['id']}&quot;)
        self.name_label.setText(f&quot; 이름 : {member['name']}&quot;)
        self.email_label.setText(f&quot;✉이메일 : {member['email']}&quot;)
        self.phone_label.setText(f&quot; 전화번호 : {member['phone']}&quot;)


# [프로필 관리 컴포넌트] 로그인된 회원의 정보를 확인하고, 실시간으로 수정 폼을 제공하는 클래스입니다.
class ProfileView(QWidget):
    def __init__(self, member_model, main_view):
        super().__init__()

        # 의존성 주입: 프로필 정보 수정 비즈니스 로직 모델과 상호작용할 메인 뷰 인스턴스를 저장합니다.
        self.member_model = member_model
        self.main_view = main_view

        # 아이디는 수정 불가능한 고정 데이터이므로 라벨(QLabel) 형태로 배치합니다.
        self.id_label = QLabel(&quot;아이디:&quot;)

        # 이름, 이메일, 전화번호는 사용자 변경이 가능하도록 입력 필드(QLineEdit)로 생성합니다.
        self.name_input = QLineEdit()
        self.email_input = QLineEdit()
        self.phone_input = QLineEdit()

        # 수정사항 반영 버튼을 정의하고 클릭 시 update_profile 메서드를 바인딩합니다.
        self.update_button = QPushButton(&quot; 정보수정&quot;)
        self.update_button.clicked.connect(self.update_profile)

        # 프로필 수정 상태 메시지를 표현할 안내 라벨입니다.
        self.message_label = QLabel(&quot;프로필 정보를 확인하거나 수정하세요.&quot;)

        # 레이아웃 중첩 구조 설계: 전체 화면은 세로 배치 구조로 생성합니다.
        layout = QVBoxLayout()
        layout.addWidget(QLabel(&quot; [프로필]&quot;))
        layout.addWidget(self.id_label)

        # 입력 필드의 텍스트 가독성을 높이기 위해 '설명 레이블 + 입력 창'을 가로(Horizontal) 레이아웃으로 개별 묶음 처리합니다.
        name_layout = QHBoxLayout()
        name_layout.addWidget(QLabel(&quot;이름 &quot;))
        name_layout.addWidget(self.name_input)

        email_layout = QHBoxLayout()
        email_layout.addWidget(QLabel(&quot;✉이메일&quot;))
        email_layout.addWidget(self.email_input)

        phone_layout = QHBoxLayout()
        phone_layout.addWidget(QLabel(&quot; 전화번호&quot;))
        phone_layout.addWidget(self.phone_input)

        # 각각 가로로 정렬된 세부 입력 단락 레이아웃들을 전체 메인 세로 레이아웃에 순서대로 병합합니다.
        layout.addLayout(name_layout)
        layout.addLayout(email_layout)
        layout.addLayout(phone_layout)
        layout.addWidget(self.update_button)
        layout.addWidget(self.message_label)

        # 최종 가로&amp;middot;세로 조립이 완료된 복합 레이아웃을 프로필 뷰 위젯에 등록합니다.
        self.setLayout(layout)

    # 메인 윈도우 컨트롤러나 로그인 성공 시 호출되어 현재 로그인 유저의 최신 데이터를 입력 필드에 채워넣는 렌더링 메서드입니다.
    def refresh(self):
        # 데이터 저장소 모델에서 회원 정보를 읽어옵니다.
        member = self.member_model.get_member_info()

        # 비정상 접근이거나 데이터 세션이 유실되었다면 함수를 안전하게 리턴 처리합니다.
        if member is None:
            return

        # 읽어온 회원 오브젝트 정보를 텍스트 및 각 입력 폼의 기본값으로 매핑합니다.
        self.id_label.setText(f&quot;아이디: {member['id']}&quot;)
        self.name_input.setText(member[&quot;name&quot;])
        self.email_input.setText(member[&quot;email&quot;])
        self.phone_input.setText(member[&quot;phone&quot;])

    # 정보수정 버튼 클릭 시 호출되며, 사용자가 새로 입력한 인적사항을 가공 및 업데이트 요청하는 메서드입니다.
    def update_profile(self):
        # 입력 폼에 수정되어 남아있는 텍스트 데이터를 확보합니다.
        name = self.name_input.text()
        email = self.email_input.text()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⑦ member.json&lt;/p&gt;
&lt;pre id=&quot;code_1780918360910&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;members&quot;: [
        {
            &quot;id&quot;: &quot;test11&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;,
            &quot;name&quot;: &quot;늑구시대&quot;,
            &quot;email&quot;: &quot;늑구@email.com&quot;,
            &quot;phone&quot;: &quot;010-1234-5678&quot;
        },
        {
            &quot;id&quot;: &quot;wolf&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;,
            &quot;name&quot;: &quot;wolf&quot;,
            &quot;email&quot;: &quot;wolf@test.com&quot;,
            &quot;phone&quot;: &quot;010-111-2222&quot;
        },
        {
            &quot;id&quot;: &quot;cat&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;,
            &quot;name&quot;: &quot;cat&quot;,
            &quot;email&quot;: &quot;cat@email.com&quot;,
            &quot;phone&quot;: &quot;010-1234-1234&quot;
        },
        {
            &quot;id&quot;: &quot;test1&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;,
            &quot;name&quot;: &quot;test1&quot;,
            &quot;email&quot;: &quot;test20@email.com&quot;,
            &quot;phone&quot;: &quot;010-123-1234&quot;
        },
        {
            &quot;id&quot;: &quot;test22&quot;,
            &quot;password_hash&quot;: &quot;03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4&quot;,
            &quot;name&quot;: &quot;늑구친구&quot;,
            &quot;email&quot;: &quot;test@email.com&quot;,
            &quot;phone&quot;: &quot;010-1234-1234&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/59</guid>
      <comments>https://goodjop79.tistory.com/59#entry59comment</comments>
      <pubDate>Mon, 8 Jun 2026 20:54:36 +0900</pubDate>
    </item>
    <item>
      <title>D+42 [Python] hashlib 뭐야?  어떻게 활용해야 할까?</title>
      <link>https://goodjop79.tistory.com/58</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 8일(월)&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;hashlib 어떻게 활용해야 할까?&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&quot;hashlib&quot;&quot;은 파이썬 표준 라이브러리로, 데이터의 무결성을 검증하거나 비밀번호 등을 안전하게 단방향 암호화할 수 있는 다양한 해시 알고리즘(MD5, SHA-1, SHA-256, SHA-512 등)을 제공한다.&lt;br /&gt;파이썬을 설치하면 별도 설치 없이 import hashlib로 바로 사용할 수 있다. &lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 해시(Hash)의 기초 개념&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단방향성 (One-way) :&lt;/b&gt; 데이터로부터 해시값을 생성할 수는 있지만, 해시값으로부터 원래 데이터를 역산하는 것은 불가능 하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고정된 길이 :&lt;/b&gt; 입력값의 길이에 상관없이 항상 동일한 길이의 해시값을 출력한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;눈사태 효과 (Avalanche Effect) :&lt;/b&gt; 입력값이 단 1글자만 달라져도 완전히 다른 해시값이 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. hashlib 기본 사용법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1 지원하는 해시 알고리즘 확인&amp;nbsp;&lt;/b&gt;&lt;br /&gt;시스템에서 사용할 수 있는 알고리즘 목록을 확인할 수 있다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import hashlib

# 항상 제공되는 안전한 알고리즘 목록
print(&quot;guaranteed:&quot;, hashlib.algorithms_guaranteed)

# 현재 시스템의 OpenSSL 라이브러리에 따라 사용 가능한 전체 알고리즘 목록
print(&quot;available:&quot;, hashlib.algorithms_available)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.2 문자열 해시값 생성 (기본 공식)&lt;/b&gt;&lt;br /&gt;해시 함수에 문자열을 전달할 때는 반드시 &lt;b&gt;바이트(bytes) 객체로 인코딩&lt;/b&gt;. 파이썬의 기본 문자열은 유니코드이기 때문&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;import hashlib

# 1. 대상 문자열 정의
message = &quot;Hello, Python!&quot;

# 2. SHA-256 해시 객체 생성 및 데이터 업데이트 (바이트로 인코딩 필수)
hash_object = hashlib.sha256()
hash_object.update(message.encode('utf-8'))

# 3. 해시 결과값 추출
# - digest(): 바이트 형태의 결과값 반환
# - hexdigest(): 16진수 문자열 형태의 결과값 반환 (가장 많이 사용됨)
hex_result = hash_object.hexdigest()

print(f&quot;원본: {message}&quot;)
print(f&quot;SHA-256 해시값: {hex_result}&quot;)
# 출력: a6ef198a0d0a7900b92f7411639f76a54911bf047f3e8f8045610ec87019a69a
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.3 한 줄로 축약해서 표현하기&lt;/b&gt;&lt;br /&gt;생성과 인코딩을 한 번에 처리하는 간결한 문법&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import hashlib

# 생성자 내부에 바로 바이트 데이터를 넘겨 처리
hex_result = hashlib.sha256(b&quot;Hello, Python!&quot;).hexdigest()
print(hex_result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;※ &lt;/b&gt;접두어 b를 붙이는 형태(b&quot;string&quot;)는 영문/숫자 리터럴에만 작동. 한글 등이 포함된 문자열은 반드시 .encode('utf-8')을 사용&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 대용량 파일 해시 생성하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용량이 큰 파일은 한 번에 메모리에 올리면 메모리 부족 오류가 발생할 수 있다. 파일을 조각(Chunk) 단위로 읽어서 해시 객체를 점진적으로 업데이트 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import hashlib

def calculate_file_hash(filepath, algorithm=&quot;sha256&quot;):
    # 명시된 알고리즘 이름으로 해시 객체 동적 생성
    hash_obj = hashlib.new(algorithm)
    
    # 64KB(65,536 바이트) 크기로 파일을 분할해서 읽기
    chunk_size = 65536
    
    with open(filepath, 'rb') as f:
        while chunk := f.read(chunk_size):
            hash_obj.update(chunk)
            
    return hash_obj.hexdigest()

# 사용 예시:
# file_sha256 = calculate_file_hash(&quot;large_video.mp4&quot;)
# print(file_sha256)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 비밀번호 암호화와 보안 가이드&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 hashlib.sha256(&quot;password&quot;.encode()) 형태로만 비밀번호를 암호화하면 레인보우 테이블(Rainbow Table)이라는 이미 계산된 해시 테이블을 통해 쉽게 해킹당할 수 있다. 이를 방지하기 위해 반드시 솔트(Salt)와 키 스트레칭(Key Stretching)을 사용.&lt;br /&gt;&lt;b&gt;&lt;i&gt;4.1 솔트(Salt)란?&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;비밀번호 해싱 전에 무작위 문자열(솔트)을 덧붙여 해싱하는 기법, 사용자마다 다른 고유의 솔트를 사용해야 보안이 유지됨&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;import hashlib
import os

password = &quot;my_secure_password&quot;

# 1. 안전한 무작위 솔트 생성 (최소 16바이트 이상 권장)
salt = os.urandom(16)

# 2. 패스워드와 솔트를 합쳐서 해싱
hash_obj = hashlib.sha256()
hash_obj.update(salt)
hash_obj.update(password.encode('utf-8'))

key = hash_obj.hexdigest()

print(f&quot;솔트 (HEX): {salt.hex()}&quot;)
print(f&quot;최종 저장할 해시값: {key}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;※ 검증할 때 동일한 솔트가 필요하므로, 데이터베이스에는 [생성된 솔트]와 [최종 해시값]을 둘 다 저장해야 함&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.2.&amp;nbsp; PBKDF2 알고리즘 사용 (권장 표준)&lt;/b&gt;&lt;br /&gt;hashlib.pbkdf2_hmac은 솔트 적용과 키 스트레칭(수만 번 반복 해싱)을 내부적으로 안전하게 처리해 주는 표준 API 이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;import hashlib
import os

password = &quot;user_password_123&quot;

# 1. 무작위 솔트 생성
salt = os.urandom(16)

# 2. PBKDF2를 사용하여 비밀번호 해싱
# 매개변수: (알고리즘명, 비밀번호바이트, 솔트, 반복횟수, 출력바이트길이)
dk = hashlib.pbkdf2_hmac(
    'sha256', 
    password.encode('utf-8'), 
    salt, 
    100000, # 반복 횟수 (기본 10만 번 이상 권장, 숫자가 높을수록 느리지만 안전함)
    dklen=32 # 출력되는 키의 길이 (바이트 단위)
)

# 데이터베이스에 저장할 값들
stored_salt = salt.hex()
stored_hash = dk.hex()

print(f&quot;DB 저장용 솔트: {stored_salt}&quot;)
print(f&quot;DB 저장용 비밀번호 해시: {stored_hash}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4.3 비밀번호 검증 (Verification) 과정&lt;/b&gt;&lt;br /&gt;사용자가 로그인할 때 입력한 비밀번호가 올바른지 검증하는 구조&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def verify_password(input_password, stored_salt_hex, stored_hash_hex):
    # 1. DB에서 가져온 16진수 문자열 솔트를 원래 바이트로 복원
    salt = bytes.fromhex(stored_salt_hex)
    
    # 2. 동일한 매개변수와 솔트로 입력된 비밀번호를 다시 해싱
    dk = hashlib.pbkdf2_hmac(
        'sha256', 
        input_password.encode('utf-8'), 
        salt, 
        100000,
        dklen=32
    )
    
    # 3. 해시값 일치 여부 비교
    # 단순 == 비교보다 timing attack을 방지하는 hmac.compare_digest() 사용이 더 안전합니다.
    import hmac
    return hmac.compare_digest(dk.hex(), stored_hash_hex)

# 검증 테스트
is_valid = verify_password(&quot;user_password_123&quot;, stored_salt, stored_hash)
print(f&quot;비밀번호 검증 결과: {is_valid}&quot;) # True
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. 핵심 요약 및 주의 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인코딩 필수 :&lt;/b&gt; hashlib 함수는 항상 유니코드 문자열이 아닌 bytes 타입을 요구하므로 반드시 .encode('utf-8')을 거치거나 바이트 데이터를 사용 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MD5 및 SHA-1 사용 지양 :&lt;/b&gt; 이 알고리즘들은 이미 충돌 취약점이 발견되었다. 보안이 중요한 데이터(예: 비밀번호)에는 절대 사용하지 말고 무결성 체크 등의 단순 작업에만 제한적으로 사용할 수도 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비밀번호 저장 최선책 :&lt;/b&gt; 단순 SHA-256 사용은 위험하다. 최소한 파이썬 표준 라이브러리의 hashlib.pbkdf2_hmac을 사용하거나, 외부 패키지인 bcrypt 또는 argon2 라이브러리를 추가하여 활용하는 것이 최선의 보안 대책이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&quot; hashlib &quot; 은 비전공자들에게 생소하고 즉시 활용하기는 다소 어렵다. 용어부터가 이해하기 어렵기 때문에 소스코드를 통해 이해하고자 인공지능 참고하여 실습 및 공부했다. 당장 활용하기에는 쉽지 않더라도 작성하면서 고민도 해 보고 이해하려고 자료도 찾아보는 등의 경험을 했다는 것만으로도 도움이 되는것 같다. 우리가 자주 사용하는 비밀번호 체계 등이 이러한 코드에서 구현되고 있다는 것은 비전공자에게 생소하기도 하다. 참으로 어려운 활용법이다. 이해하는데 시간이 걸리겠다. &lt;br /&gt;그래도 &quot;도전은 계속된다.&quot;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends2&quot; data-emoticon-name=&quot;015&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends2/large/015.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends2/large/015.png&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/58</guid>
      <comments>https://goodjop79.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 8 Jun 2026 07:31:35 +0900</pubDate>
    </item>
    <item>
      <title>D+41[Python] Resource &amp;amp; 메모장 예제 실습</title>
      <link>https://goodjop79.tistory.com/57</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 5일(금)&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Resource 이해하기&amp;nbsp;&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1.1. GUI 프로그램 리소스?&amp;nbsp; &lt;span style=&quot;color: #212529;&quot;&gt;Graphical User Interface&lt;/span&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;GUI 프로그램에서 &lt;span style=&quot;color: #8A3DB6;&quot;&gt;&lt;b&gt;리소스(Resource)&lt;/b&gt;란 프로그램 코드(로직) 자체를 제외한 &lt;/span&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;&lt;b&gt;시각적 요소와 외부 데이터&lt;/b&gt;를 통칭,&lt;/span&gt;&amp;nbsp;주로 &lt;b&gt;이미지, 아이콘, 폰트, 오디오, 텍스트(문구)&lt;/b&gt; 등이 포함되며, 프로그램 실행 시 불러와 화면에 렌더링되거나 동작을 돕는 데이터 자원이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Python 환경에서 리소스 예제 실습&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NzsJh/dJMcahdCupf/QbnYZQ3Q8cX9rD65yriYj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NzsJh/dJMcahdCupf/QbnYZQ3Q8cX9rD65yriYj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NzsJh/dJMcahdCupf/QbnYZQ3Q8cX9rD65yriYj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNzsJh%2FdJMcahdCupf%2FQbnYZQ3Q8cX9rD65yriYj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;752&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3. PyQT 리소스 디자인 &quot;메모장&quot; 예제 실습 &lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (1) &lt;/b&gt;&lt;b&gt; .ui 파일에서 디자인 요소를 입력 : 텍스트, 개체이름, 도구설명, 아이콘테마, 확인, 단축키 누르기, 메뉴역할 등&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nae7o/dJMcaaZP4iL/SuOuGwRU5227T4RI8kXAzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nae7o/dJMcaaZP4iL/SuOuGwRU5227T4RI8kXAzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nae7o/dJMcaaZP4iL/SuOuGwRU5227T4RI8kXAzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNae7o%2FdJMcaaZP4iL%2FSuOuGwRU5227T4RI8kXAzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;923&quot; height=&quot;898&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (2) 그림아이콘도 액션 애디터에서 선택하여 설정 가능&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EV4S7/dJMcaaZP4iN/qydnvYkR4tCzj6qfTBmk90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EV4S7/dJMcaaZP4iN/qydnvYkR4tCzj6qfTBmk90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EV4S7/dJMcaaZP4iN/qydnvYkR4tCzj6qfTBmk90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEV4S7%2FdJMcaaZP4iN%2FqydnvYkR4tCzj6qfTBmk90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1211&quot; height=&quot;961&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (3) 액션 애디터에서 텍스트만 입력하면 name 자동입력, 단축키(Shortcut)는 입력칸에 단축키를 키보드로 입력시 자동입력됨&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;957&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vaMZN/dJMcaaZP4iM/5UJLKmd1nzZiQe4l8m16H1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vaMZN/dJMcaaZP4iM/5UJLKmd1nzZiQe4l8m16H1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vaMZN/dJMcaaZP4iM/5UJLKmd1nzZiQe4l8m16H1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvaMZN%2FdJMcaaZP4iM%2F5UJLKmd1nzZiQe4l8m16H1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1456&quot; height=&quot;957&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;957&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (4&lt;/b&gt;&lt;b&gt;) 액션 요소들은&amp;nbsp; Property Editor (노란 항목들)에서 세부 사항을 클릭하여 각각 수정할 수 있다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OZukm/dJMcajoRuDT/EPM3gV0cOekkp4ZtyhvtJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OZukm/dJMcajoRuDT/EPM3gV0cOekkp4ZtyhvtJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OZukm/dJMcajoRuDT/EPM3gV0cOekkp4ZtyhvtJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOZukm%2FdJMcajoRuDT%2FEPM3gV0cOekkp4ZtyhvtJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1023&quot; height=&quot;988&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;4. Python 환경에서 &quot;메모장&quot; 예제 실습 (이미지 미포함)&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/86apA/dJMcaayOfnV/sGFKkQmBickmWn2WO4mlsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/86apA/dJMcaayOfnV/sGFKkQmBickmWn2WO4mlsK/img.png&quot; data-alt=&quot;메모장 만들기 예제 실습 화면 (이미지 미포함)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/86apA/dJMcaayOfnV/sGFKkQmBickmWn2WO4mlsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F86apA%2FdJMcaayOfnV%2FsGFKkQmBickmWn2WO4mlsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;958&quot; height=&quot;792&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;메모장 만들기 예제 실습 화면 (이미지 미포함)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1027&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rR3sy/dJMcaip2Ic2/YRZAKiGzEnV896VOLd9SQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rR3sy/dJMcaip2Ic2/YRZAKiGzEnV896VOLd9SQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rR3sy/dJMcaip2Ic2/YRZAKiGzEnV896VOLd9SQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrR3sy%2FdJMcaip2Ic2%2FYRZAKiGzEnV896VOLd9SQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1115&quot; height=&quot;1027&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1027&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;color: #777777;&quot;&gt;메모장 만들기 예제 실습 화면 &lt;b&gt;(이미지를&amp;nbsp; 코드에 포함하여 작성한 경우)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;※ 소스 코드&lt;/b&gt;&lt;br&gt;&lt;b&gt;① dmainwindow.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# &quot;&quot; 메모장 이미지 포함 예제 실습 패키지 임&quot;&quot;&quot;

import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_form import Ui_MainWindow

class MainWindow(QMainWindow):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, parent=None):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(parent)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ui = Ui_MainWindow()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ui.setupUi(self)

if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app = QApplication(sys.argv)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;widget = MainWindow()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;widget.show()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;② ui_form.py&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt; ※ 프로그램에서 기호 및 이모티콘 이미지가 어떤 로직에 의해 구현되는지 이해하고, 인공지능 응용 참고&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# &quot;&quot; 메모장 이미지 포함 예제 실습 패키지 임&quot;&quot;&quot;

## Form generated from reading UI file 'form.ui'
## Created by: Qt User Interface Compiler version 6.10.1

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMetaObject, QObject, QPoint, QRect,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QCursor, QFont, QFontDatabase, QGradient,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QIcon, QImage, QKeySequence, QLinearGradient,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QPainter, QPalette, QPixmap, QRadialGradient,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QTransform)
from PySide6.QtWidgets import (QApplication, QMainWindow, QMenu, QMenuBar,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QSizePolicy, QStatusBar, QWidget)

class Ui_MainWindow(object):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def setupUi(self, MainWindow):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not MainWindow.objectName():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.setObjectName(u&quot;MainWindow&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.resize(700, 500)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionNew = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionNew.setObjectName(u&quot;actionNew&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionOpen = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionOpen.setObjectName(u&quot;actionOpen&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionSave = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionSave.setObjectName(u&quot;actionSave&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint.setObjectName(u&quot;actionPrint&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint_Preview = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint_Preview.setObjectName(u&quot;actionPrint_Preview&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionExport_PDF = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionExport_PDF.setObjectName(u&quot;actionExport_PDF&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionClose = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionClose.setObjectName(u&quot;actionClose&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUndo = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUndo.setObjectName(u&quot;actionUndo&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRedo = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRedo.setObjectName(u&quot;actionRedo&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCut = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCut.setObjectName(u&quot;actionCut&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCopy = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCopy.setObjectName(u&quot;actionCopy&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPaste = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPaste.setObjectName(u&quot;actionPaste&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionBold = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionBold.setObjectName(u&quot;actionBold&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionItalic = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionItalic.setObjectName(u&quot;actionItalic&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUnderline = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUnderline.setObjectName(u&quot;actionUnderline&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionLeft = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionLeft.setObjectName(u&quot;actionLeft&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRight = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRight.setObjectName(u&quot;actionRight&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCenter = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCenter.setObjectName(u&quot;actionCenter&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionJustify = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionJustify.setObjectName(u&quot;actionJustify&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionFont = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionFont.setObjectName(u&quot;actionFont&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionColor = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionColor.setObjectName(u&quot;actionColor&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionAbout_App = QAction(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionAbout_App.setObjectName(u&quot;actionAbout_App&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.centralwidget = QWidget(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.centralwidget.setObjectName(u&quot;centralwidget&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.setCentralWidget(self.centralwidget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar = QMenuBar(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.setObjectName(u&quot;menubar&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.setGeometry(QRect(0, 0, 700, 22))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile = QMenu(self.menubar)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.setObjectName(u&quot;menuFile&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit = QMenu(self.menubar)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.setObjectName(u&quot;menuEdit&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat = QMenu(self.menubar)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.setObjectName(u&quot;menuFormat&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuAbout = QMenu(self.menubar)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuAbout.setObjectName(u&quot;menuAbout&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.setMenuBar(self.menubar)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.statusbar = QStatusBar(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.statusbar.setObjectName(u&quot;statusbar&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.setStatusBar(self.statusbar)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.addAction(self.menuFile.menuAction())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.addAction(self.menuEdit.menuAction())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.addAction(self.menuFormat.menuAction())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menubar.addAction(self.menuAbout.menuAction())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionNew)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionOpen)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionSave)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addSeparator()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionPrint)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionPrint_Preview)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionExport_PDF)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addSeparator()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.addAction(self.actionClose)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addAction(self.actionUndo)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addAction(self.actionRedo)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addSeparator()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addAction(self.actionCut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addAction(self.actionCopy)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.addAction(self.actionPaste)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionBold)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionItalic)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionUnderline)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addSeparator()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionLeft)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionRight)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionJustify)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addSeparator()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionFont)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.addAction(self.actionColor)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuAbout.addAction(self.actionAbout_App)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.retranslateUi(MainWindow)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMetaObject.connectSlotsByName(MainWindow)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# setupUi

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def retranslateUi(self, MainWindow):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MainWindow.setWindowTitle(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot; ️ MainWindow&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionNew.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  New&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionNew.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+N&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionOpen.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Open&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionOpen.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+O&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionSave.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Save&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionSave.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+S&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot; ️ Print&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+P&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint_Preview.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Print Preview&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPrint_Preview.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+Shift+P&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionExport_PDF.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Export PDF&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionExport_PDF.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+E&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionClose.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;❌ Close&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionClose.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+Q, Return&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUndo.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;↩️ Undo&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUndo.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+Z&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRedo.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;↪️ Redo&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRedo.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+Y&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCut.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;✂️ Cut&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCut.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+X&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCopy.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Copy&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCopy.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+C&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPaste.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Paste&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionPaste.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+V&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionBold.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;&amp;lt;b&amp;gt;B&amp;lt;/b&amp;gt; Bold&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionBold.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+B&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionItalic.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;&amp;lt;i&amp;gt;I&amp;lt;/i&amp;gt; Italic&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionItalic.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+I&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUnderline.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;&amp;lt;u&amp;gt;U&amp;lt;/u&amp;gt; Underline&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionUnderline.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+E&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionLeft.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;⬅️ Left&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionLeft.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+L&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRight.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;➡️ Right&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionRight.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+R&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCenter.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;↕️ Center&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionCenter.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+K&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionJustify.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;↔️ Justify&quot;, None))
#if QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionJustify.setShortcut(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;Ctrl+J&quot;, None))
#endif // QT_CONFIG(shortcut)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionFont.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Font&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionColor.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Color&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.actionAbout_App.setText(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;ℹ️ About App&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFile.setTitle(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  File&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuEdit.setTitle(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;  Edit&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuFormat.setTitle(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot; ️ Format&quot;, None))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.menuAbout.setTitle(QCoreApplication.translate(&quot;MainWindow&quot;, u&quot;❓ About&quot;, None))&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cldwmm/dJMcadCaxOq/r2Gjah3APyCR0OVGtqbPTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cldwmm/dJMcadCaxOq/r2Gjah3APyCR0OVGtqbPTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cldwmm/dJMcadCaxOq/r2Gjah3APyCR0OVGtqbPTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcldwmm%2FdJMcadCaxOq%2Fr2Gjah3APyCR0OVGtqbPTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;228&quot; data-origin-width=&quot;414&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;5. 가상환경 채팅 환경설정 (Ollama) 및 준비&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;b&gt; &amp;nbsp; 가. 환경설정 (터미널에서 설정하기 실습)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ollama -v (버젼 확인), sudo systemctl status ollama(실행상태 확인), ollama pull llama3.2:3b (실습모델 다운로드),&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ollama run llama3.2:3b (터미널 모델 응답 확인), curl 활용 api/chat 호출 결과 확인, test_ollama.py (호출 결과 확인)&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 나. 파이썬 환경에서 가상환경 폴더 설정&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;b&gt; &amp;nbsp; 다. 환경설정 후 모델응답 확인 화면&amp;nbsp; &amp;nbsp;&lt;/b&gt; &amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;851&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boIxFw/dJMcajoRuDP/5s16hs5M4aS9MbZL1vXxA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boIxFw/dJMcajoRuDP/5s16hs5M4aS9MbZL1vXxA0/img.png&quot; data-alt=&quot;실습모델 환경설정 및 인공지능 기업질문 채팅 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boIxFw/dJMcajoRuDP/5s16hs5M4aS9MbZL1vXxA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboIxFw%2FdJMcajoRuDP%2F5s16hs5M4aS9MbZL1vXxA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;822&quot; height=&quot;851&quot; data-origin-width=&quot;822&quot; data-origin-height=&quot;851&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실습모델 환경설정 및 인공지능 기업질문 채팅 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/57</guid>
      <comments>https://goodjop79.tistory.com/57#entry57comment</comments>
      <pubDate>Fri, 5 Jun 2026 19:33:25 +0900</pubDate>
    </item>
    <item>
      <title>D+40 [Python]  File작성 및 JSON활용, 프로젝트 로직 이해하기</title>
      <link>https://goodjop79.tistory.com/56</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 4일(목)&lt;/b&gt;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 내용 :&amp;nbsp; File작성,&amp;nbsp;JSON활용,&amp;nbsp;프로젝트&amp;nbsp;로직&amp;nbsp;이해&lt;/b&gt;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 로직이란?&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIStUb/dJMcabYLTRt/qPsMGsoneRtIuOQiKzbXCK/tfile.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIStUb/dJMcabYLTRt/qPsMGsoneRtIuOQiKzbXCK/tfile.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIStUb/dJMcabYLTRt/qPsMGsoneRtIuOQiKzbXCK/tfile.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIStUb%2FdJMcabYLTRt%2FqPsMGsoneRtIuOQiKzbXCK%2Ftfile.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;185&quot; height=&quot;101&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;주어진 상황과 조건에 따라 프로그램이 어떻게 동작해야하는지를 정의하고, 프로그램이 문제를&amp;nbsp; 해결하기 위해 수행하는 일련의 규칙과 절차를 말한다.&lt;br&gt;즉, 프로그램이 어떻게 실행되고 동작하는지, 그 논리적인 흐름 이다.&lt;/p&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 교육 후기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;어느덧 비전공자로서 교육 40일째다. 그동안 수많은 컴퓨터 용어들과 씨름하면서 지금도 로직을 이해하기 위해 애쓰고 있고, 이해가 잘 안되는 부분도 많지만 참 흥미롭다.&lt;br&gt;프로그램 소스코드(문서)들과 AI, 참고자료, 영상 등 찾아가면서 진행하고 있다. 방금 이해하고도 뒤돌아서면 잊어버리곤 하지만,&amp;nbsp; 지속적인 반복 학습과 꾸준한 공부습관만큼 중요한 것은 없는 것 같다.&amp;nbsp;&lt;br&gt;레이아웃, 시그널과 슬롯, 파일작성 및 분리, 파일 요구사항 추가 작성, JSON을 활용한 &amp;nbsp;DB 정리추출&lt;br&gt;프로젝트의 중요성과 어려움을 실감하고 있다. 그러나 시간이 갈수록 흥미롭다. 소스코드들이 하나씩 눈에 들어오고 기본 내용들을 수정해 가면서 이해하고자 한다.&amp;nbsp; 6. 8.(월) 에는 서울대에 앤비디아 젠슨황이 방문한다. 대학생들에게 어떠한 미래비젼과 사업적 수단을 갖고 어떤 시그널을 줄지가 궁금하다.&amp;nbsp;과거에는 관심이 없었던 개발자와 사업가 들에 대한 관심도 점덤 더 커져가고 있다.&amp;nbsp;&lt;br&gt;MS도스, 윈도우, AI, 시티폰, 삐삐, 핸드폰, 스마트폰 세상은 너무도 빠르게 변하고 있음을 실감한다.. 거리의 어르신들이 택시를 잡으려고 노려하는 모습들, 키오스크에 노부부가 한참을 쳐다보면 유튜브 영상을 보면서 절차데로 하는 모습 등은 과거에는 상상도 할 수 없었던 모습들이다. 이 모든것이 IT 시대에 AI까지 가세하면서 변하는 세상에 적응해 가는 우리들의 모습이 아닐까?&amp;nbsp;&lt;br&gt;오늘도 &quot;도전은 계속된다.&quot;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;1. 프로그램 흐름도 (예시)&amp;nbsp; &amp;nbsp;&lt;/b&gt;GUI → MV → MV-LLM → MV →MV →GUI →MV →.JSON&lt;u&gt;&lt;br&gt;&lt;/u&gt;JSON은 주로 '키(Key)와 값(Value)'의 쌍으로 이루어져 있으며, 구조가 매우 직관적. 파이썬의 딕셔너리(Dictionary)나 자바스크립트의 객체(Object)와 거의 똑같이 생겼다&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oosO5/dJMcafUo6DF/YmetvpgnIt5EXk7i2o4Tn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oosO5/dJMcafUo6DF/YmetvpgnIt5EXk7i2o4Tn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oosO5/dJMcafUo6DF/YmetvpgnIt5EXk7i2o4Tn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoosO5%2FdJMcafUo6DF%2FYmetvpgnIt5EXk7i2o4Tn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;2. File&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;가. &quot;외부&quot; Data 저장, 과거에는 Text로 저장했는데 현재는 자료가 많아지면서 &quot;DB&quot;에 저장한다.&lt;br&gt;나. 네트웍으로 외부에 연결해서 다른 파일을 불러온다는 것은 같은 의미, 저장된&amp;nbsp;공간에서&amp;nbsp;파일을&amp;nbsp;읽고&amp;nbsp;쓰는&amp;nbsp;것이&amp;nbsp;같다.&lt;br&gt;다. 파일을 읽고 쓰는 것과 데이터를 읽고 쓰는 것, 네트웍을 읽고 쓰는 것은 모두 동일하다&lt;br&gt;라. 파일에 작성을 해서 파일을 불러올 수 있는 학습하기&lt;br&gt;&lt;b&gt;2.1. File 다루기&amp;nbsp;&lt;/b&gt;&lt;br&gt;가.&amp;nbsp;파일을&amp;nbsp;읽어오는데&amp;nbsp;열고&amp;nbsp;나서&amp;nbsp;반드시&amp;nbsp;닫아주는&amp;nbsp;명령어&amp;nbsp;작성&amp;nbsp;해&amp;nbsp;주어야&amp;nbsp;한다.&amp;nbsp;&lt;br&gt;나.&amp;nbsp;옵션의&amp;nbsp;역할이&amp;nbsp;중요도는&amp;nbsp;낮지만&amp;nbsp;이해하기&amp;nbsp;(쓰기전용,&amp;nbsp;읽기&amp;nbsp;전용&amp;nbsp;등&amp;nbsp;제한사항을&amp;nbsp;부여한다)&lt;br&gt;다. 인코딩 : 문자열을 utf-8 방식으로 저장했을 때 문자열이 깨질수 있다. 저장하는 패턴이 다를때 문제 발생&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ※ 해결 방법 : QTextStream 사용해서 텍스트 파일을 다룬다&lt;br&gt;라. 회원가입 기능 활용 file로 저장하고, json으로 읽고 불러온다.&amp;nbsp;&lt;br&gt;마. 로직과 데이터를 다루는 UI가 포함되므로서 복잡도가 상승한다&lt;br&gt;&lt;b&gt;예시) Project 구조 (.Py파일)&lt;/b&gt;&lt;br&gt;&amp;nbsp;├─ main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 프로그램 시작 코드&lt;br&gt;&amp;nbsp;├─&amp;nbsp;model_log.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;공통&amp;nbsp;데이터&amp;nbsp;모델&lt;br&gt;&amp;nbsp;├─&amp;nbsp;view_scroll.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;QScrollArea&amp;nbsp;기반&amp;nbsp;View&lt;br&gt;&amp;nbsp;├─&amp;nbsp;view_list.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;QListWidget&amp;nbsp;기반&amp;nbsp;View&lt;br&gt;&amp;nbsp;├─&amp;nbsp;view_textedit.py&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;QTextEdit&amp;nbsp;기반&amp;nbsp;View&lt;br&gt;&amp;nbsp;└─&amp;nbsp;widget.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;세&amp;nbsp;View를&amp;nbsp;QTabWidget으로&amp;nbsp;묶는&amp;nbsp;메인&amp;nbsp;화면&lt;br&gt;&lt;br&gt;&lt;b&gt;2.2. 실행&amp;nbsp;흐름&lt;/b&gt;&lt;br&gt;가. main.py 실행&amp;nbsp; → LogModel 생성&amp;nbsp; → ViewWidget 생성 → ScrollView / ListView / TextEditView에 같은 LogModel 전달 →&lt;br&gt;&lt;b&gt;→ &lt;/b&gt;각 View가 logsChanged 시그널에 refresh_view() 연결 → 로그&amp;nbsp;추가&amp;nbsp;시&amp;nbsp;모든&amp;nbsp;View가&amp;nbsp;자동&amp;nbsp;갱신&lt;br&gt;나. LogModel은 로그 데이터를 저장하고, View들은 LogModel의 logs()를 읽어서 화면을 그린다.&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;※ 참고 사항&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;1.&amp;nbsp; IT 발전지연 사례 (공인인증서)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;공인인증서는 &quot;한국의 대표적인 IT 및 신산업 발전 지연 사례&quot;로, 각각 규제 기술 강제(공인인증서)로 인해 혁신이 가로막혔던 사건&lt;br&gt;&lt;br&gt;&lt;b&gt;1.1. 공인인증서 (IT 서비스 및 핀테크 발전 지연)지연 원인&lt;/b&gt;&lt;br&gt;1999년부터 21년간 특정전자서명기술(공인인증서)을 법적으로 강제하며, 액티브X(ActiveX) 등 보안 프로그램 설치를 의무화 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.2.&amp;nbsp; IT 발전 저해 사례&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; (1) 갈라파고스 규제 : &lt;b&gt;글로벌 웹 표준에 어긋나는 독자적 프로그램 강제로 인해 글로벌 기업의 국내 시장 진입이 어려웠고&lt;/b&gt;, 국내 인터넷 서비스의 편리성이 크게 저해되었다.&lt;br&gt;&amp;nbsp; &amp;nbsp; (2) 혁신 기술 차단 : 블록체인 기반의 분산ID(DID)나 생체인식 등 새롭고 편리한 인증 기술과 신생 기업(스타트업)의 시장 진입이 규제에 막혀 성장이 지연되었다.&lt;br&gt;&amp;nbsp; &amp;nbsp; (3) 그 결과 오랜 논란 끝에 2020년 12월 &quot;전자서명법 개정&quot;으로 공인인증서 제도가 폐지되었고, 이후 패스(PASS), 카카오, 네이버 등 민간 사설인증서 시장이 활성화되었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.3. 규제 완화 이후 발전시킨 사례&lt;/b&gt;&lt;br&gt;주요 플레이어 중심의 시장 재편 독점 체제가 무너진 빈자리를 3대 민간 인증 축이 양분 및 삼분하며 치열하게 경쟁하고 있다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (1) 빅테크·플랫폼 (네이버·카카오·토스)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;국민 메신저와 포털의 접근성을 무기로 시장을 빠르게 장악. 톡지갑이나 앱 내부에서 몇 번의 터치만으로 발급과 인증이 가능하며, 전자고지서 확인부터 페이 결제까지 연동해 생활 밀착형 플랫폼으로 자리 잡았다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (2) 통신 3사 (PASS)&lt;/b&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 휴대폰 본인확인 서비스의 강력한 인프라를 바탕으로 초기 사설 인증 시장을 선점했다. 앱을 통한 간편 로그인과 모바일 신분증 기능 등을 연계하며 높은 점유율을 유지하고 있다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; (3) 금융권 (금융인증서 및 자체 인증)&lt;/b&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;기존 공인인증서를 대체하는 금융결제원의 '금융인증서' 외에도 KB국민, 신한, NH농협 등 주요 은행이 자체 인증서를 발급하고 있다.&amp;nbsp; 최근에는 은행권들이 방송통신위원회로부터 '본인확인기관' 자격까지 연이어 획득하면서 빅테크와 대등한 수준으로 영역을 확장했다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;(4) 공동인증서 (구 공인인증서)&amp;nbsp;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;기존 공인인증서는 명칭이 변경되어 여전히 범용성 높은 수단 중 하나로 선택지로서 생존해 있다. 기술적 장벽이 사라져 실질적인 인터넷 이용 환경이 대폭 개선되었다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;(5)&amp;nbsp;No 플러그인 (액티브X 폐지)&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;EXE 실행파일이나 보안 프로그램을 무분별하게 필수 설치해야 했던 불편함이 사라졌다.&amp;nbsp; 웹 표준(HTML5)과 클라우드 기반 보관 방식을 도입하여 크롬, 사파리, 웨일 등 다양한 브라우저와 모바일 환경에서 제약 없이 구동된다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;(6) 간편한 생체인증&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;매번 외우기 힘들었던 복잡한 대소문자·특수문자 비밀번호 대신 지문, 안면인식(Face ID), 간편 PIN(6자리), 패턴 등으로 3초 만에 인증이 완료된다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &lt;b&gt;&amp;nbsp;(7) 유효기간 연장 및 자동 갱신&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 과거 1년마다 갱신해야 했던 번거로움이 개선되어 민간·금융 인증서의 유효기간은 2~3년으로 늘어났으며, 번거로운 절차 없이 자동 갱신되는 구조로 바뀌었다.&lt;br&gt;＊ 그럼에도 불구하고 편리함 뒤에는 보안의 위험성이 항상 있다. 기업마다 보안 관련 개인정보 보호를 더욱 잘해야 하는 이유이다.&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #006DD7;&quot;&gt;2. 자율주행 차량 &amp;amp; GPU 관련 영상 시청 (#젠슨황)&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACAIQAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt; &lt;b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;2.1.&amp;nbsp; 엔비디아(NVIDIA) GPU와 CUDA의 발전 및 수요 폭증&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;GPU의 진화 (자유도 부여) :&lt;/b&gt; 과거 3D 게임의 단순 그래픽(삼각형) 렌더링에만 쓰이던 GPU를 범용 연산에도 활용할 수 있도록 &lt;b&gt;자유도를 열어주면서 탄생한 기술이 바로 CUDA&lt;/b&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;하드웨어 대중화 :&lt;/b&gt; 엔비디아가 고성능 3D 게임용 그래픽카드를 대량 생산·판매하면서 책상 위에서도 강력한 GPU 파워를 누리는 시대가 열렸다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;수요 폭증 :&lt;/b&gt; GPU에 CUDA가 탑재되면서 그래픽 처리뿐만 아니라 슈퍼컴퓨터, 암호화폐, 블록체인, AI 개발 등 고성능 프로세싱이 필요한 모든 분야로 활용도가 확장되었다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;지속적인 성장 :&lt;/b&gt; AI 및 다양한 기술 생태계가 CUDA는 앞으로도 GPU에 대한 수요는 폭발적으로 증가할 수밖에 없다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.2.&amp;nbsp;&lt;/b&gt;&lt;b&gt;CUDA의 핵심 작동 원리&lt;/b&gt;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 수천 개의 계산을 동시에 처리하는 대규모 병렬 컴퓨팅,&lt;/b&gt; CPU가 한 번에 몇 가지의 복잡한 일을 처리한다면, CUDA는 초등학생 수천 명에게 쉬운 사칙연산을 동시에 시키는 방식으로 속도를 극대화 한다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;가. &amp;nbsp;스레드 구조 (Thread Hierarchy) :&amp;nbsp;&lt;/b&gt;CUDA는 연산의 최소 단위인 &lt;b&gt;스레드(Thread)&lt;/b&gt;를 체계적으로 조직하여 GPU의 수많은 코어에 분배한다.&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 스레드 (Thread) :&lt;/b&gt; 실제 연산을 수행하는 가장 작은 실행 단위&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 블록 (Block) :&lt;/b&gt; 스레드들이 모인 그룹,&amp;nbsp; 같은 블록 내의 스레드들은 서로 데이터를 공유하고 동기화할 수 있다.&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 그리드 (Grid) :&lt;/b&gt; 블록들이 모인 최상위 그룹. 하나의 CUDA 커널(함수)이 실행될 때 생성되는 전체 스레드 영역&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;나. 메모리 구조 (Memory Hierarchy)&lt;/b&gt;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - &lt;/b&gt;&amp;nbsp;&lt;b&gt;레지스터 (Register) :&lt;/b&gt; 각 스레드 전용의 가장 빠른 메모리 공간&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; -&amp;nbsp;&lt;b&gt;공유 메모리 (Shared Memory) :&lt;/b&gt; 같은 블록 내의 스레드들이 고속으로 공동 사용하는 메모리&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 글로벌 메모리 (Global Memory) :&lt;/b&gt; GPU 전체가 공유하는 메모리(VRAM)로, 용량이 크지만 속도가 가장 느리고, &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;CPU에서 GPU로 데이터를 보낼 때 먼저 거치는 곳&lt;br&gt;CUDA는 CPU(Host)의 통제를 받으며, 수많은 &lt;b&gt;스레드와 블록(Grid 구조)&lt;/b&gt;을 활용해 대량의 데이터를 동시에 계산하고, &lt;b&gt;고속 공유 메모리&lt;/b&gt;를 이용해 연산 병목 현상을 해결하는 방식으로 작동한다. 이것을 앤비디아에서 만들어서 세계적 공급망을 확대하고 있다.&amp;nbsp;&lt;br&gt;끝.&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/56</guid>
      <comments>https://goodjop79.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 4 Jun 2026 21:29:16 +0900</pubDate>
    </item>
    <item>
      <title>D+39 [아두이노] Arduino &amp;amp; AI를 응용한 &amp;quot;프로그램 기획서&amp;quot; 작성 예시</title>
      <link>https://goodjop79.tistory.com/55</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 &lt;/b&gt;&lt;b&gt;: 2026&lt;/b&gt;&lt;b&gt;년 &lt;/b&gt;&lt;b&gt;6&lt;/b&gt;&lt;b&gt;월 &lt;/b&gt;&lt;b&gt;2&lt;/b&gt;&lt;b&gt;일&lt;/b&gt;&lt;b&gt;(&lt;/b&gt;&lt;b&gt;화&lt;/b&gt;&lt;b&gt;)&lt;/b&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 실습 과제 &lt;/b&gt;&lt;b&gt;: Arduino &amp;amp; AI를 응용한 &quot;프로그램 기획서&quot; 작성&lt;/b&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 주 제 &lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;파이썬 GUI 프로그램 및 데이터베이스 설치, 파이썬 GUI, MySQL 데이터베이스, 리눅스 지식을 갖춘 개발자들을 위한 2일내&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;br&gt;&amp;nbsp; &amp;nbsp;완성 아두이노 연동 토이 프로젝트 기획안을 작성해 보고 프로그램도 구현해 보기&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;프로젝&lt;/b&gt;&lt;b&gt;트 명 &lt;/b&gt;&lt;b&gt;: &lt;/b&gt;&lt;b&gt;&amp;nbsp;IoT 무인 매장 보안 및 방문객 관리 시스템&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1.1. 시스템 구조 이해하기 (아두이노 - 통신 - 리눅스 - 사용자 인터페이스)&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmlOju/dJMcaf7QTAe/pL6V8qh0mZrXDcc8tSq6RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmlOju/dJMcaf7QTAe/pL6V8qh0mZrXDcc8tSq6RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmlOju/dJMcaf7QTAe/pL6V8qh0mZrXDcc8tSq6RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmlOju%2FdJMcaf7QTAe%2FpL6V8qh0mZrXDcc8tSq6RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;u&gt;☆ 프&lt;/u&gt;&lt;/b&gt;&lt;b&gt;&lt;u&gt;로그램 개발 기획서&amp;nbsp;&lt;/u&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #333333;&quot;&gt;*&amp;nbsp; 자료 별첨 및 시각화 *&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 프로젝트 개요&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;1.1&amp;nbsp;프로젝트&amp;nbsp;배경&amp;nbsp;및&amp;nbsp;목적&lt;/b&gt;&lt;br&gt;최근&amp;nbsp;무인&amp;nbsp;매장의&amp;nbsp;급격한&amp;nbsp;증가로&amp;nbsp;인해&amp;nbsp;절도,&amp;nbsp;기물&amp;nbsp;파손,&amp;nbsp;주취자&amp;nbsp;소란&amp;nbsp;등의&amp;nbsp;보안&amp;nbsp;사고가&amp;nbsp;급증하고&amp;nbsp;있다.&amp;nbsp;상주&amp;nbsp;직원이&amp;nbsp;없는&amp;nbsp;무인&amp;nbsp;매장의&amp;nbsp;특성상,&amp;nbsp;실시간&amp;nbsp;위험&amp;nbsp;감지와&amp;nbsp;즉각적인&amp;nbsp;현장&amp;nbsp;제어&amp;nbsp;시스템의&amp;nbsp;부재는&amp;nbsp;큰&amp;nbsp;경제적&amp;nbsp;손실과&amp;nbsp;치안&amp;nbsp;공백을&amp;nbsp;야기한다.&amp;nbsp;&lt;br&gt;실제로 무인아이스크림 매장에 들렀다가 앞에 고객이 스캔하는 척만 하고 그냥 가버려서 취소해도 다음 결재 진행이 안되는 상황을 목격한 적이 있다.&amp;nbsp; 본&amp;nbsp;프로젝트는&amp;nbsp;이러한&amp;nbsp;한계를&amp;nbsp;극복하기&amp;nbsp;위해&amp;nbsp;&lt;b&gt;아두이노&amp;nbsp;센서&amp;nbsp;네트워크,&amp;nbsp;리눅스&amp;nbsp;중앙&amp;nbsp;서버,&amp;nbsp;MySQL&amp;nbsp;데이터베이스,&amp;nbsp;파이썬&amp;nbsp;GUI&amp;nbsp;관제&amp;nbsp;프로그램을&amp;nbsp;유기적으로&amp;nbsp;연동&lt;/b&gt;하여,&amp;nbsp;&lt;b&gt;'매장 상태 실시간 시각화'와 '원격 현장 제어'가 동시에 가능한 지능형 'IoT 무인 매장 보안 및 방문객 관리 시스템'&lt;/b&gt;을&amp;nbsp;구축하는&amp;nbsp;것을&amp;nbsp;목적으로&amp;nbsp;한다.&lt;br&gt;&lt;br&gt;&lt;b&gt;1.2&amp;nbsp;프로젝트&amp;nbsp;개발&amp;nbsp;범위&lt;/b&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 하드웨어 : 무인 매장 4대 구역(식품, 의류, 가구, 잡화)의 사람 움직임(PIR) 및 소음 레벨(사운드 센서) 데이터 수집, 경보(부저) 및 개폐(서보 모터) 구동 제어&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 미들웨어 / 인프라 : 리눅스 OS 기반 소켓 통신 서버 구축, 보안 로그 파일 관리 및 MySQL 데이터베이스 설계/운영&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 소프트웨어(GUI) : PyQt/Tkinter 기반 실시간 대시보드 화면 구현, 통계 데이터 시각화(그래프), 임계치 초과 시 경고 알림 팝업 및 원격 제어 인터페이스 제공&lt;br&gt;&lt;br&gt;&lt;b&gt;1.3&amp;nbsp;기대&amp;nbsp;효과&lt;/b&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 보안 효율성 극대화 : 다수의 분산된 무인 매장을 단일 관제 화면에서 효율적으로 모니터링 가능&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 즉각적인 초동 조치 : 위급 상황 발생 시 원격 제어를 통해 매장 잠금 및 경보를 작동시켜 2차 피해 방지&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;마케팅&amp;nbsp;활용:&amp;nbsp;매장&amp;nbsp;구역별&amp;nbsp;실시간/시간대별&amp;nbsp;방문객&amp;nbsp;통계&amp;nbsp;데이터를&amp;nbsp;분석하여&amp;nbsp;매장&amp;nbsp;운영&amp;nbsp;및&amp;nbsp;마케팅&amp;nbsp;전략에&amp;nbsp;활용&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br&gt;&lt;b&gt;2. 추진 일정 및 인력 관리 Plan&amp;nbsp; &lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;*&amp;nbsp;&amp;nbsp;자료 별첨, 시각화 *&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;2.1&amp;nbsp;개발&amp;nbsp;일정&amp;nbsp;(2일&amp;nbsp;완성&amp;nbsp;타임라인)&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;본 프로젝트는 기존 보유 역량(Python, DB, Linux)을 기반으로 2일간 단기 집중형(Toy Project)으로 진행된다.&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;[1일&amp;nbsp;차:&amp;nbsp;하드웨어&amp;nbsp;빌드&amp;nbsp;및&amp;nbsp;데이터&amp;nbsp;연동]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;오전&amp;nbsp;(09:00&amp;nbsp;~&amp;nbsp;12:00):&amp;nbsp;아두이노&amp;nbsp;센서&amp;nbsp;회로&amp;nbsp;구성&amp;nbsp;및&amp;nbsp;데이터&amp;nbsp;수집&amp;nbsp;시리얼&amp;nbsp;통신&amp;nbsp;테스트&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;오후&amp;nbsp;(13:00&amp;nbsp;~&amp;nbsp;16:00):&amp;nbsp;Linux&amp;nbsp;환경에&amp;nbsp;MySQL&amp;nbsp;서버&amp;nbsp;구축&amp;nbsp;및&amp;nbsp;DB&amp;nbsp;테이블&amp;nbsp;설계&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;야간&amp;nbsp;(16:00&amp;nbsp;~&amp;nbsp;18:00):&amp;nbsp;Python&amp;nbsp;시리얼&amp;nbsp;통신&amp;nbsp;연동&amp;nbsp;및&amp;nbsp;실시간&amp;nbsp;데이터&amp;nbsp;DB&amp;nbsp;저장&amp;nbsp;구현&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;[2일&amp;nbsp;차:&amp;nbsp;GUI&amp;nbsp;개발&amp;nbsp;및&amp;nbsp;시스템&amp;nbsp;통합]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;오전&amp;nbsp;(09:00&amp;nbsp;~&amp;nbsp;12:00):&amp;nbsp;Python&amp;nbsp;GUI&amp;nbsp;화면&amp;nbsp;레이아웃&amp;nbsp;및&amp;nbsp;실시간&amp;nbsp;그래프&amp;nbsp;시각화&amp;nbsp;구현&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;오후&amp;nbsp;(13:00&amp;nbsp;~&amp;nbsp;16:00):&amp;nbsp;GUI&amp;nbsp;제어&amp;nbsp;명령의&amp;nbsp;리눅스&amp;nbsp;서버&amp;nbsp;경유&amp;nbsp;및&amp;nbsp;아두이노&amp;nbsp;액추에이터&amp;nbsp;원격&amp;nbsp;제어&amp;nbsp;구현&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;야간&amp;nbsp;(16:00&amp;nbsp;~&amp;nbsp;18:00):&amp;nbsp;전체&amp;nbsp;시나리오&amp;nbsp;테스트,&amp;nbsp;예외&amp;nbsp;처리&amp;nbsp;및&amp;nbsp;최종&amp;nbsp;프로젝트&amp;nbsp;디버깅&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.2&amp;nbsp;조직&amp;nbsp;구성&amp;nbsp;및&amp;nbsp;R&amp;amp;R&amp;nbsp;(역할&amp;nbsp;분담)&lt;/b&gt;&lt;br&gt;팀원은&amp;nbsp;PM을&amp;nbsp;포함하여&amp;nbsp;총&amp;nbsp;5명으로&amp;nbsp;구성되며,&amp;nbsp;각&amp;nbsp;파트별&amp;nbsp;역할과&amp;nbsp;1,&amp;nbsp;2일&amp;nbsp;차&amp;nbsp;상세&amp;nbsp;To-Do&amp;nbsp;업무는&amp;nbsp;다음과&amp;nbsp;같다.&lt;br&gt;&lt;br&gt;&lt;b&gt;팀원 구분&lt;/b&gt;&lt;b&gt;주요 역할&lt;/b&gt;&lt;b&gt;1&lt;/b&gt;&lt;b&gt;일 차 &lt;/b&gt;&lt;b&gt;To-Do (&lt;/b&gt;&lt;b&gt;인프라 및 단품 개발&lt;/b&gt;&lt;b&gt;)&lt;/b&gt;&lt;b&gt;2&lt;/b&gt;&lt;b&gt;일 차 &lt;/b&gt;&lt;b&gt;To-Do (&lt;/b&gt;&lt;b&gt;시스템 통합 및 검증&lt;/b&gt;&lt;b&gt;)&lt;/b&gt;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 368px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 22px;&quot;&gt;&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt;팀원 구분&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt;주요 역할&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt;1일차 (To-Do 인프라 및 단품개발)&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt;2일차 (To-Do 시스템 통합 및 검증)&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;height: 66px; text-align: center;&quot;&gt;&lt;b&gt;PM&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;일정 관리 및 데이터 흐름 조율&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• 연동 규격서(JSON) 배포 및 싱크 조정&lt;br&gt;• 1일 차 데이터 DB 적재 교차 검증&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• 데이터 엔드 투 엔드 흐름 모니터링&lt;br&gt;• 통합 테스트 및 최종 시연 주도&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 88px;&quot;&gt;&lt;td style=&quot;height: 88px; text-align: center;&quot;&gt;&lt;b&gt;아두이노 개발자&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 88px; text-align: justify;&quot;&gt;센서 수집 및 펌웨어 개발&lt;/td&gt;&lt;td style=&quot;height: 88px; text-align: justify;&quot;&gt;• PIR/사운드 센서 회로 구성 및 테스트&lt;br&gt;• 1초 주기 JSON 데이터 시리얼 출력&lt;/td&gt;&lt;td style=&quot;height: 88px; text-align: justify;&quot;&gt;• 수신 JSON 제어 명령 파싱 로직 구현&lt;br&gt;• 부저 및 서보 모터 구동 통합&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 44px;&quot;&gt;&lt;td style=&quot;height: 44px; text-align: center;&quot;&gt;&lt;b&gt;GUI &lt;/b&gt;&lt;b&gt;개발자&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 44px; text-align: justify;&quot;&gt;U/I 화면 및 시각화 개발&lt;/td&gt;&lt;td style=&quot;height: 44px; text-align: justify;&quot;&gt;• PyQt/Tkinter 기본 윈도우 구축&lt;br&gt;• 더미 데이터 기반 UI 레이아웃 구현&lt;/td&gt;&lt;td style=&quot;height: 44px; text-align: justify;&quot;&gt;• 실시간 꺾은선 그래프 시각화 구현&lt;br&gt;• 위험 팝업창 및 제어 버튼 이벤트 연동&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;height: 66px; text-align: center;&quot;&gt;&lt;b&gt;DB &lt;/b&gt;&lt;b&gt;엔지니어&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;데이터 모델링 및 쿼리 최적화&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• 리눅스 내 MySQL 실행 및 DDL 수행&lt;br&gt;• 외부 접근 권한 및 방화벽 설정&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• GUI용 실시간 로그 조회 쿼리 최적화&lt;br&gt;• 조치 상태(action_taken) 업데이트 쿼리 작성&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 66px;&quot;&gt;&lt;td style=&quot;height: 66px; text-align: center;&quot;&gt;&lt;b&gt;시스템 엔지니어&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;리눅스 인프라 및 소켓 서버 구축&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• 리눅스 개발 서버 환경 구성 (IP 고정)&lt;br&gt;• GUI-서버 간 소켓 통신 환경 테스트&lt;/td&gt;&lt;td style=&quot;height: 66px; text-align: justify;&quot;&gt;• 소켓-시리얼 제어 명령 중계 스크립트 완성&lt;br&gt;• 예외 로그 자동화 스크립트 작성&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;3. 시스템 아키텍처 및 하드웨어 구성&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;3.1&amp;nbsp;하드웨어&amp;nbsp;부품&amp;nbsp;구성&amp;nbsp;및&amp;nbsp;선정&amp;nbsp;이유&lt;/b&gt;&lt;br&gt;단기간&amp;nbsp;내&amp;nbsp;가시적인&amp;nbsp;연동&amp;nbsp;성과를&amp;nbsp;낼&amp;nbsp;수&amp;nbsp;있고&amp;nbsp;무인&amp;nbsp;매장&amp;nbsp;시나리오에&amp;nbsp;적합한&amp;nbsp;가성비&amp;nbsp;높은&amp;nbsp;부품을&amp;nbsp;선정한다.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;PIR&amp;nbsp;인체감지&amp;nbsp;센서&amp;nbsp;(HC-SR501):&amp;nbsp;매장&amp;nbsp;내&amp;nbsp;방문객&amp;nbsp;출입&amp;nbsp;및&amp;nbsp;움직임&amp;nbsp;감지용&amp;nbsp;(출입&amp;nbsp;인원&amp;nbsp;카운팅)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;사운드&amp;nbsp;감지&amp;nbsp;센서&amp;nbsp;(LM393&amp;nbsp;등):&amp;nbsp;매장&amp;nbsp;내&amp;nbsp;비명,&amp;nbsp;유리가&amp;nbsp;깨지는&amp;nbsp;등&amp;nbsp;소란&amp;nbsp;행위&amp;nbsp;및&amp;nbsp;위험&amp;nbsp;소음&amp;nbsp;감지용&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;피에조&amp;nbsp;부저&amp;nbsp;(Piezo&amp;nbsp;Buzzer):&amp;nbsp;위험&amp;nbsp;상황&amp;nbsp;발생&amp;nbsp;시&amp;nbsp;GUI&amp;nbsp;및&amp;nbsp;리눅스&amp;nbsp;원격&amp;nbsp;명령으로&amp;nbsp;현장&amp;nbsp;경보음&amp;nbsp;발생용&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;서보&amp;nbsp;모터&amp;nbsp;(SG90):&amp;nbsp;GUI에서&amp;nbsp;출입문&amp;nbsp;잠금/해제&amp;nbsp;버튼&amp;nbsp;클릭&amp;nbsp;시&amp;nbsp;원격&amp;nbsp;도어락&amp;nbsp;개폐&amp;nbsp;연출용&lt;br&gt;&lt;br&gt;&lt;b&gt;3.2&amp;nbsp;시스템&amp;nbsp;파일&amp;nbsp;디렉터리&amp;nbsp;구조&lt;/b&gt;&lt;br&gt;형상관리(Git)&amp;nbsp;시&amp;nbsp;팀원&amp;nbsp;간&amp;nbsp;소스코드&amp;nbsp;충돌을&amp;nbsp;방지하기&amp;nbsp;위해&amp;nbsp;역할을&amp;nbsp;기준으로&amp;nbsp;디렉터리를&amp;nbsp;격리&amp;nbsp;설계한다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 364px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 17px;&quot;&gt;&lt;td style=&quot;width: 100.0%; height: 17px; text-align: justify;&quot;&gt;unattended-store-security/&lt;br&gt;├──&amp;nbsp;arduino/&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;store_security_node/&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;store_security_node.ino&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;아두이노&amp;nbsp;센서/액추에이터&amp;nbsp;제어&amp;nbsp;펌웨어&lt;br&gt;├──&amp;nbsp;database/&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;ddl.sql&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;데이터베이스&amp;nbsp;테이블&amp;nbsp;생성&amp;nbsp;스크립트&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;sample_data.sql&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;테스트용&amp;nbsp;초기&amp;nbsp;더미&amp;nbsp;데이터&amp;nbsp;스크립트&lt;br&gt;├──&amp;nbsp;linux_server/&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;socket_server.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;GUI&amp;nbsp;명령&amp;nbsp;수신&amp;nbsp;및&amp;nbsp;아두이노&amp;nbsp;중계&amp;nbsp;소켓&amp;nbsp;서버&lt;br&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;logger.sh&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;시스템&amp;nbsp;예외&amp;nbsp;로그&amp;nbsp;관리&amp;nbsp;쉘&amp;nbsp;스크립트&lt;br&gt;└──&amp;nbsp;python_gui/&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;GUI&amp;nbsp;프로그램&amp;nbsp;실행&amp;nbsp;메인&amp;nbsp;엔트리&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;config.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;DB&amp;nbsp;접속&amp;nbsp;정보&amp;nbsp;및&amp;nbsp;소켓&amp;nbsp;포트&amp;nbsp;설정&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;ui/&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;main_window.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;UI&amp;nbsp;레이아웃&amp;nbsp;및&amp;nbsp;위젯&amp;nbsp;배치&amp;nbsp;소스&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;database/&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;db_handler.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;MySQL&amp;nbsp;Connection&amp;nbsp;및&amp;nbsp;CRUD&amp;nbsp;처리&amp;nbsp;클래스&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 데이터베이스 및 통신 규격 설계&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;4.1&amp;nbsp;데이터베이스&amp;nbsp;스키마&amp;nbsp;정의&amp;nbsp;(MySQL&amp;nbsp;DDL)&lt;/b&gt;&lt;br&gt;매장&amp;nbsp;구분(식품,&amp;nbsp;의류,&amp;nbsp;가구,&amp;nbsp;잡화)을&amp;nbsp;마스터&amp;nbsp;테이블로&amp;nbsp;분리하고,&amp;nbsp;일자별&amp;nbsp;출입&amp;nbsp;로그&amp;nbsp;및&amp;nbsp;보안&amp;nbsp;이벤트를&amp;nbsp;관계형으로&amp;nbsp;설계하여&amp;nbsp;데이터&amp;nbsp;정규화를&amp;nbsp;달성한다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sql
CREATE DATABASE IF NOT EXISTS unattended_store_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE unattended_store_db;

-- 1. 매장 구분 마스터 테이블
CREATE TABLE IF NOT EXISTS stores (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;store_id INT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;store_name VARCHAR(50) NOT NULL UNIQUE,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- '식품매장', '의류매장' 등
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;congestion_limit INT NOT NULL DEFAULT 20&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; -- 혼잡판단 임계 인원
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. 방문객 출입 로그 테이블 (매장 ID 외래키 연동)
CREATE TABLE IF NOT EXISTS visitor_logs (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_id INT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;store_id INT NOT NULL,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;visitor_count INT NOT NULL DEFAULT 1,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- 실시간 인원 변동값 (+1/-1)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;status VARCHAR(20) NOT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; -- 'NORMAL'(원활), 'CROWDED'(혼잡)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FOREIGN KEY (store_id) REFERENCES stores(store_id) ON DELETE CASCADE,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INDEX idx_detected_at (detected_at),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INDEX idx_store_id (store_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 3. 보안 및 위험 상황 이벤트 테이블
CREATE TABLE IF NOT EXISTS security_events (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;event_id INT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;store_id INT NULL,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;event_at DATETIME DEFAULT CURRENT_TIMESTAMP,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;event_type VARCHAR(30) NOT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; -- 'SOUND_ALERT', 'FORCED_ENTRY'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sound_level INT NULL,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- 감지된 소음 데시벨 값
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;action_taken VARCHAR(20) DEFAULT 'PENDING',&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- 'PENDING', 'BUZZER_ON', 'DOOR_LOCKED'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FOREIGN KEY (store_id) REFERENCES stores(store_id) ON DELETE SET NULL,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INDEX idx_event_at (event_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 초기 마스터 데이터 삽입
INSERT INTO stores (store_name, congestion_limit) VALUES 
('식품매장', 15), ('의류매장', 10), ('가구매장', 5), ('잡화매장', 20);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&lt;b&gt;4.2 매장 구분이 포함된 JSON 통신 규격 (Protocol)&lt;/b&gt;&lt;br&gt;①&amp;nbsp;아두이노&amp;nbsp;➔&amp;nbsp;파이썬&amp;nbsp;(센서&amp;nbsp;데이터&amp;nbsp;송신)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;식품매장(ID:&amp;nbsp;1)&amp;nbsp;방문객&amp;nbsp;감지:&amp;nbsp;{&quot;type&quot;:&amp;nbsp;&quot;VISITOR&quot;,&amp;nbsp;&quot;store_id&quot;:&amp;nbsp;1,&amp;nbsp;&quot;store_name&quot;:&amp;nbsp;&quot;GROCERY&quot;,&amp;nbsp;&quot;motion&quot;:&amp;nbsp;1,&amp;nbsp;&quot;sound&quot;:&amp;nbsp;40}&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;• 잡화매장(ID: 4) 위험소음감지: {&quot;type&quot;: &quot;SECURITY&quot;, &quot;store_id&quot;: 4, &quot;store_name&quot;: &quot;GENERAL&quot;, &quot;motion&quot;: 1, &quot;sound&quot;: 88}&lt;br&gt;②&amp;nbsp;파이썬&amp;nbsp;➔&amp;nbsp;아두이노&amp;nbsp;(원격&amp;nbsp;액추에이터&amp;nbsp;제어&amp;nbsp;수신)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;잡화매장(ID:&amp;nbsp;4)&amp;nbsp;경보&amp;nbsp;부저&amp;nbsp;작동&amp;nbsp;명령:&amp;nbsp;{&quot;command&quot;:&amp;nbsp;&quot;BUZZER&quot;,&amp;nbsp;&quot;store_id&quot;:&amp;nbsp;4,&amp;nbsp;&quot;value&quot;:&amp;nbsp;&quot;ON&quot;}&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;•&amp;nbsp;식품매장(ID:&amp;nbsp;1)&amp;nbsp;출입문&amp;nbsp;강제&amp;nbsp;잠금&amp;nbsp;명령:&amp;nbsp;{&quot;command&quot;:&amp;nbsp;&quot;DOOR&quot;,&amp;nbsp;&quot;store_id&quot;:&amp;nbsp;1,&amp;nbsp;&quot;value&quot;:&amp;nbsp;&quot;LOCK&quot;}&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;5. 사용자 인터페이스 (GUI Layout)&lt;/b&gt;&lt;br&gt;파이썬&amp;nbsp;GUI&amp;nbsp;화면은&amp;nbsp;관리자의&amp;nbsp;즉각적인&amp;nbsp;모니터링과&amp;nbsp;제어가&amp;nbsp;가능하도록&amp;nbsp;좌측(모니터링)과&amp;nbsp;우측(현황/원격&amp;nbsp;제어)&amp;nbsp;레이아웃으로&amp;nbsp;2분할&amp;nbsp;설계한다.&lt;br&gt;============================================================================&lt;br&gt;&amp;nbsp;[상태바]&amp;nbsp;&amp;nbsp; &amp;nbsp;시스템&amp;nbsp;상태:&amp;nbsp;위험&amp;nbsp;(보안&amp;nbsp;경고&amp;nbsp;발생!)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;현재&amp;nbsp;시간:&amp;nbsp;2026-06-02&amp;nbsp;16:19:00&lt;br&gt;============================================================================&lt;br&gt;&amp;nbsp;(좌측)&amp;nbsp; &amp;nbsp;실시간&amp;nbsp;매장&amp;nbsp;모니터링&amp;nbsp;영역&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;(우측)&amp;nbsp; &amp;nbsp;매장별&amp;nbsp;현황&amp;nbsp;및&amp;nbsp;원격&amp;nbsp;제어&amp;nbsp;영역&lt;br&gt;------------------------------------------------|---------------------------------------&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;[ &amp;nbsp;매장별&amp;nbsp;실시간&amp;nbsp;방문객&amp;nbsp;세부&amp;nbsp;내역]&lt;br&gt;&amp;nbsp;&amp;nbsp;[ &amp;nbsp;실시간&amp;nbsp;총&amp;nbsp;방문객&amp;nbsp;추이&amp;nbsp;그래프]&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;-------------------------------------&lt;br&gt;&amp;nbsp;&amp;nbsp;+------------------------------------------+&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;식품매장&amp;nbsp;:&amp;nbsp;[&amp;nbsp;12&amp;nbsp;명&amp;nbsp;]&amp;nbsp; &amp;nbsp;원활&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(차트)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;의류매장&amp;nbsp;:&amp;nbsp;[&amp;nbsp;&amp;nbsp;8&amp;nbsp;명&amp;nbsp;]&amp;nbsp; &amp;nbsp;원활&lt;br&gt;&amp;nbsp;&amp;nbsp;+------------------------------------------+&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; ️&amp;nbsp;가구매장&amp;nbsp;:&amp;nbsp;[&amp;nbsp;&amp;nbsp;3&amp;nbsp;명&amp;nbsp;]&amp;nbsp; &amp;nbsp;쾌적&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;잡화매장&amp;nbsp;:&amp;nbsp;[&amp;nbsp;25&amp;nbsp;명&amp;nbsp;]&amp;nbsp; &amp;nbsp;혼잡&lt;br&gt;&amp;nbsp;&amp;nbsp;[ &amp;nbsp;실시간&amp;nbsp;매장&amp;nbsp;소음도&amp;nbsp;그래프]&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;-------------------------------------&lt;br&gt;&amp;nbsp;&amp;nbsp;+------------------------------------------+&amp;nbsp;&amp;nbsp;|&amp;nbsp;[ &amp;nbsp;실시간&amp;nbsp;보안&amp;nbsp;경고&amp;nbsp;알림]&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(차트)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;|&amp;nbsp;-------------------------------------&lt;br&gt;&amp;nbsp;&amp;nbsp;+------------------------------------------+&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;⚠️&amp;nbsp;[16:12:45]&amp;nbsp;잡화매장&amp;nbsp;사운드&amp;nbsp;센서&amp;nbsp;임계치&amp;nbsp;초과!&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;-------------------------------------&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;[ &amp;nbsp;출입문&amp;nbsp;및&amp;nbsp;경보기&amp;nbsp;원격&amp;nbsp;제어]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;출입문&amp;nbsp;상태:&amp;nbsp;[ &amp;nbsp;열림]&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;[&amp;nbsp;강제&amp;nbsp;원격&amp;nbsp;잠금&amp;nbsp;]&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;nbsp;원격&amp;nbsp;잠금&amp;nbsp;해제&amp;nbsp;]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp; &amp;nbsp;경보기&amp;nbsp;상태:&amp;nbsp;[ &amp;nbsp;대기]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;[&amp;nbsp;사이렌&amp;nbsp;강제&amp;nbsp;구동&amp;nbsp;]&amp;nbsp;[&amp;nbsp;사이렌&amp;nbsp;끄기&amp;nbsp;]&lt;br&gt;------------------------------------------------|---------------------------------------&lt;br&gt;&amp;nbsp;(하단)&amp;nbsp; &amp;nbsp;최근&amp;nbsp;보안&amp;nbsp;이벤트&amp;nbsp;로그&amp;nbsp;(DB&amp;nbsp;연동&amp;nbsp;실시간&amp;nbsp;출력&amp;nbsp;테이블)&lt;br&gt;&amp;nbsp;+-----------------------+-----------------------+-----------------------+-------------+&lt;br&gt;&lt;br&gt;&amp;nbsp;|&amp;nbsp;이벤트&amp;nbsp;시간&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;유형&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;수치&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;조치&amp;nbsp;상태&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&lt;br&gt;&amp;nbsp;+-----------------------+-----------------------+-----------------------+-------------+&lt;br&gt;&lt;br&gt;&amp;nbsp;|&amp;nbsp;2026-06-02&amp;nbsp;16:12:45&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;SOUND_ALERT&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;85&amp;nbsp;dB&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;PENDING&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&lt;br&gt;&amp;nbsp;|&amp;nbsp;2026-06-02&amp;nbsp;15:30:12&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;VISITOR&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;잡화매장&amp;nbsp;감지&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&amp;nbsp;NORMAL&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|&lt;br&gt;&amp;nbsp;+-----------------------+-----------------------+-----------------------+-------------+&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. 최종 통합 테스트 및 검증 시나리오&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;개발&amp;nbsp;완료&amp;nbsp;시점(2일&amp;nbsp;차&amp;nbsp;16:00&amp;nbsp;이후)&amp;nbsp;시스템의&amp;nbsp;End-to-End&amp;nbsp;동작&amp;nbsp;신뢰성을&amp;nbsp;검증하기&amp;nbsp;위한&amp;nbsp;3단계&amp;nbsp;통합&amp;nbsp;시나리오&amp;nbsp;프로세스이다.&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;width: 100.0%; text-align: justify;&quot;&gt;&lt;b&gt;&amp;nbsp; • Step 1: 정상 방문객 출입 상황 검증&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 시연 행동: 시연자가 식품매장 구역(PIR 센서)에 움직임을 발생시킨다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 검증 기준: GUI 우측 식품매장 실시간 인원이 즉시 1명 증가하고, 좌측 총 방문객 추이 그래프가 실시간 우상향하는지 확인한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp;• Step 2: 매장 내 위급 상황 발생 검증&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 시연 행동: 잡화매장 사운드 센서에 큰 소음(85dB 이상)을 노출시킨다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 검증 기준: 상단 상태바가 위험(Red) 상태로 즉시 변경되고, 우측 알림창에 빨간색 경고 메시지가 팝업되며, 하단 DB 로그 테이블에&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;PENDING 상태의 이벤트 행이 실시간 한 줄 추가되는지 확인한다.&lt;br&gt;&lt;b&gt;&amp;nbsp; • Step 3: 관리자 원격 제어 및 상황 종료 검증&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 시연 행동: GUI 개발자가 화면 우측의 [강제 원격 잠금] 및 [사이렌 강제 구동] 버튼을 마우스로 클릭한다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;◦ 검증 기준: 리눅스 소켓 서버를 거쳐 명령이 전송되어 아두이노의 피에조 부저에서 경보음이 울리고, 서보 모터가 90도 회전하여 &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;도어락이 작동하는지 확인한다&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;최종적으로 하단 테이블의 조치 상태 컬럼이 BUZZER_ON / DOOR_LOCKED로 자동 업데이트되는지 검증한다.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;figure data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;010&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/010.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/010.gif&quot; width=&quot;150&quot;&gt;&lt;/figure&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;프로그램 개발 기획자가 작성하는 문서란 뭐야?&lt;/b&gt;&lt;/blockquote&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACAMQAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt; &lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;프로그램 개발 기획자는 프로젝트의 시작부터 배포까지 전 과정을 총괄하며, 목적과 방향성을 명확히 하는 핵심 문서를 작성해야&amp;nbsp; 한다.&lt;/span&gt;&amp;nbsp;핵심 산출물로는 프로젝트 개요서, 요구사항 정의서, IA(인포메이션 아키텍처) 및 플로우차트, 화면 설계서(와이어프레임/스토리보드), API 명세서가 있다. 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;1. 프로젝트 개요서 (Project Overview)&lt;/b&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAICRAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACAkQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
   프로젝트의 목적, 타겟 사용자, 핵심 기능, 개발 일정 및 예산을 한눈에 파악할 수 있도록 1장 분량으로 요약한 문서입니다. 이해관계자 및 팀원들에게 방향성을 제시하는 나침반 역할 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIDxAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;2. 요구사항 정의서 (Requirements Document)&lt;/b&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIERAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBEQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
   프로그램이 반드시 갖추어야 할 기능적·비기능적 요구사항을 나열한 문서로 클라이언트와 개발팀 간의 오해를 방지하고 개발 범위(Scope)를 확정 짓는 기준이 된다. 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIExAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;3. IA 및 플로우차트 (Information Architecture &amp;amp; Flow Chart)&lt;/b&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIFRAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBUQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
   서비스의 전체적인 구조와 메뉴 트리를 도식화한 문서, 사용자가 서비스에 진입하여 원하는 목적을 달성하기까지의 이동 경로(User Journey)와 화면 간의 연결 관계를 한눈에 볼 수 있다. 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIFhAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;4. 화면 설계서 (Wireframe &amp;amp; Storyboard)&lt;/b&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIGBAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACBgQAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
   UI/UX 디자인과 개발을 위해 각 화면의 레이아웃, 컴포넌트(버튼, 입력창 등), 데이터 연동 방식, 예외 처리 규칙을 상세히 기록한 문서입니다. 기획자의 의도가 가장 많이 반영되는 핵심 산출물 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIHhAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(0, 29, 53);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt; &lt;b&gt;5. API 명세서 (API Specification)&lt;/b&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-ved=&quot;2ahUKEwjA8o64s-iUAxXPqFYBHf_NLjoQi4wTegoIAggACAAIHxAA&quot; data-bfc=&quot;&quot;&gt; 
 &lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACB8QAQ&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;
   프론트엔드 개발자와 백엔드 개발자가 데이터를 원활하게 주고받을 수 있도록 규약을 정의한 문서, 엔드포인트(URL), 통신 방식(HTTP Method), 요청 및 응답 데이터(Request/Response Parameter) 형식 등을 명시 
 &lt;/div&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/55</guid>
      <comments>https://goodjop79.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 2 Jun 2026 20:04:19 +0900</pubDate>
    </item>
    <item>
      <title>D+36+[Python] 오버로딩,오버라이딩</title>
      <link>https://goodjop79.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;ㅇ 일&amp;nbsp; 자 :&amp;nbsp; 2026년 5월 31(일)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 내&amp;nbsp; 용 : class 오버로딩과 오버라이딩 개념 이해하기&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;1. 오버로딩(Overloading) : 확장, 과적하다, 많이 싣다&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; 오버로딩은 &lt;u&gt;한 클래스 내에서 같은 이름의 메서드를 여러 개 정의하는 것&lt;/u&gt;을 말한다. 컴퓨터가 메서드 이름이 같아도 매개변수의 타입이나 개수를 보고 &quot;아, 이 메서드를 부른 거구나!&quot; 하고 구별할 수 있게 조치를 취해두는 것임&lt;br&gt;가.&amp;nbsp; 핵심 조건: 메서드 이름은 같아야 하지만, 매개변수의 개수나 타입, 순서가 달라야 한다 (리턴 타입만 다른 것은 인정되지 않음)&lt;br&gt;나. 우리가 글을 쓸 때 쓰는 '펜(Pen)'을 떠올려 보면 write(검은펜), write(빨간펜), write(형광펜, 굵게)처럼 &lt;span style=&quot;color: #8A3DB6;&quot;&gt;행동의 이름은 같지만,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;쥐어주는 도구(매개변수)에 따라 다르게 작동&lt;/span&gt;하는 것과 같다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;class Calculator {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1번: 정수 두 개를 더함
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int add(int a, int b) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return a + b;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 2번: 실수 두 개를 더함 (매개변수 타입이 다름)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;double add(double a, double b) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return a + b;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 3번: 정수 세 개를 더함 (매개변수 개수가 다름)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int add(int a, int b, int c) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return a + b + c;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;2. 오버라이딩(Overriding) : 재정의, 위에 오르다, 덮어쓰다.&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp;오버라이딩은 &lt;span style=&quot;color: #8A3DB6;&quot;&gt;'부모 클래스로부터 물려받은 메서드를 자식 클래스에서 마음대로 바꾸어 다시 쓰는 것&lt;/span&gt;' 부모가 준 기능이 마음에 안 들거나, 자식 클래스에 맞게 업그레이드가 필요할 때 사용한다.&lt;br&gt;가. 핵심 조건 : 부모 메서드와 이름, 매개변수, 리턴 타입이 완전히 똑같아야 한다. &lt;u&gt;내용(중괄호 {} 안의 코드)만 변경&lt;/u&gt;&lt;br&gt;나. 부모님께 '가업(메서드)'을 물려받았는데, 자식 대에서 트렌드에 맞게 '인테리어와 메뉴(내부 코드)'를 싹 리모델링해서 운영하는 것, 간판 이름(메서드 이름)은 그대로 유지한다. &lt;br&gt;예)&lt;i&gt;&quot; 소고기를 속이면 3대가 망한다&quot; 3대째 간판명인데 주인이 대를 이어 환경을 바꿈.&lt;/i&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;class Animal {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void cry() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(&quot;동물이 웁니다.&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}

class Cat extends Animal {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override // 부모의 cry()를 덮어쓰겠다는 표시 (생략 가능하지만 붙이는 게 좋음)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void cry() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;System.out.println(&quot;야옹~ 야옹~&quot;); // 자식 입맛에 맞게 재정의
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/54</guid>
      <comments>https://goodjop79.tistory.com/54#entry54comment</comments>
      <pubDate>Tue, 2 Jun 2026 18:56:04 +0900</pubDate>
    </item>
    <item>
      <title>D+39 [Project] Alot 개인 프로젝트(2)</title>
      <link>https://goodjop79.tistory.com/53</link>
      <description>&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 2일(화)&lt;/b&gt;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 주 제 :&amp;nbsp;&lt;/b&gt;파이썬 프로그램, GUI, MySQL 데이터베이스, 리눅스 지식을 갖춘 신입 개발자들을 위한 토이 프로그램 만들기 (Arduino,&amp;nbsp;AI응용)&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;ㅇ &lt;b&gt;실습 후기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;인공지능 응용시스템 신입 개발자는 하드웨어와 소프트웨어를 아우르는 풀스텍 (Full-STack) loT 역량을 길러야 한다고 생각한다.&amp;nbsp;&lt;br&gt;그러기 위해서는 부단한 공부와 설계 역량을 기르고, 프로그램을 작성할 때 사용자 요구사항을 잘 파악하여 잘 구현하여 테스트까지 잘 되어 오류가 없어야 될 것이다. 그러나 오류가 없을 수는 없겠고, 그 오류들을 수정하고 고칠수 있는 역량을 길러야 한다는 것을 더욱 실감하고 있다.&amp;nbsp;&lt;br&gt;특히, '팅커캐드'로 회로를 설계하고, 아두이노 '보드'에 연결한 후 그 시그널들을 리눅스 환경에서 코드를 실행하고, 연동하여 화면에 구현되기까지의 과정을 처음엔 이해할 수 없어 계속 실행해 봐야 했다. 빠른시간내에&amp;nbsp;이해하는 것이 다소 어려웠지만 계속 공부해야 한다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;① 데이터 파이프라인 설계 능력&amp;nbsp;&lt;/b&gt;&lt;br&gt;&quot;단순 아두이노 제어에 그치지 않고,&amp;nbsp;하드웨어, 파이썬 미들웨어,&amp;nbsp;RDBMS로 이어지는 데이터 흐름을 소규모지만, 직접 설계하고 정형 데이터로 저장해 본 경험&quot;을 통해 향후 데이터가 모이면 스마트기기 설비를 위한 AI&amp;nbsp;모델의 학습 데이터가 될 수 있을 것이다.&lt;br&gt;&lt;b&gt;② 비동기 처리 및 예외 처리&amp;nbsp;&lt;/b&gt;&lt;br&gt;시리얼 통신과&amp;nbsp;GUI가 동시에 작동할 때 발생하는 병목 현상을 해결하기 위해 쓰레드(Thread)를 사용함.&amp;nbsp;연결이 끊겼을 때의 예외 처리(디바이스 재연결 로직 등)&lt;br&gt;&lt;b&gt;③ 리눅스 인프라 및 자동화 역량&lt;/b&gt;&lt;br&gt;단순히&amp;nbsp;PC에서 띄운 프로그램이 아니라,&amp;nbsp;리눅스 환경에서&amp;nbsp;DB를 운영하고&amp;nbsp;shell script와&amp;nbsp;crontab을 통해 백업을 자동화하는 등&lt;br&gt;&amp;nbsp;'운영 안정성'을 고려한 개발자가 되어야 할 것이다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이처럼 프로그램 개발은 창의적인 아이디어와 AI의 응용능력을 결합했을 때 어느정도의 결과물이 나올지 그리고 향후 비개발자들이 활용할 수 있는 다양한 프로그램들이 진화하게 될 것이다. 개발자&amp;nbsp;역량은 하루에 길러지는 것이 아니겠지만 &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&quot;도전은 계속 된다.&quot;&lt;/span&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;&amp;lt; 요구 사항 &amp;gt;&amp;gt;&lt;br&gt;# 개발 완료 후 리눅스 가상 서버 환경에서 터미널 접속만으로 시스템 전체를 한 번에 빌드하고 구동시킬 수 있게 하기&lt;br&gt;# 자동화 쉘 스크립트를 제작 및 추후 적재된 sensor_logs 이력을 바탕으로 이상 징후 탐지 AI 모델 및 시계열 트래픽 예측 하기&lt;br&gt;# 원천 정형 데이터를 엔지니어링하는 데이터 파이프라인 구축 하기&lt;br&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;※ 구현 화면 (loT 무인 매장 실시간 보안 및 방문객 관리 시스템)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LHufm/dJMcaccfKQ6/t3j0qHXVbMIdnhVXZ1K1XK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LHufm/dJMcaccfKQ6/t3j0qHXVbMIdnhVXZ1K1XK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LHufm/dJMcaccfKQ6/t3j0qHXVbMIdnhVXZ1K1XK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLHufm%2FdJMcaccfKQ6%2Ft3j0qHXVbMIdnhVXZ1K1XK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1857&quot; height=&quot;916&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XLuvo/dJMcagsbDBx/RVIT3J8qvlAuG4L96KkOb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XLuvo/dJMcagsbDBx/RVIT3J8qvlAuG4L96KkOb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XLuvo/dJMcagsbDBx/RVIT3J8qvlAuG4L96KkOb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXLuvo%2FdJMcagsbDBx%2FRVIT3J8qvlAuG4L96KkOb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1857&quot; height=&quot;916&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9TqY5/dJMcagsbDBy/7KKKrN73o6ebbxsuwnvK41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9TqY5/dJMcagsbDBy/7KKKrN73o6ebbxsuwnvK41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9TqY5/dJMcagsbDBy/7KKKrN73o6ebbxsuwnvK41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9TqY5%2FdJMcagsbDBy%2F7KKKrN73o6ebbxsuwnvK41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1857&quot; height=&quot;916&quot; data-origin-width=&quot;1857&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;프로젝트 명&lt;/b&gt;&lt;b&gt;: &lt;/b&gt;&lt;b&gt;loT 무인 매장 실시간 보안 및 방문객 관리 시스템&lt;/b&gt;&lt;br&gt;아두이노로 환경(동작 센서 감지&amp;nbsp;등) 데이터를 수집하고, 파이썬 GUI 화면에 실시간 시각화하며, 위험 상황 발생 시 리눅스 서버 및 아두이노 액추에이터를 통해 현장을 원격 제어하는 시스템&lt;br&gt;&amp;nbsp;&lt;br&gt;1.1. &lt;b&gt;전체 시스템 구조&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;[ 아두이노 (하드웨어) ] 
       │ (센서 데이터 송신 / 제어 명령 수신)
       ▼ Serial (UART) 통신
[ 파이썬 GUI PC (미들웨어 &amp;amp; 메인 앱) ] ───► [ MySQL (데이터베이스) ]
       │                                     (로그 및 센서 데이터 저장)
       ▼ SSH / 네트워크 통신
[ 리눅스 서버 (백업 및 모니터링) ]&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;-- 아두이노 활용 시스템 구조 이해하기&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GOB0i/dJMb99T70gg/gw4s0C0oS9AFbbwZx7d8pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GOB0i/dJMb99T70gg/gw4s0C0oS9AFbbwZx7d8pK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GOB0i/dJMb99T70gg/gw4s0C0oS9AFbbwZx7d8pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGOB0i%2FdJMb99T70gg%2Fgw4s0C0oS9AFbbwZx7d8pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;711&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.233%; height: 168px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt; 
 &lt;tbody&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 100%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;하드웨어&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;(&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;아두이노&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;)&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;온습도 센서&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;(DHT11),&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;조도 센서&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;, LED,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;부저&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;서보모터 등 기단부 구성&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;메인 애플리케이션&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;(&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;파이썬&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;GUI)&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;PyQt&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Tkinter&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;기반 프로그램&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;데이터 수집&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;, DB&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;저장&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;시각화&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;제어 명령 하향 전달 담당&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;데이터베이스&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;(MySQL)&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;센서 측정값 테이블&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;시스템 제어 이력 로그 테이블 관리&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;운영체제&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;(Linux)&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;span&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;파이썬 프로그램이 구동되는 환경 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;DB&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;서버 운영 환경&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;크론탭&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;(Crontab)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;을 활용한 주기적&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;DB&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;백업 스크립트 실행&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
 &lt;/tbody&gt; 
&lt;/table&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #DDDDDD;&quot;&gt;Project : loT 무인 매장 실시간 보안 및 방문객 관리 시스템 만들기 (아두이노, AI응용)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;1. 패키지 파일 (loTMemberControl)&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Ed90/dJMcahq8rdm/xmxwGlNQy65VomiYiBMIVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Ed90/dJMcahq8rdm/xmxwGlNQy65VomiYiBMIVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Ed90/dJMcahq8rdm/xmxwGlNQy65VomiYiBMIVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Ed90%2FdJMcahq8rdm%2FxmxwGlNQy65VomiYiBMIVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1261&quot; height=&quot;498&quot; data-origin-width=&quot;1261&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.1. 소스코드 (패키지 파일)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;b&gt;① __init__.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;'''loT 무인 매장 실시간 보안 및 방문객 관리 시스템 만들기 (AI응용)'''&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;② main.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 메인 파일
import sys

from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; QHBoxLayout, QLabel, QPushButton, QTableWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; QTableWidgetItem, QMessageBox, QSplitter)

from main_dashboard import *

if __name__ == &quot;__main__&quot;:
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from datetime import datetime
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app = QApplication(sys.argv)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dashboard = SmartStoreDashboard()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dashboard.show()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sys.exit(app.exec_())&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;③ main_dashboard.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;## PyQt5 메인 데쉬보드

import random
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; QHBoxLayout, QLabel, QPushButton, QTableWidget,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; QTableWidgetItem, QMessageBox, QSplitter)
from PyQt5.QtCore import QThread, pyqtSignal, QTimer, Qt
from PyQt5.QtGui import QFont, QColor


# 앞서 구현한 모듈 클래스 가져오기 (파일 이름이 같아야 합니다)
# from database_manager import DatabaseManager
# from serial_manager import ArduinoSerialManager

# -----------------------------------------------------------------------------
# 1. 시리얼 통신을 백그라운드에서 처리할 QThread 클래스 (화면 멈춤 방지)
# -----------------------------------------------------------------------------
class SerialWorker(QThread):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# GUI 메인 스레드로 '동작 감지 이벤트'를 보내기 위한 커스텀 신호 정의
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;motion_detected_signal = pyqtSignal()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, port=&quot;/dev/ttyACM0&quot;):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.port = port
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = True

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def run(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;백그라운드에서 실행될 루프&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;[GUI 인프라] 시리얼 백그라운드 스레드 시작&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 실제 아두이노 장비 연결 시 아래 주석을 해제하고 연동하세요.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# manager = ArduinoSerialManager(port=self.port)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# manager.start_reading(db_callback=self.motion_detected_signal.emit)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# [2일간의 단기 개발용 팁] 하드웨어 미연결 시 테스트를 위한 가상 센서 생성기 (Mock)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.is_running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.msleep(random.randint(4000, 8000))&amp;nbsp;&amp;nbsp;# 4~8초 간격으로 랜덤 감지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.is_running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 UI 화면에 신호 발송
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.motion_detected_signal.emit()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def stop(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.wait()


# -----------------------------------------------------------------------------
# 2. 메인 대시보드 GUI 윈도우 클래스
# -----------------------------------------------------------------------------
class SmartStoreDashboard(QMainWindow):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 가상의 DB 연결 매니저 객체 생성 (실제 환경 시 아래 주석 해제)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# self.db = DatabaseManager(host=&quot;localhost&quot;, user=&quot;store_user&quot;, password=&quot;password&quot;, database=&quot;store_db&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 개발 모드 디버깅용 상태 변수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.current_mode = &quot;BUSINESS&quot;&amp;nbsp;&amp;nbsp;# 기본: 영업 모드
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.today_visitor_count = 0

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.initUI()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.start_serial_thread()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def initUI(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;UI 컴포넌트 초기화 및 배치&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;IoT 무인 매장 실시간 보안 및 방문객 관리 시스템&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setGeometry(100, 100, 1000, 600)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 위젯 및 레이아웃 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_widget = QWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setCentralWidget(main_widget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout = QHBoxLayout(main_widget)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 스플리터를 활용해 좌측(제어/상태), 우측(로그 데이터) 화면 분할
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;splitter = QSplitter(Qt.Horizontal)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ---------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# [좌측 영역] 현황판 및 제어 패널
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ---------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_widget = QWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout = QVBoxLayout(left_widget)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 타이틀 라벨
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title_label = QLabel(&quot;  매장 실시간 모니터링 시스템&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title_label.setFont(QFont(&quot;Arial&quot;, 16, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title_label.setAlignment(Qt.AlignCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout.addWidget(title_label)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 현재 운영 모드 표시 패널
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label = QLabel(&quot;운영 상태: 영업 모드 (BUSINESS)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setFont(QFont(&quot;Arial&quot;, 14, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setAlignment(Qt.AlignCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #2ECC71; color: white; border-radius: 10px; padding: 15px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout.addWidget(self.mode_status_label)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 실시간 센서 알림 창 (핵심 요구사항: 감지 시 배경 변경)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box = QLabel(&quot;정상 운영 중\n(움직임 없음)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setFont(QFont(&quot;Arial&quot;, 14, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setAlignment(Qt.AlignCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #F2F4F4; color: #34495E; border: 2px solid #BDC3C7; border-radius: 10px; padding: 30px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout.addWidget(self.alert_box)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 오늘 누적 방문자수 현황
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.counter_label = QLabel(&quot;오늘 총 방문객 수: 0 명&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.counter_label.setFont(QFont(&quot;Arial&quot;, 12))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.counter_label.setAlignment(Qt.AlignCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout.addWidget(self.counter_label)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 모드 전환 버튼 제어 영역
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;btn_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_business = QPushButton(&quot;영업 모드 전환&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_business.setFont(QFont(&quot;Arial&quot;, 11, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_business.setStyleSheet(&quot;background-color: #3498DB; color: white; padding: 10px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_business.clicked.connect(lambda: self.change_store_mode(&quot;BUSINESS&quot;))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_security = QPushButton(&quot;보안 모드 전환&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_security.setFont(QFont(&quot;Arial&quot;, 11, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_security.setStyleSheet(&quot;background-color: #E74C3C; color: white; padding: 10px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_security.clicked.connect(lambda: self.change_store_mode(&quot;SECURITY&quot;))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;btn_layout.addWidget(self.btn_business)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;btn_layout.addWidget(self.btn_security)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;left_layout.addLayout(btn_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ---------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# [우측 영역] 실시간 이벤트 로그 테이블 현황
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# ---------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;right_widget = QWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;right_layout = QVBoxLayout(right_widget)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;table_title = QLabel(&quot;  최근 센서 감지 이력 (실시간 적재 로그)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;table_title.setFont(QFont(&quot;Arial&quot;, 12, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;right_layout.addWidget(table_title)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table = QTableWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.setColumnCount(3)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.setHorizontalHeaderLabels([&quot;감지 시간&quot;, &quot;장치 ID&quot;, &quot;상태 모드&quot;])
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.horizontalHeader().setStretchLastSection(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;right_layout.addWidget(self.log_table)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 스플리터에 양쪽 레이아웃 추가 후 메인 레이아웃에 탑재
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;splitter.addWidget(left_widget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;splitter.addWidget(right_widget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_layout.addWidget(splitter)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 실시간 경고 연출 원상복구를 위한 타이머 설정 (3초 뒤 정상화)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_timer = QTimer()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_timer.setSingleShot(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_timer.timeout.connect(self.reset_alert_box)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# -----------------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 비즈니스 로직 및 이벤트 핸들러 함수들
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# -----------------------------------------------------------------------------
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def start_serial_thread(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;시리얼 수신 스레드 가동 및 시그널-슬롯 연결&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker = SerialWorker(port=&quot;/dev/ttyACM0&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker.motion_detected_signal.connect(self.handle_motion_detection)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def change_store_mode(self, mode):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[제어] 버튼 클릭 시 매장의 운영 모드를 전환&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.current_mode = mode
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# self.db.update_store_mode(mode) # 2일 차 DB 연동 시 반영

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if mode == &quot;BUSINESS&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setText(&quot;운영 상태: 영업 모드 (BUSINESS)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #2ECC71; color: white; border-radius: 10px; padding: 15px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.reset_alert_box()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setText(&quot;운영 상태: 경비 보안 모드 (SECURITY)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.mode_status_label.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #D35400; color: white; border-radius: 10px; padding: 15px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setText(&quot;  보안 모드 가동 중\n침입 감지 대기&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setStyleSheet(&quot;background-color: #34495E; color: white; border-radius: 10px; padding: 30px;&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def handle_motion_detection(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[핵심 연동] 아두이노 스레드로부터 감지 신호를 받았을 때 실행되는 함수&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_time = datetime.now().strftime(&quot;%H:%M:%S&quot;) if 'datetime' in globals() else &quot;14:23:45&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. DB 적재 프로세스 호출 (팀원 B 코드 연동 포인트)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# self.db.insert_sensor_log(device_id=&quot;ARDUINO_01&quot;, current_mode=self.current_mode)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 실시간 모드별 화면 시각화 예외 분기 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.current_mode == &quot;BUSINESS&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 영업 모드: 방문객 카운트 업 및 초록빛 알림
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.today_visitor_count += 1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.counter_label.setText(f&quot;오늘 총 방문객 수: {self.today_visitor_count} 명&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setText(&quot;  방문객 입장!\n환영합니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #E8F8F5; color: #117864; border: 2px solid #2ECC71; border-radius: 10px; padding: 30px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_timer.start(3000)&amp;nbsp;&amp;nbsp;# 3초 후 평시 상태로 복구

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elif self.current_mode == &quot;SECURITY&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 보안 모드: 무단 침입 경고 빨간색 연출 및 경고창(Modal Pop-up) 발생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setText(&quot;  침입 발생!!  \n경보 시스템 가동&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #FADBD8; color: #78281F; border: 3px solid #E74C3C; border-radius: 10px; padding: 30px;&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 아두이노 비프음 발생 신호 송신을 원할 시 여기서 팀원 A 클래스 메서드 호출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# self.worker.manager.send_trigger_to_buzzer()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 크리티컬 예외 상황 팝업 메시지 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.critical(self, &quot;  무단 침입 경보&quot;, &quot;보안 모드 중 움직임이 감지되었습니다!\n즉시 관리자 확인이 필요합니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 우측 실시간 테이블 로그 행 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;row_position = self.log_table.rowCount()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.insertRow(row_position)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.setItem(row_position, 0, QTableWidgetItem(f&quot;2026-06-03 {current_time}&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.setItem(row_position, 1, QTableWidgetItem(&quot;ARDUINO_01&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.setItem(row_position, 2, QTableWidgetItem(self.current_mode))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.log_table.scrollToBottom()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def reset_alert_box(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;평시 모니터링 화면으로 색상 원상 복구&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.current_mode == &quot;BUSINESS&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setText(&quot;정상 운영 중\n(움직임 없음)&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.alert_box.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #F2F4F4; color: #34495E; border: 2px solid #BDC3C7; border-radius: 10px; padding: 30px;&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def closeEvent(self, event):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;프로그램 종료 시 스레드를 안전하게 반환하여 프로세스 꼬임 방지&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker.stop()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;event.accept()


# --- 메인 애플리케이션 실행 프로세스 ---
if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 데이터 가공용 시간 라이브러리 임포트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;from datetime import datetime&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;④ main_view.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# PyQt5 메인 화면 및 이벤트 슬롯 소스코드 (main_view.py)
# 특징: 팀원 A의 시리얼 매니저를 서브 스레드로 구동하고, pyqtSignal을 통해 센서감지 이벤트 실시간 렌더링 함수로 중계

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSplitter
from PyQt5.QtCore import QThread, pyqtSignal, QTimer, Qt
from PyQt5.QtGui import QFont


# [팀원 C 단독 구현] 시리얼 이벤트를 UI 스레드와 격리하기 위한 비동기 워커
class SerialListenerWorker(QThread):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 UI 스레드와 통신할 비동기 신호 정의
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sensor_trigger = pyqtSignal()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, serial_mgr):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sm = serial_mgr

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def run(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;백그라운드에서 시리얼 대기열을 감시하며 UI 메인 스레드 방해 금지&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 팀원 A가 만든 시리얼 매니저의 무한 루프 수신기를 콜백 형식으로 구동
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.sm:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sm.start_reading(db_callback=self.sensor_trigger.emit)


class MainWindow(QMainWindow):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, serial_mgr, db_mgr):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sm = serial_mgr
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.db = db_mgr
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.init_ui()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.init_sub_thread()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def init_ui(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;무인 매장 보안 모니터링 시스템 v1.0&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setGeometry(100, 100, 1100, 650)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 레이아웃 및 스플리터 배치
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.main_widget = QWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setCentralWidget(self.main_widget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.layout = QHBoxLayout(self.main_widget)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.splitter = QSplitter(Qt.Horizontal)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 좌측 레이아웃 관할 (팀원 C 영역)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.left_panel = QWidget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.left_layout = QVBoxLayout(self.left_panel)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.title = QLabel(&quot;  실시간 매장 관리 서버&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.title.setFont(QFont(&quot;Malgun Gothic&quot;, 16, QFont.Bold))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.left_layout.addWidget(self.title)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display = QLabel(&quot;정상 모니터링 중&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #2ECC71; color: white; padding: 20px; font-size: 14pt; border-radius: 8px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setAlignment(Qt.AlignCenter)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.left_layout.addWidget(self.status_display)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.splitter.addWidget(self.left_panel)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.layout.addWidget(self.splitter)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 상태 복구용 일회성 타이머
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.recovery_timer = QTimer()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.recovery_timer.setSingleShot(True)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.recovery_timer.timeout.connect(self.restore_ui_status)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def init_sub_thread(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;팀원 A의 H/W 제어기를 서브 스레드로 이식하여 UI 랙 현상 원천 차단&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker = SerialListenerWorker(self.sm)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker.sensor_trigger.connect(self.on_motion_detected)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.worker.start()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def on_motion_detected(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[실시간 UI 갱신 슬롯] 아두이노 신호 수신 시 실시간 호출&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_mode = self.db.get_current_mode() if self.db else &quot;BUSINESS&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 실시간 데이터베이스 적재 (팀원 B 모듈 호출)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.db:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.db.insert_sensor_log()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if current_mode == &quot;BUSINESS&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setText(&quot;  방문객 입장 감지&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #3498DB; color: white; padding: 20px; font-size: 14pt; border-radius: 8px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.recovery_timer.start(2500)&amp;nbsp;&amp;nbsp;# 2.5초 후 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setText(&quot;  무단 침입 발생! 경보 가동&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #E74C3C; color: white; padding: 20px; font-size: 14pt; border-radius: 8px;&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 물리 경보 작동 명령 송신 (팀원 A 모듈 호출)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.sm: self.sm.send_trigger_to_hardware('W')

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def restore_ui_status(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setText(&quot;정상 모니터링 중&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.status_display.setStyleSheet(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;background-color: #2ECC71; color: white; padding: 20px; font-size: 14pt; border-radius: 8px;&quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑤ app_runner.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 팀원 E : 리눅스 런타임 배포 인프라 및 AI 전처리 파이프라인 설계
# 구현 핵심 아키텍처팀원 E는 프로젝트 총괄(PM)로서 전체 소스코드 연동 통합 파일(app_runner.py)을 작성하고,
# 개발 완료 후 리눅스 가상 서버 환경에서 터미널 접속만으로 시스템 전체를 한 번에 빌드하고 구동시킬 수 있는
# 자동화 쉘 스크립트를 제작. 추후 적재된 sensor_logs 이력을 바탕으로 이상 징후 탐지 AI 모델 및 시계열 트래픽 예측 AI를
# 올릴 수 있도록 원천 정형 데이터를 엔지니어링하는 데이터 파이프라인 뼈대를 구축

import sys
from PyQt5.QtWidgets import QApplication


# 팀원 A, B, C, D가 작성한 개별 모듈 임포트
# from serial_manager import ArduinoSerialManager
# from database_manager import DatabaseManager
# from main_view import MainWindow
# from sub_control_view import AnalyticsControlPanel

class AIDataPipeline:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[AI 확장성 전담 클래스] 적재된 시계열 로그 가공 전처리 파이프라인&quot;&quot;&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, db_mgr):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.db = db_mgr

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def transform_logs_for_ai_training(self, raw_logs):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[전처리 함수] AI 모델(LSTM/IsolationForest) 학습용 시계열 피처 엔지니어링 벡터 변환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Raw Data Format: [{'created_at': datetime, 'current_mode': 'BUSINESS'}]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ai_feature_matrix = []
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for log in raw_logs:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 시간 데이터를 인공지능이 인식하기 좋은 수치형 다차원 독립 변수로 변환 (Feature Extraction)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_time = log['created_at']
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;feature_vector = [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_time.weekday(),&amp;nbsp;&amp;nbsp;# 요일 데이터 (0:월 ~ 6:일)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_time.hour,&amp;nbsp;&amp;nbsp;# 시간대 (0~23)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_time.minute,&amp;nbsp;&amp;nbsp;# 분 단위 트래픽
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;1 if log['current_mode'] == 'BUSINESS' else 0&amp;nbsp;&amp;nbsp;# 범주형 변수 원핫인코딩 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ai_feature_matrix.append(feature_vector)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[AI Pipeline] 원천 로그 {len(raw_logs)}건 데이터셋(Dataset) 행렬 변환 완료.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return ai_feature_matrix&amp;nbsp;&amp;nbsp;# 딥러닝 프레임워크 텐서 주입용 최종 넘파이/리스트 반환


if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app = QApplication(sys.argv)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 인프라 인스턴스 레이어 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# db = DatabaseManager(host=&quot;localhost&quot;, user=&quot;root&quot;, password=&quot;password&quot;, database=&quot;store_db&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# sm = ArduinoSerialManager(port=&quot;/dev/ttyACM0&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 디버깅용 Mock 객체 바인딩 (통합 테스트 가동용)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;db, sm = None, None

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 프론트엔드 렌더링 프레임 결합 (C팀원 뷰 + D팀원 뷰 결합)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_ui = MainWindow(serial_mgr=sm, db_mgr=db)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sub_ui = AnalyticsControlPanel(main_window=main_ui, db_mgr=db, serial_mgr=sm)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 UI 스플리터 우측 도킹 레이아웃 완성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_ui.splitter.addWidget(sub_ui)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;main_ui.show()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. AI 파이프라인 가동 가능성 검증 디버그 로그
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pipeline = AIDataPipeline(db_mgr=db)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# fake_raw_logs = [{'created_at': datetime.now(), 'current_mode': 'BUSINESS'}]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# pipeline.transform_logs_for_ai_training(fake_raw_logs)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sys.exit(app.exec_())&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑥ database_manager.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# PyMySQL 백엔드 쿼리 연동 소스코드 (database_manager.py)
# 특징: Connection Pool을 모방한 동적 커넥션 관리 방식을 채택하여 무인 매장의 비활성 시간대 대기 시 발생하는 연결 끊김(Timeout) 현상을
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 원천 차단함, GUI 팀원이 딕셔너리 형태로 데이터를 편리하게 다룰 수 있도록 DictCursor를 설정함
import pymysql
from pymysql import MySQLError

class DatabaseManager:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, host=&quot;127.0.0.1&quot;, user=&quot;root&quot;, password=&quot;your_password&quot;, database=&quot;store_db&quot;):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.config = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;host&quot;: host,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;user&quot;: user,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;password&quot;: password,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;database&quot;: database,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;charset&quot;: &quot;utf8mb4&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;cursorclass&quot;: pymysql.cursors.DictCursor&amp;nbsp;&amp;nbsp;# 데이터를 딕셔너리 형태로 반환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def _get_connection(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;매 요청마다 안전한 연결 인스턴스를 반환하여 무중단 상태 유지&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return pymysql.connect(**self.config)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 오류] MySQL 서버 연결 실패 (네트워크/인증 에러): {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def get_current_mode(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;팀원 A, C 연동용: 현재 매장의 운영 모드(영업/보안) 상태 조회&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return &quot;BUSINESS&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT operation_mode FROM store_settings LIMIT 1;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result = cursor.fetchone()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return result['operation_mode'] if result else &quot;BUSINESS&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 오류] 모드 조회 쿼리 실패: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;BUSINESS&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def update_store_mode(self, target_mode):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;팀원 D 연동용: GUI 제어 셸에서 모드 변경 시 DB 마스터 테이블 갱신&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if target_mode not in [&quot;BUSINESS&quot;, &quot;SECURITY&quot;]: return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;UPDATE store_settings SET operation_mode = %s WHERE setting_id = 1;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (target_mode,))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 변경] 매장 모드가 [{target_mode}]로 업데이트되었습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 오류] 모드 변경 트랜잭션 실패: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def insert_sensor_log(self, device_id=&quot;ARDUINO_01&quot;, sensor_type=&quot;PIR&quot;, detected_value=1):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;팀원 A 시리얼 이벤트 연동용: 동작 감지 발생 시 이력 안전 적재&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_mode = self.get_current_mode() # 적재 시점의 상태값 스냅샷 확보
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INSERT INTO sensor_logs (device_id, sensor_type, detected_value, current_mode)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VALUES (%s, %s, %s, %s);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (device_id, sensor_type, detected_value, current_mode))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback() # 예외 발생 시 트랜잭션 롤백으로 데이터 원자성 보장
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 오류] 로그 저장 트랜잭션 롤백 실행: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def get_hourly_traffic_stats(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;팀원 D 통계 그래프 연동용: 최근 24시간 동안의 시간대별 유입량 집계 통계 조회&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return []
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SELECT 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HOUR(created_at) AS event_hour,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;COUNT(*) AS visitor_count
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FROM sensor_logs
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;WHERE current_mode = 'BUSINESS'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AND created_at &amp;gt;= DATE_SUB(NOW(), INTERVAL 1 DAY)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GROUP BY HOUR(created_at)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ORDER BY event_hour ASC;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return cursor.fetchall()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 오류] 시간별 대시보드 통계 조회 실패: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return []
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.close()&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑦ DatabaseManager.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# MySQL 데이터베이스 연동 및 관리를 위한 클래스 (DatabaseManager)

import pymysql
from pymysql import MySQLError
from datetime import datetime


class DatabaseManager:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, host=&quot;localhost&quot;, user=&quot;store_user&quot;, password=&quot;your_password&quot;, database=&quot;store_db&quot;):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[초기화] 데이터베이스 접속 정보를 설정합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;charset을 utf8mb4로 설정하여 이모지 및 다양한 문자열을 지원합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursorclass를 DictCursor로 설정하여 조회 결과를 딕셔너리 형태로 받아옵니다. (GUI 연동 시 편리)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.config = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;host&quot;: host,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;user&quot;: user,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;password&quot;: password,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;database&quot;: database,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;charset&quot;: &quot;utf8mb4&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;cursorclass&quot;: pymysql.cursors.DictCursor&amp;nbsp;&amp;nbsp;# 결과를 튜플이 아닌 딕셔너리 형식으로 반환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def _get_connection(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[내부 메서드] 호출 시마다 새로운 DB 커넥션 객체를 생성하여 반환합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;매번 새로 연결함으로써 Timeout(연결 끊김) 문제를 방지합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return pymysql.connect(**self.config)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 연결 실패] MySQL 서버 접속 정보를 확인하세요: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return None

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def get_current_mode(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[조회] store_settings 테이블에서 현재 매장의 운영 모드(영업/보안 등)를 조회합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return &quot;BUSINESS&quot;&amp;nbsp;&amp;nbsp;# DB 연결 실패 시 기본값으로 'BUSINESS' 모드 반환

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 최신 설정 데이터 1건을 조회합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;SELECT operation_mode FROM store_settings LIMIT 1;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result = cursor.fetchone()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 결과가 있으면 운영 모드를 반환하고, 없으면 기본값을 반환합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return result['operation_mode'] if result else &quot;BUSINESS&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[SQL 오류] 운영 모드 조회 실패: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;BUSINESS&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn: conn.close()&amp;nbsp;&amp;nbsp;# 사용한 커넥션은 반드시 닫아줍니다.

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def insert_sensor_log(self, device_id=&quot;ARDUINO_01&quot;, sensor_type=&quot;PIR&quot;, detected_value=1):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[등록] 아두이노 등 기기에서 감지된 센서 로그를 DB에 실시간으로 저장합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 로그를 저장하기 전, 현재 매장이 어떤 운영 모드인지 먼저 파악합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_mode = self.get_current_mode()&amp;nbsp;&amp;nbsp;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 센서 ID, 센서 종류, 감지된 값, 그리고 감지 당시의 운영 모드를 함께 저장합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INSERT INTO sensor_logs (device_id, sensor_type, detected_value, current_mode)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;VALUES (%s, %s, %s, %s);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql, (device_id, sensor_type, detected_value, current_mode))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.commit()&amp;nbsp;&amp;nbsp;# 데이터 변경 사항을 실제 DB에 영구 반영
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[DB 저장 성공] 센서 로그 등록 완료 (현재 모드: {current_mode})&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn.rollback()&amp;nbsp;&amp;nbsp;# 오류 발생 시 이전 상태로 복구(롤백)하여 데이터 무결성 유지
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[SQL 오류] 센서 로그 등록 실패 (Rollback 실행): {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn: conn.close()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def get_hourly_traffic_stats(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[통계] GUI 프로그램의 차트(Matplotlib 등) 시각화를 위한 최근 24시간 시간대별 방문자 수 통계를 조회합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn = self._get_connection()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not conn: return []

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;with conn.cursor() as cursor:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 영업(BUSINESS) 모드일 때 발생한 최근 1일(24시간) 동안의 로그를 시간대별로 그룹화하여 카운트합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sql = &quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SELECT 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HOUR(created_at) AS event_hour,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;COUNT(*) AS visitor_count
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FROM sensor_logs
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;WHERE current_mode = 'BUSINESS'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AND created_at &amp;gt;= DATE_SUB(NOW(), INTERVAL 1 DAY)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GROUP BY HOUR(created_at)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ORDER BY event_hour ASC;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cursor.execute(sql)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# [{'event_hour': 14, 'visitor_count': 5}, ...] 구조의 리스트 목록을 반환합니다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return cursor.fetchall()&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except MySQLError as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[SQL 오류] 시간대별 통계 조회 실패: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return []
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;finally:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if conn: conn.close()


# --- 코드 동작 테스트를 위한 실행 가이드 ---
if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. DB 매니저 객체 생성 (실제 사용 시 본인의 MySQL 계정 정보 입력 필요)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;db = DatabaseManager(host=&quot;127.0.0.1&quot;, user=&quot;root&quot;, password=&quot;password&quot;, database=&quot;store_db&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. 현재 운영 모드 확인 테스트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[테스트 1] 현재 매장 운영 모드: {db.get_current_mode()}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 센서 감지 이벤트 발생 시 로그 저장 테스트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;\n[테스트 2] 아두이노 PIR 센서 감지 로그 저장 시도...&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;db.insert_sensor_log(device_id=&quot;ARDUINO_01&quot;, sensor_type=&quot;PIR&quot;, detected_value=1)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 4. 차트 UI 표기를 위한 통계 데이터 추출 테스트
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;\n[테스트 3] GUI 차트용 24시간 통계 데이터 조회...&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats = db.get_hourly_traffic_stats()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for row in stats:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;시간대: {row['event_hour']}시 -&amp;gt; 방문 횟수: {row['visitor_count']}회&quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑧ serial_manager_A.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# PC측 파이썬 시리얼 중계 소스코드
# 특징: 리눅스 환경에서 아두이노 포트를 자동 탐색하며, 물리적 탈착 에러가 발생해도 프로세스가 죽지 않고
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 무한 재연결을 시도하는 내결함성(Fault-tolerance) 예외 처리가 반영됨
import time
import serial
import serial.tools.list_ports

class ArduinoSerialManager:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, port=&quot;/dev/ttyACM0&quot;, baudrate=9600, timeout=1):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.port = port
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.baudrate = baudrate
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.timeout = timeout
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def find_arduino_port(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;리눅스 시스템 내 아두이노 USB 장치 자동 식별&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ports = list(serial.tools.list_ports.comports())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for p in ports:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if &quot;Arduino&quot; in p.description or &quot;ch340&quot; in p.description.lower() or &quot;ttyACM&quot; in p.device:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[HW] 장치 탐색 완료: {p.device}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return p.device
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return self.port

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def connect(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.port = self.find_arduino_port()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser = serial.Serial(port=self.port, baudrate=self.baudrate, timeout=self.timeout)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[HW] 아두이노 연결 성공 -&amp;gt; {self.port}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except serial.SerialException as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[  연결 실패] 포트 열기 오류: {e}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser = None
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return False

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def start_reading(self, db_callback):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;무한 루프 데이터 수신 및 유실 예외 처리&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not self.ser and not self.connect():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;time.sleep(2) # 아두이노 리셋 대기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser.reset_input_buffer()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while self.is_running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.ser and self.ser.in_waiting &amp;gt; 0:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;line = self.ser.readline().decode(&quot;utf-8&quot;).strip()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if line == &quot;DETECT&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;[HW ➔ PC] 동작 감지 신호 포착&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;db_callback() # 팀원 B(DB), C(GUI) 모듈과 인터페이스 연동

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except (serial.SerialException, OSError):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;\n[  경고] 아두이노 USB 장치 연결 해제됨!&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.handle_reconnect(db_callback)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except UnicodeDecodeError:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue # 초기 노이즈 데이터 무시

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def send_trigger_to_hardware(self, command_char):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;PC ➔ 아두이노 물리 제어 신호 송신 ('W' or 'O')&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.ser and self.ser.is_open:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser.write(command_char.encode('utf-8'))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[PC ➔ HW] 제어 명령 송신: {command_char}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception as e:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;[HW] 제어 명령 송신 실패: {e}&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def handle_reconnect(self, db_callback):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;자동 재연결 메커니즘&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.ser: self.ser.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;[HW] 5초 간격으로 재연결 복구를 시도합니다...&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while not self.is_running:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;time.sleep(5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.connect():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.start_reading(db_callback)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def close(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.is_running = False
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.ser and self.ser.is_open:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ser.close()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;[HW] 시리얼 포트 자원 반환 완료.&quot;)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑨ sub_control_view.py&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# PyQt5 대시보드 통계 및 원격 제어 소스코드 (sub_control_view.py)
# 특징: QTimer를 이용하여 10초 주기로 DB 백엔드 통계를 자동 리프레시(Auto-Refresh)하며 그래프 캔버스를 다시 그린다

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QHBoxLayout, QMessageBox
from PyQt5.QtCore import QTimer
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas


class AnalyticsControlPanel(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, main_window, db_mgr, serial_mgr):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.main_win = main_window
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.db = db_mgr
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sm = serial_mgr
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.init_sub_ui()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def init_sub_ui(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sub_layout = QVBoxLayout(self)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 1. 원격 제어 토글 버튼 레이아웃
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ctrl_layout = QHBoxLayout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_biz = QPushButton(&quot;영업 모드&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_sec = QPushButton(&quot;보안 강화&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_biz.clicked.connect(lambda: self.toggle_mode(&quot;BUSINESS&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.btn_sec.clicked.connect(lambda: self.toggle_mode(&quot;SECURITY&quot;))

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ctrl_layout.addWidget(self.btn_biz)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ctrl_layout.addWidget(self.btn_sec)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sub_layout.addLayout(ctrl_layout)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 2. Matplotlib 차트 내장 인프라 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.figure, self.ax = plt.subplots(figsize=(5, 3))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.canvas = FigureCanvas(self.figure)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sub_layout.addWidget(self.canvas)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 3. 10초 주기 실시간 데이터 자동 동기화 타이머 가동
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.refresh_timer = QTimer()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.refresh_timer.timeout.connect(self.update_statistics_chart)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.refresh_timer.start(10000)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.update_statistics_chart()&amp;nbsp;&amp;nbsp;# 최초 1회 실행

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def toggle_mode(self, mode):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[역방향 물리 제어] 제어 명령 하향 전파&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.db:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.db.update_store_mode(mode)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if mode == &quot;BUSINESS&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if self.sm: self.sm.send_trigger_to_hardware('O')&amp;nbsp;&amp;nbsp;# 아두이노 경보 해제 명령 ('O')
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.information(self, &quot;모드 변경&quot;, &quot;정상 운영 모드로 전환되었습니다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;else:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;QMessageBox.warning(self, &quot;모드 변경&quot;, &quot;보안 감시 체제로 전환되었습니다. 침입 발생 시 경보가 울립니다.&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 메인 윈도우 상태 창 즉시 동기화 촉진
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.main_win.restore_ui_status()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def update_statistics_chart(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&quot;&quot;[시각화 Engine] DB 통계 기반 데이터 동적 드로잉&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not self.db: return

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 팀원 B가 구현한 시간대별 딕셔너리 리스트 취득
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;db_data = self.db.get_hourly_traffic_stats()

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hours = [row['event_hour'] for row in db_data]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;counts = [row['visitor_count'] for row in db_data]

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 데이터가 없을 시 가상 예외 방어 데이터 할당
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if not hours:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hours, counts = [12, 13, 14], [0, 0, 0]

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.clear()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.bar(hours, counts, color='#3498DB', edgecolor='#2980B9')
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.set_title(&quot;Hourly Visitor Traffic (Last 24 Hours)&quot;, fontsize=10)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.set_xlabel(&quot;Hour (24h)&quot;, fontsize=8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.set_ylabel(&quot;Detections&quot;, fontsize=8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ax.grid(True, axis='y', linestyle='--', alpha=0.7)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.figure.tight_layout()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.canvas.draw()&amp;nbsp;&amp;nbsp;# 캔버스 리렌더링&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;⑩ loTMemberControl.sql&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 리눅스 서버 환경에서 활용가능한 sql 코드
-- 리눅스 서버 환경의 MySQL 8.0 표준 엔진에 맞추어 작성되었으며, 다국어 지원 및 이모지 로그 저장을 위해 utf8mb4 인코딩을 적용
-- 1. 데이터베이스 생성 및 선택
CREATE DATABASE IF NOT EXISTS store_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE store_db;

-- 2. 매장 상태 관리 테이블 (마스터 테이블)
CREATE TABLE IF NOT EXISTS store_settings (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setting_id INT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;store_name VARCHAR(50) NOT NULL COMMENT '매장 지점명',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operation_mode VARCHAR(20) NOT NULL DEFAULT 'BUSINESS' COMMENT '현재 모드 (BUSINESS: 영업, SECURITY: 보안)',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '상태 변경 시간'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 시스템 가동을 위한 기본 데이터 삽입 (기본값: 영업 모드)
INSERT INTO store_settings (store_name, operation_mode) 
VALUES ('광주 AI 무인스토어 1호점', 'BUSINESS')
ON DUPLICATE KEY UPDATE updated_at = NOW();

-- 3. PIR 센서 감지 이력 테이블 (트랜잭션 데이터 테이블)
CREATE TABLE IF NOT EXISTS sensor_logs (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;log_id BIGINT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;device_id VARCHAR(20) NOT NULL DEFAULT 'ARDUINO_01' COMMENT '식별용 아두이노 ID',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sensor_type VARCHAR(20) NOT NULL DEFAULT 'PIR' COMMENT '센서 종류',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;detected_value INT NOT NULL DEFAULT 1 COMMENT '감지 값 (1: 움직임 발생)',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_mode VARCHAR(20) NOT NULL COMMENT '감지 시점의 매장 운영 모드',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '감지 로그 시각',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- 성능 최적화 인덱스: GUI 실시간 대시보드 및 AI 시계열 예측 가속화용
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INDEX idx_created_at (created_at),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;INDEX idx_mode_created (current_mode, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 4. [AI 연동 대비] 시간대별 방문객 집계 테이블
CREATE TABLE IF NOT EXISTS hourly_statistics (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stat_id INT AUTO_INCREMENT PRIMARY KEY,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stat_date DATE NOT NULL COMMENT '통계 날짜',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stat_hour INT NOT NULL COMMENT '통계 시간 (0~23시)',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;visitor_count INT NOT NULL DEFAULT 0 COMMENT '해당 시간 총 누적 방문 수',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;anomaly_score FLOAT DEFAULT 0.0 COMMENT 'AI 모델 결과 이상치 점수',
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-- 데이터 중복 및 왜곡 방지를 위한 유니크 제약
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;UNIQUE KEY uq_date_hour (stat_date, stat_hour)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;※ script_team.txt &lt;/b&gt;(참고사항 기록)&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# main.dashbord.py 실행 전 터미널에 pip install PyQt5 matplotlib pymysql pyserial이 설치되어 있는지 확인하기
dashboard = SmartStoreDashboard()
dashboard.show()
sys.exit(app.exec_())


A팀원 : 리눅스(Linux) 인프라 환경 설정 조치 내용
=========================================
# 1. 아두이노가 연결된 시리얼 장치 명칭 및 연결 상태 확인
ls -l /dev/ttyACM*

# 2. 파이썬 프로세스가 시리얼 포트에 접근할 수 있도록 읽기/쓰기 권한 강제 부여
sudo chmod 666 /dev/ttyACM0

# 3. [고도화] 매번 권한을 주기 번거로우므로 현재 계정(User)을 시리얼 다이얼아웃 그룹에 등록 (재부팅 후 영구 적용)
sudo usermod -a -G dialout $USER


B팀원 : 리눅스 기반 데이터베이스 원격 보안 설정 조치
==========================================
# 1. 리눅스 터미널에서 MySQL 설정 파일 편집 (IP 바인딩 해제)
# sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# bind-address = 127.0.0.1 부분을 0.0.0.0 으로 수정 후 저장

# 2. 리눅스 방화벽(UFW)에서 MySQL 기본 포트(3306) 허용 조치
sudo ufw allow 3306/tcp
sudo ufw reload

# 3. MySQL 내부 접속 후 외부 접속용 전용 계정 생성 및 권한 부여
# CREATE USER 'store_user'@'%' IDENTIFIED BY 'your_password';
# GRANT ALL PRIVILEGES ON store_db.* TO 'store_user'@'%';
# FLUSH PRIVILEGES;


# E팀원 : 리눅스 런타임 배포 인프라 및 AI 전처리 파이프라인 설계 보고서
==========================================================
# 구현 핵심 아키텍처팀원 E는 프로젝트 총괄(PM)로서 전체 소스코드 연동 통합 파일(app_runner.py)을 작성하고,
	# 개발 완료 후 리눅스 가상 서버 환경에서 터미널 접속만으로 시스템 전체를 한 번에 빌드하고 구동시킬 수 있는
	# 자동화 쉘 스크립트를 제작. 추후 적재된 sensor_logs 이력을 바탕으로 이상 징후 탐지 AI 모델 및 시계열 트래픽 예측
	# 원천 정형 데이터를 엔지니어링하는 데이터 파이프라인 뼈대를 구축

# 무인 보안 시스템 백업 및 의존성 환경 원클릭 자동 구축 스크립트
echo &quot;[인프라] 패키지 인덱스 업데이트 및 필수 종속성 파이썬 라이브러리 검증 시작...&quot;
sudo apt-get update -y

# PyQt5가 리눅스 환경 GUI 환경 가상 디스플레이(X11) 플러그인을 원활히 찾을 수 있도록 환경 변수 선언
export QT_QPA_PLATFORM=xcb

# 가상 환경 인프라 구축 및 파이썬 패키지 일괄 무중단 설치
echo &quot;[인프라] PyMySQL, PySerial, PyQt5, Matplotlib 인프라 종속성 다운로드 중...&quot;
pip install pymysql pyserial PyQt5 matplotlib --quiet

echo &quot;[인프라] USB 아두이노 통신 디바이스 권한 영구 개방 설정 조치&quot;
sudo chmod 666 /dev/ttyACM0 || echo &quot;[경고] 물리 아두이노 장치가 포트에 잡히지 않았습니다. 시뮬레이션 모드로 대치합니다.&quot;

echo &quot;[인프라] 스마트 매장 보안 모니터링 시스템 어플리케이션을 백그라운드로 실행합니다.&quot;
nohup python3 app_runner.py &amp;gt; system_run.log 2&amp;gt;&amp;amp;1 &amp;amp;

echo &quot;[인프라] 배포 성공! 실시간 런타임 로그 확인: 'tail -f system_run.log'&quot;


# 핵심 기술 설명 (팀원 C, D 공유용)
==============================
1. 비동기 이벤트 기반 스레드 설계 구조 (`QThread` &amp;amp; `pyqtSignal`):
&amp;nbsp;&amp;nbsp; &quot;파이썬 GUI 컴포넌트들은 싱글 스레드로 작동하기 때문에, 무한 루프로 시리얼 통신을 감시하면 화면 전체가 멈추는(Freezing) 크리티컬 락 현상이 
&amp;nbsp;&amp;nbsp; 발생함. 이를 해결하기 위해 **시리얼 수신 로직을 백그라운드 멀티스레드(`QThread`)로 완전히 분리하고, 데이터가 수집된 순간에만 인터럽트 
&amp;nbsp;&amp;nbsp; 신호(`pyqtSignal`)를 보내 화면을 렌더링**하는 안정적인 아키텍처를 구현함.&quot; 

2. 시뮬레이터(Mock 데이터) 환경 준비 능력 :
&amp;nbsp;&amp;nbsp; &quot;2일이라는 제한된 시간 속에서 하드웨어 제작과 소프트웨어 UI 개발이 병렬로 진행되어야 했음. 하드웨어가 완비되지 않은 시점에도 소프트웨어 결합성 
&amp;nbsp;&amp;nbsp; 테스트를 조기에 진행할 수 있도록 **가상 센서 이벤트 발생기(Random Sleep Loop)를 스레드 내부에 모킹(Mocking)하여 프로젝트 지연 리스크를 최소화
&amp;nbsp;&amp;nbsp;
※ 팀원 5인이 무인 매장 보안 토이 프로젝트를 완수하기 위한 문서들
&amp;nbsp;&amp;nbsp; (전체 기획안 ➔ 일정 계획서 ➔ 데이터베이스 DDL ➔ 시리얼 통신 예외 처리 코드 ➔ PyQt5 비동기 GUI 소스코드)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Project</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/53</guid>
      <comments>https://goodjop79.tistory.com/53#entry53comment</comments>
      <pubDate>Tue, 2 Jun 2026 18:41:39 +0900</pubDate>
    </item>
    <item>
      <title>D+38 [아두이노] Arduino(네오픽셀,2진수,소리감지,블루투스) 제어 실습</title>
      <link>https://goodjop79.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 6월 1일(월)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ Aduino(네오픽셀, 2진수, 소리감지, 블루투스) 제어 실습&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;ㅇ 실습 후기&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;① 아두이노를 활용한 기계제어 실습의 경우, 보드로만 작은 규모의 연동체계를 경험하였으나, 이것이 기초가 되어 기계산업에서데이타 코드로 신호를 기계에 전달하여 기계가 스마트하게 가동되는 등 지속적으로 발전하고 있다.&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp;② 그러나 프로그램의 업그레이드는 비용발생과 직결되어 기업의 영업이익에 영향을 줄 수도 있으므로 쉽게 신기술을 적용하지 못하는 경우가 많다. 특히 중소기업의 경우는 더 그러할 수도 있을 것이다.&lt;br&gt;&amp;nbsp; ③ 각 실습마다 모델이 다른 경우 코드작성시 애러 발생율이 높으며, 모델과 라이브러리 관리에서 해당되는 최신모델을 다운로드 받아서 일치시켜야 하는 번거로움이 많다.&lt;br&gt;&amp;nbsp; &amp;nbsp;④ 각급 학교 교육에서도 이와같은 교육을 지속해야 한다면 한번 구매한 교보재를 예산문제로 몇년동안 지속해서 사용해야 할 경우 빠르게 변하는 AI 시대에 어떤 것들을 체계화 해서 학교 교육에 새롭게 반영해야 할지 고민해 봐야 할 때인 것 같다.&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. 응용 스틱형 네오픽셀 다루기(교재 400P)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 준비물&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 네오픽셀 라이브러리 설치하기 (툴,라이브러리관리자 - 검색어 NeoPixel - Adafruit NeoPixel 선택 후 설치)&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. 회로 구성하기 (구성요소 기본 - 검색어 neo - 구성요소에서 활용)&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 스케치 코드 작성 및 읽기&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.2. 회로 구성 및 코드 작성&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pvmbp/dJMcaarZw88/KDQN9flv7nMuYNIsUam4NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pvmbp/dJMcaarZw88/KDQN9flv7nMuYNIsUam4NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pvmbp/dJMcaarZw88/KDQN9flv7nMuYNIsUam4NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpvmbp%2FdJMcaarZw88%2FKDQN9flv7nMuYNIsUam4NK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1606&quot; height=&quot;866&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [스틱형 네오픽셀 다루기 2] 교재400&amp;nbsp;&amp;nbsp;
# include &amp;lt;Adafruit_NeoPixel.h&amp;gt;
#define PIN&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 제어를 위한 디지털 핀 번호
#define NUMPIXELS&amp;nbsp;&amp;nbsp; 8&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 네오픽셀의 LED 수 (2*4개)
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
&amp;nbsp;&amp;nbsp;// 네오픽셀 라이브러리 초기화
&amp;nbsp;&amp;nbsp;pixels.begin(); 
}
void loop() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setColor();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay(100);
}

// 이후 부분은 램프에 백색등은 켜지지만 임의의 색상 변경은 제한됨, 교보재(NeoPixel)램프가 교재와 차이
//void loop() {
&amp;nbsp;&amp;nbsp;setColor();
&amp;nbsp;&amp;nbsp;delay(100);&amp;nbsp;&amp;nbsp;
}

void setColor();{
&amp;nbsp;&amp;nbsp;for (int i=0; i&amp;lt;; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int redColor = random(0, 256);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int greenColor = random(0,256)'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int blurColor = random(0,256);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pixels.setPixelColor(i,pixels.Color(redColor, greenColor, blueColor));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pixels.show();
&amp;nbsp;&amp;nbsp;}&amp;nbsp;&amp;nbsp; //&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.3. 실습 결과&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;램프에 백색등은 켜지지만 임의의 색상 변경은 제한됨, 교보재(NeoPixel) setColor 작성시 코드 오류 --&amp;gt; 코드 / 모델 일치화&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;1.4. 응용 스틱형 네오픽셀 다루기 보드에 &quot;램프등&quot; 의미&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 가. TX (Transmit / 송신):&lt;/b&gt; 아두이노가 컴퓨터나 다른 모듈로 데이터를 보내고 있을 때 깜빡임&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 나. RX (Receive / 수신):&lt;/b&gt; 아두이노가 컴퓨터나 다른 모듈로부터 데이터를 받고 있을 때 깜빡임&lt;/p&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;b&gt;다. 참고&amp;nbsp;및&amp;nbsp;주의&amp;nbsp;사항&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(1) 코드 업로드 중 : 아두이노 IDE에서 새 스케치(코드)를 보드에 올릴 때 이 램프들이 정신없이 깜빡이는 것은 정상적인 과정&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(2) 작동 중 깜빡임 : 센서나 블루투스 통신 등을 연결했을 때 데이터가 오고 갈 때마다 반짝이는 것도 정상&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(3) 항상 켜져 있는 경우 : 만약 아무런 통신 동작을 하지 않거나 코드가 작동하지 않는데도 TX/RX 램프가 계속 켜져 있거나&amp;nbsp; &amp;nbsp;꺼지지 않는다면, 선 연결에 문제가 있거나 보드 칩셋이 손상되었을 수 있음&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. &quot;2진수 카운터&quot; 만들기 (교재 391P)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1. &lt;/b&gt;&lt;b&gt;회로 구성 및 코드 작성&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1823&quot; data-origin-height=&quot;931&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1cEkb/dJMcaftgkmp/wXn3A995xzaUIa5J0dKKQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1cEkb/dJMcaftgkmp/wXn3A995xzaUIa5J0dKKQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1cEkb/dJMcaftgkmp/wXn3A995xzaUIa5J0dKKQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1cEkb%2FdJMcaftgkmp%2FwXn3A995xzaUIa5J0dKKQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1823&quot; height=&quot;931&quot; data-origin-width=&quot;1823&quot; data-origin-height=&quot;931&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1823&quot; data-origin-height=&quot;931&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xUvPl/dJMcabEoJyH/MPsxtUTfqfyVDYaQuRVHxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xUvPl/dJMcabEoJyH/MPsxtUTfqfyVDYaQuRVHxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xUvPl/dJMcabEoJyH/MPsxtUTfqfyVDYaQuRVHxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxUvPl%2FdJMcabEoJyH%2FMPsxtUTfqfyVDYaQuRVHxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1823&quot; height=&quot;931&quot; data-origin-width=&quot;1823&quot; data-origin-height=&quot;931&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 2진수 카운터 만들기(아두이노입문 교재 391p) 2026.06.01.

int flag = 0;&amp;nbsp;&amp;nbsp; // 버튼의 상태를 구분하기 위한 변수
int count = 0;&amp;nbsp;&amp;nbsp;// 카운터 변수 초기값
int pinNumber = 8;&amp;nbsp;&amp;nbsp;// 핀 개수
int ledPinNum[] = { 6,7,8,9,10,11,12,13 };&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// LED 제어를 위한 디지털 핀 번호

void setup()&amp;nbsp;&amp;nbsp;
{
&amp;nbsp;&amp;nbsp;// LED 제어를 위한 출력 모드
&amp;nbsp;&amp;nbsp;for(int i=0; i&amp;lt;pinNumber; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pinMode(ledPinNum[i], OUTPUT);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;}
// 버튼 제어를 위한 입력 모드
&amp;nbsp;&amp;nbsp;pinMode(2, INPUT);
}

void loop() 
{

&amp;nbsp;&amp;nbsp;int inputValue = digitalRead(2);
&amp;nbsp;&amp;nbsp;if (inputValue == HIGH) {&amp;nbsp;&amp;nbsp;// 푸시 버튼이 눌러졌을 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (flag == 0) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 상태 변경
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;flag = 1;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;} else {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 푸시 버튼이 떼어졌을 때
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (flag == 1) { 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;count++;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 카운트값 1씩 증가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;displayBinaryLED(count); // 카운터값을 LED로 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;flag = 0;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 상태 구분 변수를 초기화
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;delay(100);&amp;nbsp;&amp;nbsp; // 100ms 지연시간정&amp;nbsp;&amp;nbsp;// 100ms 지연 시간 
}

void displayBinaryLED(int num)
{
&amp;nbsp;&amp;nbsp;for (int i = 0; i &amp;lt; pinNumber; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int state = (num &amp;gt;&amp;gt; i) &amp;amp; 1;&amp;nbsp;&amp;nbsp;// 각 비트의 상태를 가져옴
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(ledPinNum[i], state);&amp;nbsp;&amp;nbsp;// LED 상태를 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.2. 실습 결과&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp;가. 코드를 스케치 컴파일 및 업로드 하는데 애러는 발생하지 않았으며, 버튼 제어를 통한 LED 제어 등 실습간 선 연결을 잘못해서 램프에 불이 들어오지 않았다. &lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. TX / RX 에 불이 깜빡이는 것으로 보아 아두이노가 컴퓨터나 다른 모듈로 데이터를 보내고 있음이 확인되었다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 기계는 사용자가 설치한 데로 답을 주듯이 사용자가 어떻게 명령하고, 설계 및 설비 하느냐에 따라 결과 값은 달라진다.&amp;nbsp;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 라. 카운트 값이 변화될 때 버튼으로 제어해서 불빛을 출력하는 기계제어의 원리를 조금은 이해하게 되었다.&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. &quot;소리감지 센서 모듈&quot;로 소리 감지기 만들기 (교재 416P)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.1. 준비물 : &lt;/b&gt;LED 1개, 저항(220Ω) 1개, 브레드보드 1개&amp;nbsp;&lt;br&gt;&lt;b&gt;3.2.&amp;nbsp;&lt;/b&gt;&lt;b&gt;회로 구성 및 코드 작성&lt;/b&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1643&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ev22hh/dJMcabqTueo/JqjClTCz7i8snI3jwdKrjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ev22hh/dJMcabqTueo/JqjClTCz7i8snI3jwdKrjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ev22hh/dJMcabqTueo/JqjClTCz7i8snI3jwdKrjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fev22hh%2FdJMcabqTueo%2FJqjClTCz7i8snI3jwdKrjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1643&quot; height=&quot;886&quot; data-origin-width=&quot;1643&quot; data-origin-height=&quot;886&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 소리감지 센서 모듈로 소리 감지기 만들기(아두이노입문 교재 416p) 2026.06.01.
// 코드 작성후 공간애러가 있다면 툴에서 자동포멧을 선택하면 코드가 바로 정렬이 된다.

const int soundSensorPin = A0;&amp;nbsp;&amp;nbsp;// 소리 감지 센서 모듈을 연결한 핀 번호
const int ledPin = 13;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 아두이노에 LED를 연결한 핀 번호

int clapCount = 0;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 박수 소리를 감지하기 위한 변수
unsigned long previousTime = 0;&amp;nbsp;&amp;nbsp;// 이전 시간을 저장하느 변수
unsigned long clapInterval = 500; // 박수 소리의 간격을 정의하는 변수
const int thresholdValue = 100;&amp;nbsp;&amp;nbsp;// 박수 소리 감지를 위한 임계치 설정

void setup() {
&amp;nbsp;&amp;nbsp;Serial.begin(9600);
&amp;nbsp;&amp;nbsp;pinMode(ledPin, OUTPUT);
}

void loop() {

&amp;nbsp;&amp;nbsp;unsigned long currentTime = millis();&amp;nbsp;&amp;nbsp;// 밀리초 단위의 현재 시간

&amp;nbsp;&amp;nbsp;int sensorValue = analogRead(soundSensorPin); //소리 감지센서 모듈에서 값을 읽음

&amp;nbsp;&amp;nbsp;Serial.println(sensorValue); //시리얼 모니터에 값을 출력

&amp;nbsp;&amp;nbsp;if (sensorValue &amp;gt; thresholdValue) {&amp;nbsp;&amp;nbsp; // 임계치 이상의 소리가 감지되면 박수 소리를 인식
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (currentTime - previousTime &amp;gt; clapInterval) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clapCount++;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;previousTime = currentTime;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;if (clapCount &amp;gt;= 1) {&amp;nbsp;&amp;nbsp;// 박수 소리가 감지되면 LED의 상태를 변경
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int status = digitalRead(ledPin);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(ledPin, !status);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clapCount = 0;&amp;nbsp;&amp;nbsp; // 박수 소리 감지 회수 초과
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// 지연시간
&amp;nbsp;&amp;nbsp;delay(50);

}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3.3. 실습 결과&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;'소리감지 센서' 교보재 제한에 따른 소리 실습은 시뮬레이션으로만 진행함&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 스마트폰으로 원격 제어장치 제어하기(교재 379P)&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.1. 준비물 : &lt;/b&gt;스마트폰,&amp;nbsp;블루투스&amp;nbsp;모듈(HM-10,다운로드),&amp;nbsp;아두이노&amp;nbsp;보드,&amp;nbsp;LED,&amp;nbsp;저항(200Ω),&amp;nbsp;브레드보드&lt;/p&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;** 미흡분야 추가 실습 예정 **&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#include &amp;lt;SoftwareSerial.h&amp;gt;
SoftwareSerial BTSerial(12,13);

void setup(){
BTSerial.begin(9600);
Serial.begin(9600);
}
void loop(){
 if (BTSerial.available()) {
&amp;nbsp;&amp;nbsp;char ch = BTSerial.read();
&amp;nbsp;&amp;nbsp;Serial.println(ch);
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;if (ch == '1')
&amp;nbsp;&amp;nbsp;	digitalWrite(11, HIGH);
&amp;nbsp;&amp;nbsp;else
&amp;nbsp;&amp;nbsp;	digitalWrite(11,LOW);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Arduino</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/52</guid>
      <comments>https://goodjop79.tistory.com/52#entry52comment</comments>
      <pubDate>Mon, 1 Jun 2026 20:00:21 +0900</pubDate>
    </item>
    <item>
      <title>D+36 [Python] OOP(객체지향프로그래밍), Class 개념과 활용</title>
      <link>https://goodjop79.tistory.com/50</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일&amp;nbsp; 자 :&amp;nbsp; 2026년 5월 29일(목)&lt;/b&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;제&amp;nbsp; 목 : OOP(객체지향프로그래밍),&amp;nbsp;Class&amp;nbsp;개념과&amp;nbsp;활용&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 서&amp;nbsp; 론&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; 현대 소프트웨어 개발 환경은 과거에 비해 비교할 수 없을 만큼 복잡해지고, 요구사항은 끊임없이 변화하고 있다. &lt;br&gt;초기 프로그래밍 방식이었던 절차지향 프로그래밍은 프로그램을 단순히 명령어의 순차적인 실행으로 보았기에, 규모가 커질수록 코드를 이해하고, 수정하고, 유지보수하는 것이 기하급수적으로 어려워지는 한계에 봉착했다.&lt;br&gt;&amp;nbsp; 이러한 한계를 극복하고 더 유연하고 재사용 가능한 소프트웨어를 만들기 위해 등장한 것이 바로 객체지향 프로그래밍(OOP)이다.&lt;b&gt; 객체지향프로그래밍(OOP)는 &quot;현실 세계의 사물과 개념&quot;을 프로그램의 독립된 단위인 '객체'로 모델링하여 프로그램을 구성&lt;/b&gt;한다.&lt;br&gt;&amp;nbsp; 신입 개발자가 &lt;b&gt;OOP의 핵심 개념과 구현하는 도구인 Class를 반드시 이해하고 적용&lt;/b&gt;해야 하는 이유는 다음과 같다.&lt;br&gt;&amp;nbsp; 첫째, &lt;b&gt;유지보수성 향상&lt;/b&gt;이다. 프로그램이 거대해질수록 스파게티 코드처럼 엉킨 코드는 수정할 때마다 새로운 버그를 유발한다. OOP는 코드 간의 '의존성'을 줄이고 독립된 '객체' 단위로 관리하여, 변화에 유연하게 대처할 수 있게 한다. &lt;br&gt;&amp;nbsp; 둘째, &lt;b&gt;코드 재사용성&lt;/b&gt;이다. 잘 설계된 Class는 다른 프로젝트나 프로그램의 다른 부분에서 그대로 재사용할 수 있어 개발 속도를 높이고 중복 코드를 줄여준다. &lt;br&gt;&amp;nbsp; 셋째, &lt;b&gt;협업의 용이성&lt;/b&gt;이다. OOP를 기반으로 한 설계는 코드의 의미를 명확하게 전달하며, 각 개발자가 독립된 객체나 모듈을 담당하여 개발할 수 있는 구조를 제공한다.&lt;br&gt;따라서 신입 개발자에게 OOP와 Class는 단순히 지나쳐가는 기술이 아니라, 복잡한 현대 소프트웨어 개발 환경에서 살아남고 성장하기 위해 반드시 갖춰야 할 필수적인 기술적 체력이자 기본기라고 할 수 있다.&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가. 신입개발자가 갖춰야 할 기술적 목표는 무엇인가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) 신입 개발자의 현실적인 기술 목표&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; 신입개발자는 프로그래밍 이라는 소프트웨어 개발의 각 단계를 이해하고 이를 바탕으로 자신이 만든 Project 라는 소프트웨어 결과를 Portfolio 에 정리해야 한다. 신입 개발자의 참신함은 ‘전체 흐름’을 꿰뚫는 Portfolio 구축에 있다. &quot;class까지는 순차적인 학습이 가능하지만 이후는 내용이 순차적이 아니라 복합적인 교육이 진행된다&quot;. &lt;br&gt;프로그래밍이라는 소프트웨어 개발의 각 단계를 올바르게 이해하고, 그 결과를 Project 기반의 Portfolio로 증명해 내야 한다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;포함할 영역:&lt;/b&gt; 암호화, 권한 관리, 배포 환경 구축&lt;/li&gt;&lt;li&gt;&lt;b&gt;제외할 영역:&lt;/b&gt; 장애 대응, 대용량 로그 및 모니터링 (현실적으로 신입 단계에서는 어렵다)&lt;/li&gt;&lt;/ul&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 본 론&lt;/b&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가. OOP (객체지향 프로그래밍) 이란 무엇인가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) 의 미&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;OOP&lt;b&gt;(Object-Oriented Programming,&lt;/b&gt;&lt;b&gt; 객체 지향 프로그래밍)&lt;/b&gt;는 프로그램을 단순히 명령어의 순차적인 나열로 보는 것이 아니라, 데이터와 기능을 가진 독립된 단위인 '객체(Object)'들의 집합과 상호작용으로 파악하는 프로그래밍 패러다임이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(2) 등장 배경&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;초기 컴퓨터 프로그래밍은 하드웨어를 직접 제어하는 기계어와 어셈블리어를 거쳐, 인간의 언어와 유사한 고급 언어(C 등)를 사용하는 절차지향 프로그래밍으로 발전 했다. 절차지향은 문제를 잘게 쪼개어 순서대로 해결하는 방식에 탁월하여 초기 소프트웨어 개발을 주도했다. 하지만 하드웨어 성능의 발전과 함께 소프트웨어의 규모와 복잡도가 상상 이상으로 커지면서 문제가 발생했다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;스파게티 코드:&lt;/b&gt; 프로그램의 각 부분이 서로 밀접하게 연결되어(높은 결합도) 코드 한 줄을 수정하면 프로그램의 전혀 다른 부분에서 예측할 수 없는 오류가 발생했다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;유지보수의 어려움:&lt;/b&gt; 코드를 이해하고 수정하는 데 드는 비용이 새로운 프로그램을 만드는 비용을 넘어서는 '소프트웨어 위기'가 발생했다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;코드 재사용의 한계:&lt;/b&gt; 비슷한 기능을 하는 코드를 다른 프로그램에서 다시 사용하기가 매우 어려워, 매번 새로운 코드를 작성해야 했다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, 현실 세계의 '객체(Object)'라는 개념에 주목하게 되었다. &lt;br&gt;현실 세계의 사물(예: 자동차)은 고유한 데이터(색상, 속도)와 기능(달리기, 멈추기)을 가지고 있으며, 다른 사물과 상호작용 한다.&lt;br&gt;프로그램 또한 이처럼 독립된 데이터와 기능을 가진 '객체'들의 집합으로 구성하고, 이 객체들이 서로 메시지를 주고받으며(함수 호출) 작동하게 함으로써, 코드 간의 연결 고리를 끊고(낮은 결합도) 각 객체의 독립성을 높여(높은 응집도) 복잡도를 관리하고 재사용성을 높이고자 탄생한 것이 바로 객체지향 프로그래밍 이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(3) 객체지향 이후의 학습은 '순환선'&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;신입 개발자의 현실적인 기술 목표에서 언급했듯, 기초 문법부터 Class 기초까지는 순차적으로 학습할 수 있다.&amp;nbsp; &amp;nbsp; 하지만 OOP의 실제 활용과 심화 학습은 단선적인 도로가 아니라 복합적인 개념들이 서로 얽혀있는 '순환선'과도 같다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMxxG/dJMcaciVtT9/g8M75RJp4QvXzlRQdLGE70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMxxG/dJMcaciVtT9/g8M75RJp4QvXzlRQdLGE70/img.png&quot; data-alt=&quot;단계별 순차적 학습 구간 : 디자인패턴 - 추상화 - 인터페이스 - 솔리드(객체지향)원칙 - 이벤트대리인) - 디자인패턴&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMxxG/dJMcaciVtT9/g8M75RJp4QvXzlRQdLGE70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMxxG%2FdJMcaciVtT9%2Fg8M75RJp4QvXzlRQdLGE70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;단계별 순차적 학습 구간 : 디자인패턴 - 추상화 - 인터페이스 - 솔리드(객체지향)원칙 - 이벤트대리인) - 디자인패턴&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(4) 핵심 개념 3가지&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;클래스(Class):&lt;/b&gt; 객체를 만들기 위한 설계도 또는 틀&lt;/li&gt;&lt;li&gt;&lt;b&gt;객체(Object):&lt;/b&gt; 설계도를 바탕으로 메모리에 구현된 실체&lt;/li&gt;&lt;li&gt;&lt;b&gt;메서드(Method):&lt;/b&gt; 객체가 수행할 수 있는 행동이나 기능(함수)&lt;/li&gt;&lt;/ul&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k98qx/dJMcagMubAp/adU8Ti1yI5zrJT2HbtYznK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k98qx/dJMcagMubAp/adU8Ti1yI5zrJT2HbtYznK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k98qx/dJMcagMubAp/adU8Ti1yI5zrJT2HbtYznK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk98qx%2FdJMcagMubAp%2FadU8Ti1yI5zrJT2HbtYznK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;(5) OOP의 4대 특징&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,0,0&quot;&gt;캡슐화 (Encapsulation)&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;데이터와 데이터를 처리하는 함수를 하나로 묶는다&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span&gt;외부에서 객체 내부 정보에 직접 접근하지 못하도록 숨겨 데이터를 보호(정보 은닉)&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,1,0&quot;&gt;상속 (Inheritance)&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;자식 클래스가 부모 클래스의 속성과 기능을 그대로 물려받는다&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span&gt;이미 만든 코드를 재사용하여 중복 코드를 줄이고 확장성을 높인다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,2,0&quot;&gt;다형성 (Polymorphism)&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;하나의 역할이나 메서드가 상황에 따라 다양한 방식으로 동작하는 성질&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span&gt;하위 클래스에서 메서드를 재정의하는 오버라이딩(Overriding)이 대표적인 예시&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;30,3,0&quot;&gt;추상화 (Abstraction)&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;30,3,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;복잡한 현실 세계의 사물에서 필요한 공통 속성과 핵심 기능만 추출하여 모델링하는 과정&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;span&gt;불필요한 세부 사항은 숨기고 중요한 개념에 집중할 수 있게 한다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;나. Class 예시를 통한 개념 이해&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;유아교육을 위한 동물 사운드 및 분류 앱의 시각화 설계도입니다. 객체 지향 프로그래밍(OOP)의 &lt;b&gt;상속(Inheritance)&lt;/b&gt; 개념을 깊이 있게 적용하여, &lt;b&gt;동물&lt;/b&gt;이라는 공통 설계도를 바탕으로 &lt;b&gt;조류, 포유류, 어류&lt;/b&gt;라는 중간 분류를 만들고, 최종적으로 &lt;b&gt;독수리, 호랑이, 상어&lt;/b&gt;라는 구체적인 객체를 효율적으로 구현한다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(1) 동물 사운드 앱 : 깊은 상속 다이어그램&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 다이어그램은 부모 클래스인 '동물'에서 시작하여, 각 동물의 생물학적 분류를 '자식 클래스'로 정의하고, 최종적으로 구체적인 동물 종류를 '손자 클래스'로 정의하는 3단계 계층 구조를 보여준다. 이를 통해 공통 속성과 기능을 최상위 클래스에서 정의하고 하위 클래스에서 이를 확장 및 재사용하는 구조를 시각화 한다.&lt;br&gt;&lt;i&gt;※ '동물 - 조류 - 포유류 - 어류'는 계층 구조상 모호한 표현이 있어, 표준적인 '조류, 포유류, 어류' 계층 구조로 정리하였음.&lt;/i&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3BOy1/dJMcadvkVu5/8J8bkzVW2ExSk5edYHvN81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3BOy1/dJMcadvkVu5/8J8bkzVW2ExSk5edYHvN81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3BOy1/dJMcadvkVu5/8J8bkzVW2ExSk5edYHvN81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3BOy1%2FdJMcadvkVu5%2F8J8bkzVW2ExSk5edYHvN81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;(2) 구조 설계 상세 설명&lt;/b&gt;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;최상위 부모 클래스: 동물 (Animal)&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;모든 동물들이 공통으로 가지는 기본 틀&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,1,1,0&quot;&gt;속성(Attributes) :&lt;/b&gt;&lt;span&gt; 이름(name)&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,1,2,0&quot;&gt;기능(Methods) :&lt;/b&gt;&lt;span&gt; 소리를 재생하는 버튼 동작(playSound)&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;중간 자식 클래스 (분류) : 조류, 포유류, 어류&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;부모 클래스(Animal)를 상속받아 생성되며,&lt;/span&gt;&lt;span&gt; 각 분류의 공통 속성이나 기능을 추가할 수 있다.&lt;/span&gt;&lt;span&gt; 여기선 예시를 위해 직접 상속을 보여 준다.&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;최하위 자식 클래스 (손자): 구체적인 동물 종류&lt;/b&gt; 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;span&gt;해당하는 중간 분류 클래스를 상속받아,&lt;/span&gt;&lt;span&gt; 각 동물에 맞는 고유한 소리(sound)만 따로 설정&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,1,1,0&quot;&gt;독수리 (Eagle):&lt;/b&gt;&lt;span&gt; sound = &quot;끼에엑&quot;&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,1,2,0&quot;&gt;호랑이 (Tiger):&lt;/b&gt;&lt;span&gt; sound = &quot;어흥&quot;&lt;/span&gt;&lt;/li&gt; 
   &lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,1,3,0&quot;&gt;상어 (Shark):&lt;/b&gt;&lt;span&gt; sound = &quot;(무소음)&quot;&lt;/span&gt;&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;(3) 파이썬 코드 구현 예시&lt;/b&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;이 구조를 활용해 실제 웹이나 앱에서 버튼을 누를 때 소리가 나도록 파이썬으로 프로그래밍 한다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 1. 최상위 부모 클래스 정의
class Animal:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, name):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.name = name

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def play_sound(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 자식 클래스에서 sound 속성을 정의해야 함을 명시 (강제성 부여)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;raise NotImplementedError(&quot;Subclass must implement abstract attribute 'sound'&quot;)


# 2. 중간 자식 클래스 정의 (분류)
class Aves(Animal): # 조류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, name):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(name)

class Mammalia(Animal): # 포유류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, name):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(name)

class Pisces(Animal): # 어류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, name):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(name)


# 3. 최하위 자식 클래스 정의 (손자, 구체적인 동물)
class Eagle(Aves): # 독수리는 조류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(&quot;독수리&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sound = &quot;끼에엑&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def play_sound(self): # 오버라이딩: 각자 고유한 소리 출력
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{self.name}이(가) 하늘에서 '{self.sound}'! 소리를 냅니다.&quot;)

class Tiger(Mammalia): # 호랑이는 포유류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(&quot;호랑이&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sound = &quot;어흥&quot;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def play_sound(self): # 오버라이딩
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{self.name}이(가) 숲속에서 '{self.sound}'! 소리를 냅니다.&quot;)

class Shark(Pisces): # 상어는 어류
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(&quot;상어&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.sound = &quot;(무소음)&quot; # 상어는 소리를 내지 않음

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def play_sound(self): # 오버라이딩: 소리가 없는 경우에 대한 구현
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;{self.name}이(가) 바다 속에서 {self.sound}으로 헤엄칩니다.&quot;)


# 4. 객체 생성 및 실행 (버튼 클릭 시 동작 시뮬레이션)

# 예시: 각 분류별로 손자 객체 생성
eagle_button = Eagle()
tiger_button = Tiger()
shark_button = Shark()

# 예시: 독수리 버튼 클릭
eagle_button.play_sound() 
# 출력: 독수리이(가) 하늘에서 '끼에엑'! 소리를 냅니다.

# 예시: 호랑이 버튼 클릭
tiger_button.play_sound()
# 출력: 호랑이이(가) 숲속에서 '어흥'! 소리를 냅니다.

# 예시: 상어 버튼 클릭 (소리 대신 메시지)
shark_button.play_sound()
# 출력: 상어이(가) 바다 속에서 (무소음)으로 헤엄칩니다.&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;이처럼 계층적인 설계를 하면, 새로운 동물(예: 참새)이 추가되더라도 기존의 조류(Aves) 클래스를 상속받아 이름과 소리만 정의해주면 매우 쉽게 기능을 확장할 수 있다. 상위 클래스(Animal, Aves 등)의 코드를 건드릴 필요가 없어 안정적이다.&lt;/b&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #1B711D;&quot;&gt;다. 프로그램 작성시 우리는 왜&amp;nbsp; 파일(&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #1B711D;&quot;&gt;.py)을&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #1B711D;&quot;&gt;분리 해야 하는가?&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; 위젯의 통합 코드를 여러 파일(main.py,&amp;nbsp; Widget.py,&amp;nbsp; from_tab.py,&amp;nbsp; buttons_tab.py 등)로 분리하여 사용하는 가장 큰 이유는&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;u&gt;유지보수의 편의성과 코드의 재사용성 때문&lt;/u&gt;&lt;/b&gt;이다. 코드&lt;/span&gt;가 한 파일에 모두 모여 있으면 초기 개발은 빠를 수 있지만, 프로그램이 커질수록 &quot;수정&quot;과 &quot;관리&quot;가 불가능해 진다. 그러므로 분야별 분담과 유지보수 편의성을 위해 class부분을 분리하여 관리한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;(1) 파일별 역할 분담 구조 (예시)&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- main.py : 프로그램의 시작점. 전체 애플리케이션을 실행하고 메인 윈도우를 띄우는 진입로 역할&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- Widget.py : 메인 화면의 큰 틀이나 공통 레이아웃을 정의하고 조각 파일들을 하나로 조립하는 본체 역할&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- form_tab.py : 입력창, 콤보박스 등 데이터 입력과 관련된 UI와 로직만 집중적으로 관리&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- buttons_tab.py: 버튼의 배치와 버튼을 눌렀을 때 실행될 기능(이벤트)만 따로 모아 관리&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;(2) 소스코드를 분리하는 핵심 이유&lt;/b&gt;&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- 가독성 향상 : 한 파일이 수천 줄이 되는 것을 방지하여 코드를 쉽게 읽을 수 있다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; - 유지보수 용이 :&lt;span style=&quot;color: #8A3DB6;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;color: #1B711D;&quot;&gt;버튼 기능을 수정할 때 buttons_tab.py만 열어서 고치면 되므로&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;&amp;nbsp;다른 코드를 건드릴 위험이 줄어든다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; - 코드 재사용 &lt;span style=&quot;color: #1B711D;&quot;&gt;:&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;color: #1B711D;&quot;&gt;form_tab.py에서 만든 입력 양식을 다른 화면이나 다른 프로젝트에서 그대로 가져다 쓸 수&lt;/span&gt;&lt;/u&gt;&lt;span style=&quot;color: #8A3DB6;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;있다.&lt;br&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; - 협업 수월 : 여러 개발자가 동시에 작업할 때, 파일이 분리되어 있으면&amp;nbsp;&lt;u&gt;깃(Git) 충돌&lt;/u&gt;&amp;nbsp;없이 각자 맡은 화면을 개발할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 예시 : main.py 설명&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;&amp;nbsp;from PyQt6.QtWidgets import QWidget&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;# 또는 PySide6.QtWidgets&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;&amp;nbsp; &amp;nbsp;class Widget(QWidget)&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;:&amp;nbsp; &amp;nbsp;# QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;&amp;nbsp;&amp;nbsp;def __init__(self, parent=None)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;:&amp;nbsp;&amp;nbsp;# 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(parent) :&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 부모 클래스(QWidget)의 생성자를 호출해 위젯을 초기화, 부모-자식 관계 설정&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 결&amp;nbsp; 론&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;결국 객체지향 프로그래밍(OOP)은 현대 소프트웨어 개발에서 복잡성을 관리하고 변화에 유연하게 대응하기 위한 가장 강력한 무기이다. 그리고 이러한 OOP의 철학을 실제 코드로 구현해 내는 핵심 도구가 바로 클래스(Class) 이다.&lt;br&gt;클래스는 &lt;b&gt;복잡한 현실 세계의 개념을 추상화하여 안전하게 묶어두는 설계&lt;/b&gt;도 역할을 한다.&amp;nbsp; 개발자는 잘 설계된 클래스를 바탕으로 코드의 중복을 획기적으로&lt;span style=&quot;color: #006DD7;&quot;&gt; 줄이고(상속)&lt;/span&gt;, 내부 데이터를 안전하게 &lt;span style=&quot;color: #006DD7;&quot;&gt;보호하며(캡슐화&lt;/span&gt;), 상황에 따라 &lt;span style=&quot;color: #006DD7;&quot;&gt;유연하게 대처하는 코드(다형성&lt;/span&gt;)를 작성할 수 있다.&lt;br&gt;신입 개발자에게 클래스를 올바르게 이해하고 적용하는 것은 단순한 문법 습득을 넘어, 거대하고 복잡한 소프트웨어 생태계로 진입하기 위한 첫 단추다. 클래스라는 기본 설계도를 탄탄히 다질 때, 비로소 고도화된 디자인 패턴이나 아키텍처라는 넓은 구조를 이해하고 다룰 수 있는 진정한 기술적 성장을 이룰 수 있을 것이다. &quot;도전은 계속된다&quot;.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;※ 기타 참고&lt;/span&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;스파게티 코드&amp;gt;&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ㅇ 1970년대 후반~1980년대 컴퓨터 학계의 공식 등장&lt;/b&gt; : &lt;b&gt;&amp;nbsp;'스파게티'라는 비유가 문헌에 등장하기 시작한 것은 1970년대 후반&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 1978년 Guy Steele이 저술한 맥카시(McCarthy)의 LISP 역사 관련 논문 등에서 프로그램 구조가 엉킨 모습을 '스파게티'에 비유하기 시작했다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 이후&amp;nbsp;1980년대 후반, ISO(국제표준화기구)의 워킹 그룹 문서&amp;nbsp;등에서&lt;b&gt; &quot;구조화되지 않고 제어 흐름이 복잡하게 뒤틀린 소스 코드&quot;&lt;/b&gt;를 공식적으로 'Spaghetti Code'라고 명명하면서 업계 표준 용어로 자리 잡았다.&lt;/span&gt;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ㅇ 1990년대 '안티패턴(Anti-Pattern)'으로 정의&lt;/span&gt;&lt;/b&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1998년 출간된 소프트웨어 공학의 명저 《안티패턴(AntiPatterns)》 책에서 소프트웨어 개발 시 절대 하지 말아야 할 대표적인 실패 사례(디자인 안티패턴) 중 하나로 '스파게티 코드'를 공식 등록하면서 오늘날까지 널리 쓰이게 되었다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 요약하자면&amp;nbsp;스파게티 코드는 &quot;코드가 얽히고설켜 유지보수가 불가능한 상태&quot;를 뜻하며, 그 근거와 어원은 1960~70년대 무분별한 GOTO문 사용으로 코드가 뒤엉키는 현상을&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;소프트웨어 공학자들이 스파게티 면발에 비유하여 비판한 데서 유래했다.&amp;nbsp; &amp;nbsp; 이를 방지하기 위해 탄생한 대표적인 패러다임이 바로 '객체지향 프로그래밍(OOP)'과 '클래스(Class) 기반 설계' 이다.&amp;nbsp; 끝.&lt;/span&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/50</guid>
      <comments>https://goodjop79.tistory.com/50#entry50comment</comments>
      <pubDate>Fri, 29 May 2026 00:28:26 +0900</pubDate>
    </item>
    <item>
      <title>D+36 [Layout] .py 파일분리 왜 할까 ? 레이아웃 종합 예제 학습(2)</title>
      <link>https://goodjop79.tistory.com/49</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 28일(목)&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ&amp;nbsp;내용&amp;nbsp;:&amp;nbsp;Layout&amp;nbsp;가상환경에서&amp;nbsp;종합&amp;nbsp;예제&amp;nbsp;학습&amp;nbsp;하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;1. QtabWidget을 통해 파일분리를 실습하였다. 왜 .py 파일분리를 해야 하는가?&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위젯의 통합 코드를 여러 파일(main.py,&amp;nbsp; Widget.py,&amp;nbsp; from_tab.py,&amp;nbsp; buttons_tab.py 등)로 분리하여 사용하는 가장 큰 이유는&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;유지보수의 편의성과 코드의 재사용성 때문&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;이다. 코드가 한 파일에 모두 모여 있으면 초기 개발은 빠를 수 있지만, 프로그램이 커질수록 &quot;수정&quot;과 &quot;관리&quot;가 불가능해 진다. 그러므로 분야별 분담과 유지보수 편의성을 위해 class부분을 분리하여 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.1. 파일별 역할 분담 구조&lt;/b&gt;&lt;br /&gt;- main.py : 프로그램의 시작점. 전체 애플리케이션을 실행하고 메인 윈도우를 띄우는 진입로 역할&lt;br /&gt;- Widget.py : 메인 화면의 큰 틀이나 공통 레이아웃을 정의하고 조각 파일들을 하나로 조립하는 본체 역할&lt;br /&gt;- form_tab.py : 입력창, 콤보박스 등 데이터 입력과 관련된 UI와 로직만 집중적으로 관리&lt;br /&gt;- buttons_tab.py: 버튼의 배치와 버튼을 눌렀을 때 실행될 기능(이벤트)만 따로 모아 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.2. 소스코드를 분리하는 핵심 이유&lt;/b&gt;&lt;br /&gt;- 가독성 향상 : 한 파일이 수천 줄이 되는 것을 방지하여 코드를 쉽게 읽을 수 있다.&lt;br /&gt;- 유지보수 용이 :&lt;span style=&quot;color: #8a3db6;&quot;&gt; &lt;b&gt;&lt;u&gt;버튼 기능을 수정할 때 buttons_tab.py만 열어서 고치면 되므로&lt;/u&gt;&lt;/b&gt;&lt;/span&gt; 다른 코드를 건드릴 위험이 줄어든다.&lt;br /&gt;- 코드 재사용 : &lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&lt;u&gt;form_tab.py에서 만든 입력 양식을 다른 화면이나 다른 프로젝트에서 그대로 가져다 쓸 수&lt;/u&gt; &lt;/b&gt;&lt;/span&gt;있다.&lt;br /&gt;- 협업 수월 : 여러 개발자가 동시에 작업할 때, 파일이 분리되어 있으면 &lt;u&gt;깃(Git) 충돌&lt;/u&gt; 없이 각자 맡은 화면을 개발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;※ 예시 : main.py 설명&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;from PyQt6.QtWidgets import QWidget&lt;/span&gt;&amp;nbsp; &amp;nbsp;# 또는 PySide6.QtWidgets&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp; &amp;nbsp;class Widget(QWidget) &lt;/span&gt;:&amp;nbsp; &amp;nbsp;# QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt; &amp;nbsp;def __init__(self, parent=None)&lt;/span&gt; :&amp;nbsp;&amp;nbsp;# 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(parent) :&lt;/span&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt; # 부모 클래스(QWidget)의 생성자를 호출해 위젯을 초기화, 부모-자식 관계 설정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Layout (QTabWidget) 예시 문제 실습&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1)&amp;nbsp; 간단한 계산기 만들기 (계산기 화면 결과값 오른쪽 정렬)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;계산기 수정전.png&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCLacb/dJMcageBOI3/vmY5vIfmCxDrbkpjC4tkeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCLacb/dJMcageBOI3/vmY5vIfmCxDrbkpjC4tkeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCLacb/dJMcageBOI3/vmY5vIfmCxDrbkpjC4tkeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCLacb%2FdJMcageBOI3%2FvmY5vIfmCxDrbkpjC4tkeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;809&quot; height=&quot;868&quot; data-filename=&quot;계산기 수정전.png&quot; data-origin-width=&quot;647&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779966681205&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QGridLayout
from PySide6.QtCore import Qt


class CalculatorWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle(&quot;계산기&quot;)
        self.setMinimumSize(336, 541)

        # [1] 그리드 레이아웃 생성
        layout = QGridLayout(self)  # 위젯을 행과 열을 기준으로 배치하는 레이아웃이다.
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # [2] 결과 라벨
    ## 계산 결과를 표시하는 리절트 라벨
        self.result_label = QLabel(&quot;0&quot;)
    ## 계산기 화면 결과값 오른쪽 정렬 한다.(AlignRight, AlignVCenter)
        self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet(&quot;font-size: 36px; padding: 8px;&quot;)
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # [3] 1줄: %, CE, C, ⌫
    ## 열은 0열 부터 3열까지 총 4개를 사용한다.
        btn_percent = QPushButton(&quot;%&quot;)
        btn_ce = QPushButton(&quot;CE&quot;)
        btn_c = QPushButton(&quot;C&quot;)
        btn_del = QPushButton(&quot;⌫&quot;)

        for b in (btn_percent, btn_ce, btn_c, btn_del):
            b.setMinimumHeight(50)
    ## 반복 코드 (버튼을 하나씩 변수로 만들고 하나씩 배치함, 계속 반복해서 코드가 길어진 단점)
        layout.addWidget(btn_percent, 1, 0)
        layout.addWidget(btn_ce, 1, 1)
        layout.addWidget(btn_c, 1, 2)
        layout.addWidget(btn_del, 1, 3)

    ## 결과 라벨이 0행을 차지하므로 버튼 배치는 1행부터 배치한다.
        # [4] 2줄: 1/x, x&amp;sup2;, &amp;radic;x, &amp;divide;
        btn_inv = QPushButton(&quot;1/x&quot;)
        btn_sq = QPushButton(&quot;x&amp;sup2;&quot;)
        btn_sqrt = QPushButton(&quot;&amp;radic;x&quot;)
        btn_div = QPushButton(&quot;&amp;divide;&quot;)

        for b in (btn_inv, btn_sq, btn_sqrt, btn_div):
            b.setMinimumHeight(50)

        layout.addWidget(btn_inv, 2, 0)
        layout.addWidget(btn_sq, 2, 1)
        layout.addWidget(btn_sqrt, 2, 2)
        layout.addWidget(btn_div, 2, 3)

        # [5] 3줄: 7 8 9 &amp;times;
        btn_7 = QPushButton(&quot;7&quot;)
        btn_8 = QPushButton(&quot;8&quot;)
        btn_9 = QPushButton(&quot;9&quot;)
        btn_mul = QPushButton(&quot;&amp;times;&quot;)

        for b in (btn_7, btn_8, btn_9, btn_mul):
            b.setMinimumHeight(50)

        layout.addWidget(btn_7, 3, 0)
        layout.addWidget(btn_8, 3, 1)
        layout.addWidget(btn_9, 3, 2)
        layout.addWidget(btn_mul, 3, 3)

        # [6] 4줄: 4 5 6 &amp;minus;
        btn_4 = QPushButton(&quot;4&quot;)
        btn_5 = QPushButton(&quot;5&quot;)
        btn_6 = QPushButton(&quot;6&quot;)
        btn_sub = QPushButton(&quot;&amp;minus;&quot;)

        for b in (btn_4, btn_5, btn_6, btn_sub):
            b.setMinimumHeight(50)

        layout.addWidget(btn_4, 4, 0)
        layout.addWidget(btn_5, 4, 1)
        layout.addWidget(btn_6, 4, 2)
        layout.addWidget(btn_sub, 4, 3)

        # [7] 5줄: 1 2 3 +
        btn_1 = QPushButton(&quot;1&quot;)
        btn_2 = QPushButton(&quot;2&quot;)
        btn_3 = QPushButton(&quot;3&quot;)
        btn_add = QPushButton(&quot;+&quot;)

        for b in (btn_1, btn_2, btn_3, btn_add):
            b.setMinimumHeight(50)

        layout.addWidget(btn_1, 5, 0)
        layout.addWidget(btn_2, 5, 1)
        layout.addWidget(btn_3, 5, 2)
        layout.addWidget(btn_add, 5, 3)

        # [8] 6줄: +/- 0 . =
        btn_neg = QPushButton(&quot;+/-&quot;)
        btn_0 = QPushButton(&quot;0&quot;)
        btn_point = QPushButton(&quot;.&quot;)
        btn_eq = QPushButton(&quot;=&quot;)

        for b in (btn_neg, btn_0, btn_point, btn_eq):
            b.setMinimumHeight(50)

        layout.addWidget(btn_neg, 6, 0)
        layout.addWidget(btn_0, 6, 1)
        layout.addWidget(btn_point, 6, 2)
        layout.addWidget(btn_eq, 6, 3)

        # [9] 행 비율 설정
        layout.setRowStretch(0, 1)
        layout.setRowStretch(1, 2)
        layout.setRowStretch(2, 2)
        layout.setRowStretch(3, 2)
        layout.setRowStretch(4, 2)
        layout.setRowStretch(5, 2)
        layout.setRowStretch(6, 2)


if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = CalculatorWindow()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-1) &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 계산기 수정된 사항 : 앞에서 길게 반복해서 사용하던 코드를 간결하게 사용하였음을 확인함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;nbsp; 계산기 만들기 코드 : 개선된 코드는 클린해진다. 클레스 상속을 여러번 받아서 사용하던 것을 2차원 리스트로 하여 코드가 간결하게 작성된다.&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;수정된주석 계산기.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;993&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVRRnh/dJMcabdfqtI/2E7H4E1DzAoolvEKkdfMN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVRRnh/dJMcabdfqtI/2E7H4E1DzAoolvEKkdfMN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVRRnh/dJMcabdfqtI/2E7H4E1DzAoolvEKkdfMN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVRRnh%2FdJMcabdfqtI%2F2E7H4E1DzAoolvEKkdfMN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;993&quot; data-filename=&quot;수정된주석 계산기.png&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;993&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-2)&amp;nbsp; 계산기 레이아웃 버튼 설명 (주석)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;계산기 버튼 설명.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;955&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A2HGD/dJMcaayIs65/NjztuUIMD6eNXYdgrNYN70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A2HGD/dJMcaayIs65/NjztuUIMD6eNXYdgrNYN70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A2HGD/dJMcaayIs65/NjztuUIMD6eNXYdgrNYN70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA2HGD%2FdJMcaayIs65%2FNjztuUIMD6eNXYdgrNYN70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1089&quot; height=&quot;955&quot; data-filename=&quot;계산기 버튼 설명.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;955&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779967113373&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle(&quot;계산기 레이아웃 데모&quot;)
        self.setMinimumSize(336, 541)

        # [1] QGridLayout 생성
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # [2] 결과 표시 라벨
        self.result_label = QLabel(&quot;0&quot;)
        self.result_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        # self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet(&quot;font-size: 36px; padding: 8px;&quot;)
        layout.addWidget(self.result_label, 0, 0, 1, 4)

### 개선된 코드는 클린하다. 클래스 상속을 여러번 받아서 사용하던 것을 버튼 2차원 리스트로 하여 코드가 간결해진다.
        # [3] 계산기 버튼 텍스트를 2차원 리스트로 정의
        buttons = [
            [&quot;%&quot;, &quot;CE&quot;, &quot;C&quot;, &quot;⌫&quot;],
            [&quot;1/x&quot;, &quot;x&amp;sup2;&quot;, &quot;&amp;radic;x&quot;, &quot;&amp;divide;&quot;],
            [&quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;&amp;times;&quot;],
            [&quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;&amp;minus;&quot;],
            [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;+&quot;],
            [&quot;+/-&quot;, &quot;0&quot;, &quot;.&quot;, &quot;=&quot;],
        ]

        # [4] 버튼 생성 및 그리드에 배치
        row_offset = 1
### 버튼 반복문 안에서 만들때 개별 변수명을 안만들고 대신 딕셔너리에 저장해서 나중에 특정 버튼을 찾는다
        self.button_map = {}

        for row_index, row in enumerate(buttons):
            for col_index, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
#### 인덱스 번호도 함께 얻는다 (row_index는 버튼 줄번호), (col_index는 열 번호)
                grid_row = row_offset + row_index
                grid_col = col_index
                layout.addWidget(btn, grid_row, grid_col)

                self.button_map[text] = btn

        # [5] 특정 버튼에 스타일 지정
        if &quot;=&quot; in self.button_map:
            self.button_map[&quot;=&quot;].setStyleSheet(&quot;font-weight: bold; font-size: 18px;&quot;)

        # [6] 행 스트레치 설정
        layout.setRowStretch(0, 1)
        for r in range(1, 7):
            layout.setRowStretch(r, 2)


if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = CalculatorLayoutDemo()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-3) 계산기 덧셈만 가능한 레이아웃&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;덧셈만계산기.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;955&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnxkas/dJMcajbengz/T2lo99Q6k0tk8NU3gLQKO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnxkas/dJMcajbengz/T2lo99Q6k0tk8NU3gLQKO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnxkas/dJMcajbengz/T2lo99Q6k0tk8NU3gLQKO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnxkas%2FdJMcajbengz%2FT2lo99Q6k0tk8NU3gLQKO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1089&quot; height=&quot;955&quot; data-filename=&quot;덧셈만계산기.png&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;955&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779967295592&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 덧셈 기능 전체 코드
### 그런데 덧셈기능만 구현되는 계산기는 c버튼 클릭시 초기화가 되지 않는 문제가 있다.
import sys
from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QPushButton, QGridLayout
)
from PySide6.QtCore import Qt


class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle(&quot;계산기 (덧셈 기능만)&quot;)
        self.setMinimumSize(336, 541)

        # ======== 계산 상태 변수 ========
        self.current_input = &quot;0&quot;  # 화면에 표시되는 현재 입력
        self.saved_value = None  # 이전 값
        self.is_add_mode = False  # + 버튼이 눌러진 상태 여부

        # ======== [1] 레이아웃 ========
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # ======== [2] 결과 표시 ========
        self.result_label = QLabel(&quot;0&quot;)
        self.result_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.result_label.setStyleSheet(&quot;font-size: 36px; padding: 8px;&quot;)
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # ======== [3] 버튼 행렬 ========
        buttons = [
            [&quot;%&quot;, &quot;CE&quot;, &quot;C&quot;, &quot;⌫&quot;],
            [&quot;1/x&quot;, &quot;x&amp;sup2;&quot;, &quot;&amp;radic;x&quot;, &quot;&amp;divide;&quot;],
            [&quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;&amp;times;&quot;],
            [&quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;&amp;minus;&quot;],
            [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;+&quot;],
            [&quot;+/-&quot;, &quot;0&quot;, &quot;.&quot;, &quot;=&quot;],
        ]

        self.button_map = {}
        row_offset = 1

        for r, row in enumerate(buttons):
            for c, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
                layout.addWidget(btn, row_offset + r, c)
                self.button_map[text] = btn

        # ======== 버튼 기능 연결 ========
        # 숫자 버튼
        for num in [&quot;0&quot;, &quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;, &quot;9&quot;]:
            self.button_map[num].clicked.connect(self.on_number_clicked)

        # 덧셈
        self.button_map[&quot;+&quot;].clicked.connect(self.on_plus_clicked)

        # =
        self.button_map[&quot;=&quot;].clicked.connect(self.on_equal_clicked)

        # 초기값 표시
        self.update_display()

    # 숫자 입력 처리
    def on_number_clicked(self):
        text = self.sender().text()

        if self.current_input == &quot;0&quot;:
            self.current_input = text
        else:
            self.current_input += text

        self.update_display()

    # + 동작
    def on_plus_clicked(self):
        self.saved_value = float(self.current_input)
        self.current_input = &quot;0&quot;
        self.is_add_mode = True
        self.update_display()

    # = 동작
    def on_equal_clicked(self):
        if self.is_add_mode and self.saved_value is not None:
            result = self.saved_value + float(self.current_input)
            self.current_input = str(result)

        self.is_add_mode = False
        self.saved_value = None
        self.update_display()

    # 화면 업데이트
    def update_display(self):
        self.result_label.setText(self.current_input)


if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = CalculatorLayoutDemo()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-4) 계산기 사칙연산이 가능하게 레이아웃 수정하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;수정된 사칙연산 계산기.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;913&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjgpQB/dJMcaak8agV/hk7Ybrnp8FdTNbA0LJG3pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjgpQB/dJMcaak8agV/hk7Ybrnp8FdTNbA0LJG3pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjgpQB/dJMcaak8agV/hk7Ybrnp8FdTNbA0LJG3pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjgpQB%2FdJMcaak8agV%2Fhk7Ybrnp8FdTNbA0LJG3pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;913&quot; data-filename=&quot;수정된 사칙연산 계산기.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;913&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779967393904&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#### [사칙연산]이 가능한 구조개선 과제 (덧셈과 여러연산자 상태변수 일반화 하기)
#### 덧셈기능만 구현되는 계산기는 초기화 기능 및 사칙연산 계산 기능을 추가하여 문제를 해결함
import sys
from PySide6.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QPushButton
from PySide6.QtCore import Qt

class CalculatorLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
### 타이틀 제목을 수정했다. 사칙연산 구조 개선
        self.setWindowTitle(&quot;계산기(사칙연산 구조개선)&quot;)
        self.setMinimumSize(336, 541)

        # ======== 데이터 초기화 ========
        self.current_input = &quot;0&quot;      # 현재 입력 중인 숫자
        self.saved_value = None       # 저장된 이전 값
        self.current_operator = None  # 현재 선택된 연산자 (+, -, *, /)

        # ======== [1] 레이아웃 ========
        layout = QGridLayout(self)
        layout.setSpacing(4)
        layout.setContentsMargins(4, 4, 4, 4)

        # ======== [2] 결과 창 ========
        self.result_label = QLabel(&quot;0&quot;)
        # PySide6에서는 Qt.AlignmentFlag 형식을 권장합니다.
        self.result_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.result_label.setStyleSheet(&quot;font-size: 36px; padding: 8px;&quot;)
        layout.addWidget(self.result_label, 0, 0, 1, 4)

        # ======== [3] 버튼 생성 ========
        buttons = [
            [&quot;%&quot;, &quot;CE&quot;, &quot;C&quot;, &quot;back&quot;],
            [&quot;1/x&quot;, &quot;x&amp;sup2;&quot;, &quot;&amp;radic;x&quot;, &quot;/&quot;],
            [&quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;*&quot;],
            [&quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;-&quot;],
            [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;+&quot;],
            [&quot;+/-&quot;, &quot;0&quot;, &quot;.&quot;, &quot;=&quot;],
        ]

        self.button_map = {}
        row_offset = 1

        for r, row in enumerate(buttons):
            for c, text in enumerate(row):
                btn = QPushButton(text)
                btn.setMinimumHeight(50)
                layout.addWidget(btn, row_offset + r, c)
                self.button_map[text] = btn

        # ======== 이벤트 연결 ========
        # 숫자 및 소수점 버튼
        for num in [&quot;0&quot;, &quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;.&quot;]:
            self.button_map[num].clicked.connect(self.on_number_clicked)

        # 사칙연산 버튼 (+, -, *, /)
### 사칙연산이 가능하도록 버튼을 눌렀을 때 값이 계산되도록 한다
        for op in [&quot;+&quot;, &quot;-&quot;, &quot;*&quot;, &quot;/&quot;]:
            self.button_map[op].clicked.connect(self.on_operator_clicked)
### 기능버튼을 사용하여 값을 계산하도록 한다.
        # 기능 버튼
        self.button_map[&quot;=&quot;].clicked.connect(self.on_equal_clicked)
        self.button_map[&quot;C&quot;].clicked.connect(self.on_clear_clicked)

### 화면 업데이트
        self.update_display()

    # 숫자 버튼 클릭 이벤트
    def on_number_clicked(self):
        text = self.sender().text()

        if text == &quot;.&quot;:
            if &quot;.&quot; in self.current_input:
                return  # 이미 소수점이 있으면 무시
            self.current_input += text
        elif self.current_input == &quot;0&quot;:
            self.current_input = text
        else:
            self.current_input += text

        self.update_display()

    # 연산자 버튼 클릭 이벤트 (+, -, *, /)
    def on_operator_clicked(self):
        op = self.sender().text()
        self.saved_value = float(self.current_input)
        self.current_operator = op
        self.current_input = &quot;0&quot;
        self.update_display()

    # = 버튼 클릭 이벤트 (결과 계산)
    def on_equal_clicked(self):
        if self.current_operator is None or self.saved_value is None:
            return

        current_val = float(self.current_input)
        result = 0

        # 연산자별 계산
        if self.current_operator == &quot;+&quot;:
            result = self.saved_value + current_val
        elif self.current_operator == &quot;-&quot;:
            result = self.saved_value - current_val
        elif self.current_operator == &quot;*&quot;:
            result = self.saved_value * current_val
        elif self.current_operator == &quot;/&quot;:
            if current_val == 0:
                self.current_input = &quot;Error (0으로 나눌 수 없음)&quot;
                self.update_display()
                self.saved_value = None
                self.current_operator = None
                return
            result = self.saved_value / current_val

        # 소수점 아래가 0이면 정수로 변환, 아니면 실수 그대로 표시
        if result.is_integer():
            self.current_input = str(int(result))
        else:
            self.current_input = str(result)

        self.current_operator = None
        self.saved_value = None
        self.update_display()

    # C 버튼 클릭 이벤트 (초기화)
    def on_clear_clicked(self):
        self.current_input = &quot;0&quot;
        self.saved_value = None
        self.current_operator = None
        self.update_display()

    # 디스플레이 갱신
    def update_display(self):
        self.result_label.setText(self.current_input)


if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = CalculatorLayoutDemo()
    w.show()
    sys.exit(app.exec())  # PySide6에서는 exec_() 대신 exec()을 사용합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-5)&lt;span&gt; QTab Widget&amp;nbsp; 버튼틀, 정보입력창 만들기&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;위젯탭1.png&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kn2pZ/dJMcac4jXu5/pIsDtNB5YUydhKaPrTaKGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kn2pZ/dJMcac4jXu5/pIsDtNB5YUydhKaPrTaKGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kn2pZ/dJMcac4jXu5/pIsDtNB5YUydhKaPrTaKGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKn2pZ%2FdJMcac4jXu5%2FpIsDtNB5YUydhKaPrTaKGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;784&quot; data-filename=&quot;위젯탭1.png&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py&lt;/p&gt;
&lt;pre id=&quot;code_1779968559888&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;pre id=&quot;code_1779967587906&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle(&quot;QTabWidget 데모&quot;)

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel(&quot;이름 :&quot;)
        line_edit_full_name = QLineEdit()
        line_edit_full_name.setPlaceholderText(&quot;이름을 입력하세요&quot;)

        form_layout = QHBoxLayout()
        form_layout.addWidget(label_full_name)
        form_layout.addWidget(line_edit_full_name)
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton(&quot;버튼 1&quot;)
        button_2 = QPushButton(&quot;버튼 2&quot;)
        button_3 = QPushButton(&quot;버튼 3&quot;)

        # 버튼 1 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_2)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_3)  # 버튼 크기 결정 값
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
    ### 처음의 위젯 버튼에 &quot;정보입력&quot; 버튼이 있고, 추가로 두번째 클릭 되는 '버튼틀'을 추가한다.
        tab_widget.addTab(widget_form, &quot;정보 입력&quot;)
        tab_widget.addTab(widget_buttons, &quot;버튼들&quot;)

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def button_1_clicked(self):
        print(&quot;버튼 1이 클릭되었습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예시 1-6) &lt;b&gt;&lt;span&gt;QTab Widget&amp;nbsp; 버튼틀 누르기&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 버튼 1동작, 버튼틀 추가&amp;nbsp; 수정하기&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;텝레이아웃2.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjAwaR/dJMcadvkQIV/qyx4GtwyysQkS2sjKS8EM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjAwaR/dJMcadvkQIV/qyx4GtwyysQkS2sjKS8EM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjAwaR/dJMcadvkQIV/qyx4GtwyysQkS2sjKS8EM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjAwaR%2FdJMcadvkQIV%2Fqyx4GtwyysQkS2sjKS8EM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;842&quot; data-filename=&quot;텝레이아웃2.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py&lt;/p&gt;
&lt;pre id=&quot;code_1779968711881&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;figure style=&quot;color: #333333; text-align: center;&quot; data-ke-style=&quot;alignCenter&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-ke-type=&quot;image&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre id=&quot;code_1779968657010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle(&quot;QTabWidget 데모&quot;)

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel(&quot;이름 :&quot;)
        line_edit_full_name = QLineEdit()
        line_edit_full_name.setPlaceholderText(&quot;이름을 입력하세요&quot;)

        form_layout = QHBoxLayout()
        form_layout.addWidget(label_full_name)
        form_layout.addWidget(line_edit_full_name)
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton(&quot;버튼 1&quot;)
        button_2 = QPushButton(&quot;버튼 2&quot;)
        button_3 = QPushButton(&quot;버튼 3&quot;)

        # 버튼 1 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_2)  # 버튼 크기 결정 값
        buttons_layout.addWidget(button_3)  # 버튼 크기 결정 값
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
    ### 처음의 위젯 버튼에 &quot;정보입력&quot; 버튼이 있고, 추가로 두번째 클릭 되는 '버튼틀'을 추가한다.
        tab_widget.addTab(widget_form, &quot;정보 입력&quot;)
        tab_widget.addTab(widget_buttons, &quot;버튼들&quot;)

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def button_1_clicked(self):
        print(&quot;버튼 1이 클릭되었습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예시 ) &lt;b&gt;&lt;b&gt;&lt;span&gt;QTab Widget 정보 입력하고 이름 확인 클릭시 출력이 된다.&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;정보입력탭 버튼클릭 출력.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC0S5Z/dJMcagFIC29/kMN01PX5o7CJkK2M9LUsN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC0S5Z/dJMcagFIC29/kMN01PX5o7CJkK2M9LUsN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC0S5Z/dJMcagFIC29/kMN01PX5o7CJkK2M9LUsN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC0S5Z%2FdJMcagFIC29%2FkMN01PX5o7CJkK2M9LUsN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1039&quot; height=&quot;880&quot; data-filename=&quot;정보입력탭 버튼클릭 출력.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt;main.py&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779968823898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication
from widget import Widget
import sys

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span&gt;widget.py&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779968855923&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle(&quot;QTabWidget 데모&quot;)

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel(&quot;이름 :&quot;)

        # &amp;lt;추가 실습 과제&amp;gt; 인스턴스 변수로 변경
        self.line_edit_full_name = QLineEdit()
        self.line_edit_full_name.setPlaceholderText(&quot;이름을 입력하세요&quot;)

        # &amp;lt;추가 실습 과제&amp;gt; 이름 확인 버튼 추가
        info_button = QPushButton(&quot;이름 확인&quot;)
        info_button.clicked.connect(self.info_button_clicked)

        # 기존: HBoxLayout &amp;rarr; 수정: 이름 입력 + 버튼 수직 배치
        row_layout = QHBoxLayout()
        row_layout.addWidget(label_full_name)
        row_layout.addWidget(self.line_edit_full_name)

        form_layout = QVBoxLayout()   # &amp;lt;추가 실습 과제&amp;gt; 탭 구성 변경
        form_layout.addLayout(row_layout)
        form_layout.addWidget(info_button)  # &amp;lt;추가 실습 과제&amp;gt;
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton(&quot;버튼 1&quot;)
        button_2 = QPushButton(&quot;버튼 2&quot;)
        button_3 = QPushButton(&quot;버튼 3&quot;)

        # 버튼 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)
        button_2.clicked.connect(self.button_2_clicked)
        button_3.clicked.connect(self.button_3_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)
        buttons_layout.addWidget(button_2)
        buttons_layout.addWidget(button_3)
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
        tab_widget.addTab(widget_form, &quot;정보 입력&quot;)
        tab_widget.addTab(widget_buttons, &quot;버튼들&quot;)

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def info_button_clicked(self):
        # &amp;lt;추가 실습 과제&amp;gt; 이름 입력 여부 확인 후 출력
        name = self.line_edit_full_name.text().strip()
#### 탭에서 이름을 입력한 후 &quot;이름확인&quot;을 클릭하면 클릭했다는 메세지가 출력된다.
        if not name:
            print(&quot;이름을 먼저 입력해 주세요.&quot;)
        else:
            print(f&quot;{name}님, info 출력 클릭하셨습니다.&quot;)

    def button_1_clicked(self):
        print(&quot;버튼 2가 클릭되었습니다.&quot;)

    def button_2_clicked(self):
        print(&quot;버튼 2가 클릭되었습니다.&quot;)

    def button_3_clicked(self):
        print(&quot;버튼 3이 클릭되었습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widget 레이아웃 버튼을 눌러서 출력결과 확인 하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;탭레이아웃 버튼 클릭 출력.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YtM2j/dJMb997B9Jx/hWdnGkdu32kEryIR0ja1A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YtM2j/dJMb997B9Jx/hWdnGkdu32kEryIR0ja1A1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YtM2j/dJMb997B9Jx/hWdnGkdu32kEryIR0ja1A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYtM2j%2FdJMb997B9Jx%2FhWdnGkdu32kEryIR0ja1A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1039&quot; height=&quot;880&quot; data-filename=&quot;탭레이아웃 버튼 클릭 출력.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widget 버튼 클릭시 클릭되었다는 메세지 출력 확인&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;버튼레이아웃 클릭메지 출력.png&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/92PEM/dJMcadvkQRH/HXkw5sndqiv7OGer3KNOF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/92PEM/dJMcadvkQRH/HXkw5sndqiv7OGer3KNOF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/92PEM/dJMcadvkQRH/HXkw5sndqiv7OGer3KNOF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F92PEM%2FdJMcadvkQRH%2FHXkw5sndqiv7OGer3KNOF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;958&quot; height=&quot;902&quot; data-filename=&quot;버튼레이아웃 클릭메지 출력.png&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py&lt;/p&gt;
&lt;pre id=&quot;code_1779968928676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;pre id=&quot;code_1779968957523&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout,
    QTabWidget, QPushButton, QLabel, QLineEdit
)

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 윈도우 제목
        self.setWindowTitle(&quot;QTabWidget 데모&quot;)

        # [1] 탭 위젯 생성
        tab_widget = QTabWidget(self)

        # &amp;lt;추가 실습 과제3&amp;gt; 탭 클릭 시 시그널 연결
        tab_widget.currentChanged.connect(self.tab_changed)

        # [2] 첫 번째 탭 : 정보 입력 탭
        widget_form = QWidget()
        label_full_name = QLabel(&quot;이름 :&quot;)

        # &amp;lt;추가 실습 과제&amp;gt; 인스턴스 변수로 변경
        self.line_edit_full_name = QLineEdit()
        self.line_edit_full_name.setPlaceholderText(&quot;이름을 입력하세요&quot;)

        # &amp;lt;추가 실습 과제&amp;gt; 이름 확인 버튼 추가
        info_button = QPushButton(&quot;이름 확인&quot;)
        info_button.clicked.connect(self.info_button_clicked)

        # 기존: HBoxLayout &amp;rarr; 수정: 이름 입력 + 버튼 수직 배치
        row_layout = QHBoxLayout()
        row_layout.addWidget(label_full_name)
        row_layout.addWidget(self.line_edit_full_name)

        form_layout = QVBoxLayout()   # &amp;lt;추가 실습 과제&amp;gt; 탭 구성 변경
        form_layout.addLayout(row_layout)
        form_layout.addWidget(info_button)  # &amp;lt;추가 실습 과제&amp;gt;
        widget_form.setLayout(form_layout)

        # [3] 두 번째 탭 : 버튼 탭
        widget_buttons = QWidget()
        button_1 = QPushButton(&quot;버튼 1&quot;)
        button_2 = QPushButton(&quot;버튼 2&quot;)
        button_3 = QPushButton(&quot;버튼 3&quot;)

        # 버튼 클릭 시 슬롯 함수 호출
        button_1.clicked.connect(self.button_1_clicked)
        button_2.clicked.connect(self.button_2_clicked)
        button_3.clicked.connect(self.button_3_clicked)

        buttons_layout = QVBoxLayout()
        buttons_layout.addWidget(button_1)
        buttons_layout.addWidget(button_2)
        buttons_layout.addWidget(button_3)
        widget_buttons.setLayout(buttons_layout)

        # [4] 탭 위젯에 탭 추가
        tab_widget.addTab(widget_form, &quot;정보 입력&quot;)
        tab_widget.addTab(widget_buttons, &quot;버튼들&quot;)

        # [5] 위젯 전체 레이아웃 설정
        layout = QVBoxLayout()
        layout.addWidget(tab_widget)
        self.setLayout(layout)

    # [6] 슬롯 함수 : 버튼 1 클릭 시 동작
    def info_button_clicked(self):
        # &amp;lt;추가 실습 과제&amp;gt; 이름 입력 여부 확인 후 출력
        name = self.line_edit_full_name.text().strip()

        if not name:
            print(&quot;이름을 먼저 입력해 주세요.&quot;)
        else:
            print(f&quot;{name}님, info 출력 클릭하셨습니다.&quot;)

    def button_1_clicked(self):
        print(&quot;버튼 2가 클릭되었습니다.&quot;)

    def button_2_clicked(self):
        print(&quot;버튼 2가 클릭되었습니다.&quot;)

    def button_3_clicked(self):
        print(&quot;버튼 3이 클릭되었습니다.&quot;)

### 웹 하나의 창에서 Layout의 버튼 중복 배치를 통해 탭을 클릭하면 동작이 출력하는 코드이다.

    # &amp;lt;추가 실습 과제3&amp;gt; 탭을 클릭하면 탭 이름 출력
    def tab_changed(self, index):
        tab_widget = self.findChild(QTabWidget)  # 현재 탭 위젯 찾기
        tab_text = tab_widget.tabText(index)     # 클릭한 탭의 이름 가져오기
        print(f&quot;'{tab_text}' 탭을 클릭했습니다.&quot;)

        # &amp;lt;추가 실습 과제4&amp;gt; 첫 번째 탭에서 두 번째 탭으로 이동하는 슬롯
        def go_to_second_tab(self):
            # 현재 위젯(self) 안에서 QTabWidget 타입의 자식 위젯을 찾아온다.
            tab_widget = self.findChild(QTabWidget)

            # 탭 위젯을 정상적으로 찾았는지 확인한다.
            if tab_widget is not None:
                # 인덱스 1번 탭으로 이동한다.
                # 0 &amp;rarr; 첫 번째 탭: &quot;정보 입력&quot;
                # 1 &amp;rarr; 두 번째 탭: &quot;버튼들&quot;
                tab_widget.setCurrentIndex(1)

#### 탭에서 이동할 때 슬롯 함수를 사용해 버튼 누름이 첫번째 탭으로 이동하는 것을 구현해 본다.
#### 처음 실행 화면부터 정보입력 창이 실행되는 메세지가 출력된다. 이후 클릭 순서에 따라 메세지가 출력된다
        # &amp;lt;추가 실습 과제4&amp;gt; 두 번째 탭에서 첫 번째 탭으로 이동하는 슬롯
        def go_to_first_tab(self):
            # 현재 위젯(self) 안에서 QTabWidget 타입의 자식 위젯을 찾아온다.
            tab_widget = self.findChild(QTabWidget)

            # 탭 위젯을 정상적으로 찾았는지 확인한다.
            if tab_widget is not None:
                # 인덱스 0번 탭으로 이동한다.
                # 0 &amp;rarr; 첫 번째 탭: &quot;정보 입력&quot;
                # 1 &amp;rarr; 두 번째 탭: &quot;버튼들&quot;
                tab_widget.setCurrentIndex(0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge 레이아웃 첫화면 탭&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;탭레이아웃 첫화면탭.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2nCB0/dJMcahYRS7q/f5FYuPkXI0YS6k0LKbPGF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2nCB0/dJMcahYRS7q/f5FYuPkXI0YS6k0LKbPGF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2nCB0/dJMcahYRS7q/f5FYuPkXI0YS6k0LKbPGF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2nCB0%2FdJMcahYRS7q%2Ff5FYuPkXI0YS6k0LKbPGF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;868&quot; height=&quot;788&quot; data-filename=&quot;탭레이아웃 첫화면탭.png&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge 스크롤 레이아웃 항목 추가 하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크롤레이아웃.png&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1KlOc/dJMcahYRS9w/AENddYDzblCY9HDGTkgyz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1KlOc/dJMcahYRS9w/AENddYDzblCY9HDGTkgyz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1KlOc/dJMcahYRS9w/AENddYDzblCY9HDGTkgyz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1KlOc%2FdJMcahYRS9w%2FAENddYDzblCY9HDGTkgyz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1004&quot; height=&quot;966&quot; data-filename=&quot;스크롤레이아웃.png&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;main.py&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779969155773&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 위젯파일과 메인파일로 분리해서 메인파일을 실행시키면 엡창이 실행된다.
#### tab_scroll_area.py 에서 메인에 분리할 참조 내용들 ####
#from PySide6.QtWidgets import (
#    QWidget, QVBoxLayout, QScrollArea,
#    QLabel, QPushButton
#)
#from PySide6.QtCore import Qt
#class ScrollAreaTab(QWidget):
#    def __init__(self, parent=None):
#        super().__init__(parent)

#### Widget에서 분리 생성된 main.py 내용들 ####
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import ScrollAreaTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Scroll Area Tab Example&quot;)
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = ScrollAreaTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == &quot;__main__&quot;:   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;pre id=&quot;code_1779969207173&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_scroll_area.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QScrollArea,
    QLabel, QPushButton
)
from PySide6.QtCore import Qt


class ScrollAreaTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.add_button = QPushButton(&quot;항목 추가&quot;)
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

# QScrollArea 사용하기
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

 # 스크롤 안에서 실제 내용은 별도의 QWidget + QVBoxLayout  실행한다. 내용이 많으면 자동스크롤바 생성
        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        self.item_count = 0
        for i in range(5):
            self.add_label(f&quot;초기 항목 {i + 1}&quot;)
        self.item_count = 5

    def add_label(self, text: str):
        label = QLabel(text)
        self.content_layout.addWidget(label)

    def add_item(self):
        self.item_count += 1
        new_text = f&quot;추가된 항목 {self.item_count}&quot;
        self.add_label(new_text)
        print(new_text)
# 항목 추가시 자동으로 맨 아래로 스크롤바 생성
        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge&amp;nbsp; 스크롤 바 생성하기&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크롤바 생성.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;951&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H4ElW/dJMcagMt6Y0/2Yui2AOa4BBW5B0RAZ3RJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H4ElW/dJMcagMt6Y0/2Yui2AOa4BBW5B0RAZ3RJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H4ElW/dJMcagMt6Y0/2Yui2AOa4BBW5B0RAZ3RJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH4ElW%2FdJMcagMt6Y0%2F2Yui2AOa4BBW5B0RAZ3RJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;982&quot; height=&quot;951&quot; data-filename=&quot;스크롤바 생성.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;951&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge 스크롤 로그 추가 하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크롤 로그 추가.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;951&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b14EAw/dJMcagFIDbC/u7rqCF07SPV7KqqcUDkTkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b14EAw/dJMcagFIDbC/u7rqCF07SPV7KqqcUDkTkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b14EAw/dJMcagFIDbC/u7rqCF07SPV7KqqcUDkTkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb14EAw%2FdJMcagFIDbC%2Fu7rqCF07SPV7KqqcUDkTkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;982&quot; height=&quot;951&quot; data-filename=&quot;스크롤 로그 추가.png&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;951&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py&lt;/p&gt;
&lt;pre id=&quot;code_1779969291623&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 스크롤을 사용자가 편집하지 못하게 전형적인 로그창 스타일 실습하기
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import LogTextEditTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;LogTextEditTab&quot;)
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = LogTextEditTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == &quot;__main__&quot;:   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;pre id=&quot;code_1779969317055&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_log_textedit.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor


class LogTextEditTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton(&quot;로그 추가&quot;)
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f&quot;[로그] {self.log_count}번째 메시지입니다.&quot;
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge&amp;nbsp; 스크롤 통합 (스크롤 영역)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크롤 통합1.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oqyf5/dJMcaiDumVj/1eFDaXCUQjFSKmJvhnts81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oqyf5/dJMcaiDumVj/1eFDaXCUQjFSKmJvhnts81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oqyf5/dJMcaiDumVj/1eFDaXCUQjFSKmJvhnts81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOqyf5%2FdJMcaiDumVj%2F1eFDaXCUQjFSKmJvhnts81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1044&quot; height=&quot;946&quot; data-filename=&quot;스크롤 통합1.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge 스크롤 통합 리스트 작성 (리스트 스크롤)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크로롤통합리스트.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhqUHe/dJMcagZV3bU/5IMhUk9DE2ZwB5jkkM6qSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhqUHe/dJMcagZV3bU/5IMhUk9DE2ZwB5jkkM6qSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhqUHe/dJMcagZV3bU/5IMhUk9DE2ZwB5jkkM6qSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhqUHe%2FdJMcagZV3bU%2F5IMhUk9DE2ZwB5jkkM6qSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1044&quot; height=&quot;946&quot; data-filename=&quot;스크로롤통합리스트.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge 스크롤 통합 로그 실습 (로그 스크롤)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;통합스크롤 로그.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k0jYK/dJMcafz0YcV/ZKaGC26MuO2F76o40kgWiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k0jYK/dJMcafz0YcV/ZKaGC26MuO2F76o40kgWiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k0jYK/dJMcafz0YcV/ZKaGC26MuO2F76o40kgWiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk0jYK%2FdJMcafz0YcV%2FZKaGC26MuO2F76o40kgWiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1044&quot; height=&quot;946&quot; data-filename=&quot;통합스크롤 로그.png&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;QTab Widge&amp;nbsp; 스크롤 통합 로그&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크롤 통합 메인파이.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFDnY/dJMcaipXgEH/5E1Nv5ukcIOsFlNuNDC0r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFDnY/dJMcaipXgEH/5E1Nv5ukcIOsFlNuNDC0r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFDnY/dJMcaipXgEH/5E1Nv5ukcIOsFlNuNDC0r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFDnY%2FdJMcaipXgEH%2F5E1Nv5ukcIOsFlNuNDC0r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1038&quot; height=&quot;791&quot; data-filename=&quot;스크롤 통합 메인파이.png&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.py&lt;/p&gt;
&lt;pre id=&quot;code_1779969464377&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 스크롤을 사용자가 편집하지 못하게 전형적인 로그창 스타일 실습하기
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

# widget.py 파일에서 class 부분 ScrollAreaTab 클래스를 가져오기
from widget import LogTextEditTab

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;LogTextEditTab&quot;)
        self.resize(400, 500)

        # 위젯 생성 후 메인 윈도우의 중앙 위젯으로 설정
        self.tab_widget = LogTextEditTab()
        self.setCentralWidget(self.tab_widget)

if __name__ == &quot;__main__&quot;:   # 스크립트가 직접 실행될 때만 코드 실행하기
    app = QApplication(sys.argv)   # 애플리케이션 객체를 생성, 명령행 인수를 전달해 Qt 환경을 초기화 함
    w = MainWindow()  # 위젯파이에서 클래스 위젯을 가리키며, 위젯 객체(윈도우 창)를 생성 한다.
    w.show()          # 생성한 위젯 창을 화면에 표시 한다.
    sys.exit(app.exec())   # Qt의 메인 이벤트 루프를 시작, 프로그램 종료시 안전하게 리소스를 해제한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;widget.py&lt;/p&gt;
&lt;pre id=&quot;code_1779969490632&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_log_textedit.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor


class LogTextEditTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton(&quot;로그 추가&quot;)
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f&quot;[로그] {self.log_count}번째 메시지입니다.&quot;
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(예시 )&lt;span&gt; Layout Widget 위에서 실습한 파일들 분리하기&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 소스코드를 분리하는 핵심 이유&lt;/b&gt;&lt;br /&gt;- 가독성 향상 : 한 파일이 수천 줄이 되는 것을 방지하여 코드를 쉽게 읽을 수 있다.&lt;br /&gt;- 유지보수 용이 :&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;u&gt;버튼 기능을 수정할 때 buttons_tab.py만 열어서 고치면 되므로&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다른 코드를 건드릴 위험이 줄어든다.&lt;br /&gt;- 코드 재사용 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&lt;u&gt;form_tab.py에서 만든 입력 양식을 다른 화면이나 다른 프로젝트에서 그대로 가져다 쓸 수&lt;/u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;있다.&lt;br /&gt;- 협업 수월 : 여러 개발자가 동시에 작업할 때, 파일이 분리되어 있으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;깃(Git) 충돌&lt;/u&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;없이 각자 맡은 화면을 개발할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1779969777675&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# __init__.py &quot;&quot;&quot; Layout 복합적인 레이아웃 예제 학습 내용이다.&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779969815140&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py

import sys

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779969872061&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_list_widget.py 클래스에서 리스트 분리

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QListWidget
)


class ListWidgetTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.add_button = QPushButton(&quot;항목 추가&quot;)
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

        self.list_widget = QListWidget()
        layout.addWidget(self.list_widget)

        self.item_count = 0
        for i in range(10):
            self.list_widget.addItem(f&quot;초기 항목 {i + 1}&quot;)
        self.item_count = 10

    def add_item(self):
        self.item_count += 1
        text = f&quot;추가 항목 {self.item_count}&quot;
        self.list_widget.addItem(text)
        print(text)

        self.list_widget.scrollToBottom()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779969949276&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_log_textedit.py  클래스에서 로그 텍스트에디트 부분 분리

class LogTextEditTab(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton(&quot;로그 추가&quot;)
        self.log_button.clicked.connect(self.add_log)
        layout.addWidget(self.log_button)

        self.log_count = 0

    def add_log(self):
        self.log_count += 1
        message = f&quot;[로그] {self.log_count}번째 메시지입니다.&quot;
        self.log_textedit.append(message)
        print(message)

        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779970000716&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tab_scroll_area.py 클래스에서 스크롤 부분 분리

class ScrollAreaTab(QWidget):
    
   def __init__(self, parent=None):
        super().__init__(parent)

        layout = QVBoxLayout(self)

        self.add_button = QPushButton(&quot;항목 추가&quot;)
        self.add_button.clicked.connect(self.add_item)
        layout.addWidget(self.add_button)

        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        self.item_count = 0
        for i in range(5):
            self.add_label(f&quot;초기 항목 {i + 1}&quot;)
        self.item_count = 5

    def add_label(self, text: str):
        label = QLabel(text)
        self.content_layout.addWidget(label)

    def add_item(self):
        self.item_count += 1
        new_text = f&quot;추가된 항목 {self.item_count}&quot;
        self.add_label(new_text)
        print(new_text)

        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779970074774&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# widget.py 위젯 클래스(부모) 
# from PyQt6.QtWidgets import QWidget  # 또는 PySide6.QtWidgets

class Widget(QWidget):  # QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함

    def __init__(self, parent=None):  # 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)
    
       # 부모 클래스(QWidget)의 생성자를 호출하여 위젯을 올바르게 초기화하고 부모-자식 관계를 설정함
        super().__init__(parent)  


        self.setWindowTitle(&quot;스크롤 예제 통합 (QTabWidget)&quot;)
        self.resize(420, 340)

        layout = QVBoxLayout(self)

        self.tab_widget = QTabWidget()
        layout.addWidget(self.tab_widget)

        scroll_tab = ScrollAreaTab(self)
        list_tab = ListWidgetTab(self)
        log_tab = LogTextEditTab(self)

        self.tab_widget.addTab(scroll_tab, &quot;스크롤 영역&quot;)
        self.tab_widget.addTab(list_tab, &quot;리스트 스크롤&quot;)
        self.tab_widget.addTab(log_tab, &quot;로그 스크롤&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;※ widget.py&amp;nbsp;위젯&amp;nbsp;클래스(부모)&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt; &amp;nbsp;from PyQt6.QtWidgets import QWidget&amp;nbsp; &amp;nbsp;# 또는 PySide6.QtWidgets&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;class Widget(QWidget):&amp;nbsp; &amp;nbsp;# QWidget(기본 위젯 클래스)을 상속받아 새로운 위젯 클래스를 정의함&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;def __init__(self, parent=None):&amp;nbsp;&amp;nbsp;# 클래스가 생성될 때 자동으로 실행되는 초기화 메서드(생성자)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(parent)&amp;nbsp; &amp;nbsp;# 부모 클래스(QWidget)의 생성자를 호출해 위젯을 초기화 하고 부모-자식 관계를 설정함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;end.&lt;/span&gt;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/49</guid>
      <comments>https://goodjop79.tistory.com/49#entry49comment</comments>
      <pubDate>Thu, 28 May 2026 21:15:57 +0900</pubDate>
    </item>
    <item>
      <title>D+35 [Layout] 레이아웃 종합 예제 학습(1)</title>
      <link>https://goodjop79.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일자&amp;nbsp; : 2026년 5월 27일(수)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 내용 : Layout 가상환경에서 종합 예제 학습 하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 1)&amp;nbsp; 사이즈 정책&amp;nbsp; 실습&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ main.py 실행하면 윈도우위젯 화면에서 버튼이 출력되고, 텍스트를 입력할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;레이예1 스크린샷 2026-05-27 13-52-10.png&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Rd12/dJMcaf003d5/mR7C6XIfJFuodv8JUecNf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Rd12/dJMcaf003d5/mR7C6XIfJFuodv8JUecNf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Rd12/dJMcaf003d5/mR7C6XIfJFuodv8JUecNf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Rd12%2FdJMcaf003d5%2FmR7C6XIfJFuodv8JUecNf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1107&quot; height=&quot;377&quot; data-filename=&quot;레이예1 스크린샷 2026-05-27 13-52-10.png&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;377&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779857759443&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.py 실행부 파일이다.

from PySide6.QtWidgets import QApplication
from widget import SizePolicyExample
import sys

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    w = SizePolicyExample()
    w.show()
    sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779857866875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# widget.py 레이아웃 종합 예제 실습하기

from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QSizePolicy, QLineEdit, QPushButton

class SizePolicyExample(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle(&quot;사이즈 정책(SizePolicy) 및 스트레치 학습&quot;)

        # [1] SizePolicy 설정
        # SizePolicy는 부모 창의 크기가 변할 때 위젯의 크기를 어떻게 조절할 것인지 결정하는 정책입니다.
        label = QLabel(&quot;텍스트 입력: &quot;)
        line_edit = QLineEdit()

        # 가로 방향은 가능한 한 늘어나고(Expanding), 세로 방향은 고정(Fixed)
        line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        # [2] 첫 번째 가로 레이아웃 : 텍스트 + 입력창
        h_layout_1 = QHBoxLayout()
        h_layout_1.addWidget(label)
        h_layout_1.addWidget(line_edit)

        # [3] 버튼 3개 생성
        button_1 = QPushButton(&quot;버튼1&quot;)
        button_2 = QPushButton(&quot;버튼2&quot;)
        button_3 = QPushButton(&quot;버튼3&quot;)

        # [4] 스트레치 설정
        # 버튼1은 stretch=2 &amp;rarr; 2배 넓은 공간 차지
        # 버튼2, 버튼3은 stretch=1 &amp;rarr; 동일 비율로 차지
        h_layout_2 = QHBoxLayout()
        h_layout_2.addWidget(button_1, 2)
        h_layout_2.addWidget(button_2, 1)
        h_layout_2.addWidget(button_3, 1)

        # [5] 최종 전체 수직 레이아웃
        v_layout = QVBoxLayout()
        v_layout.addLayout(h_layout_1)
        v_layout.addLayout(h_layout_2)

        self.setLayout(v_layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2)&amp;nbsp; 창의 좌우 마우스로 늘려서 가장 많이 늘어나는 사이즈&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp; 버튼 1이 가장 크기가 크고 나머지는 동일하다. 값을 수정해 주면 된다. 1,1,1,&amp;nbsp; ----&amp;gt;&lt;span style=&quot;color: #ef5369;&quot;&gt; 2,1,1&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-27 14-10-29.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIoQbU/dJMb99Nf5XY/QiKlYEKpPYbogtuFduHd81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIoQbU/dJMb99Nf5XY/QiKlYEKpPYbogtuFduHd81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIoQbU/dJMb99Nf5XY/QiKlYEKpPYbogtuFduHd81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIoQbU%2FdJMb99Nf5XY%2FQiKlYEKpPYbogtuFduHd81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1095&quot; height=&quot;481&quot; data-filename=&quot;스크린샷 2026-05-27 14-10-29.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779873732522&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;    h_layout_2.addWidget(button_1, 2)
    h_layout_2.addWidget(button_2, 1)
    h_layout_2.addWidget(button_3, 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 버튼 1은 크고, 버튼 2, 3번은 크기가 동일한 크기로 조정 (stretch)&amp;nbsp; stretch 값 수정&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-27 14-14-19.png&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyhd8d/dJMcaaMbT0l/XrkSLt0jkuWRYZhOw1fAzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyhd8d/dJMcaaMbT0l/XrkSLt0jkuWRYZhOw1fAzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyhd8d/dJMcaaMbT0l/XrkSLt0jkuWRYZhOw1fAzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdyhd8d%2FdJMcaaMbT0l%2FXrkSLt0jkuWRYZhOw1fAzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1096&quot; height=&quot;671&quot; data-filename=&quot;스크린샷 2026-05-27 14-14-19.png&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779871369234&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# widget.py 레이아웃 종합 예제 실습하기

from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout, QSizePolicy, QLineEdit, QPushButton

class SizePolicyExample(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle(&quot;사이즈 정책(SizePolicy) 및 스트레치 학습&quot;)

        # [1] SizePolicy 설정
        # SizePolicy는 부모 창의 크기가 변할 때 위젯의 크기를 어떻게 조절할 것인지 결정하는 정책입니다.
        label = QLabel(&quot;텍스트 입력: &quot;)
        line_edit = QLineEdit()

        # 가로 방향은 가능한 한 늘어나고(Expanding), 세로 방향은 고정(Fixed)
        line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        # [2] 첫 번째 가로 레이아웃 : 텍스트 + 입력창
        h_layout_1 = QHBoxLayout()
        h_layout_1.addWidget(label)
        h_layout_1.addWidget(line_edit)

        # [3] 버튼 3개 생성
        button_1 = QPushButton(&quot;버튼1&quot;)
        button_2 = QPushButton(&quot;버튼2&quot;)
        button_3 = QPushButton(&quot;버튼3&quot;)

        # [4] 스트레치 설정
        # 버튼1은 stretch=2 &amp;rarr; 2배 넓은 공간 차지
        # 버튼2, 버튼3은 stretch=1 &amp;rarr; 동일 비율로 차지
        h_layout_2 = QHBoxLayout()
        h_layout_2.addWidget(button_1, 2)
        h_layout_2.addWidget(button_2, 1)
        h_layout_2.addWidget(button_3, 1)

        # [5] 최종 전체 수직 레이아웃
        v_layout = QVBoxLayout()
        v_layout.addLayout(h_layout_1)
        v_layout.addLayout(h_layout_2)

        self.setLayout(v_layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2-2) stretch 값을 버튼 3에 많이 주고 나머지는 작은 값을 주어 크기를 조절한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp; 버튼1이 가장 크기가 크고 나머지는 동일하다. 값을 수정해 주면 된다. 1,1,1,&amp;nbsp; ----&amp;gt; 1,1,&lt;span style=&quot;color: #ef5369;&quot;&gt;2&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-27 14-24-33.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4zRO9/dJMb99T3KKj/lF4WNDSQdkGACRHVlpKlzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4zRO9/dJMb99T3KKj/lF4WNDSQdkGACRHVlpKlzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4zRO9/dJMb99T3KKj/lF4WNDSQdkGACRHVlpKlzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4zRO9%2FdJMb99T3KKj%2FlF4WNDSQdkGACRHVlpKlzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1095&quot; height=&quot;564&quot; data-filename=&quot;스크린샷 2026-05-27 14-24-33.png&quot; data-origin-width=&quot;1095&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779872427832&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        # [4] 레이아웃 설정 (비율)
        # 버튼3의 stretch 비율을 2로 하여 버튼1,2보다 2배 넓게 차지하도록 설정
        h_layout_2 = QHBoxLayout()
        h_layout_2.addWidget(button_1, 1)
        h_layout_2.addWidget(button_2, 1)
        h_layout_2.addWidget(button_3, 2)

        # [5] 전체 메인 레이아웃 구성
        v_layout = QVBoxLayout()
        v_layout.addLayout(h_layout_1)
        v_layout.addLayout(h_layout_2)
        
        self.setLayout(v_layout)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2-3) 사이즈 정책에서 스트레치 값을 조정하여 크기 조절&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ line_edit를 한줄 더 추가해서 텍스트 입력창의 행을 늘려준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-27 14-30-35.png&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lKaEm/dJMcad3bmrA/fTIChVdMh90YYtnAPsez40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lKaEm/dJMcad3bmrA/fTIChVdMh90YYtnAPsez40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lKaEm/dJMcad3bmrA/fTIChVdMh90YYtnAPsez40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlKaEm%2FdJMcad3bmrA%2FfTIChVdMh90YYtnAPsez40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1027&quot; height=&quot;782&quot; data-filename=&quot;스크린샷 2026-05-27 14-30-35.png&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779873605486&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 텍스트 입력창에 라인에디트 한줄을 더 추가하여 입력 행을 늘려 준다.
# [2] 첫 번째 가로 레이아웃 : 텍스트 + 입력창
        h_layout_1 = QHBoxLayout()
        h_layout_1.addWidget(label)
        h_layout_1.addWidget(line_edit)
        h_layout_1.addWidget(line_edit)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2-4) 입력창 높이를 더 크게 늘리기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 복합 레이아웃 적용 : 텍스트 입력창 크기, 버튼 3번 크게 하기, 전체적인 창크기 고정 아닌 가능한 늘어나게 하기 (수정한 부분)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;입력창 높이를 더 크게 늘리기.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxcRXz/dJMcah5FFum/kZpqF3kzNqkI1kocDatZw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxcRXz/dJMcah5FFum/kZpqF3kzNqkI1kocDatZw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxcRXz/dJMcah5FFum/kZpqF3kzNqkI1kocDatZw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxcRXz%2FdJMcah5FFum%2FkZpqF3kzNqkI1kocDatZw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1031&quot; height=&quot;711&quot; data-filename=&quot;입력창 높이를 더 크게 늘리기.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779873782423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        # 수정한 부분 / 로컬 변수 대신 self.line_edit로 선언 (인스턴스 변수화)
        self.input_field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)


        # 가로 방향은 가능한 한 늘어나고(Expanding), 세로 방향은 고정(Fixed)
        line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)

        # 수정한 부분 [2] 첫 번째 가로 레이아웃 : 텍스트 + 입력창
        h_layout_1 = QHBoxLayout()
        h_layout_1.addWidget(label)
        h_layout_1.addWidget(line_edit)
        h_layout_1.addWidget(line_edit)

        # [3] 버튼 3개 생성
        button_1 = QPushButton(&quot;버튼1&quot;)
        button_2 = QPushButton(&quot;버튼2&quot;)
        button_3 = QPushButton(&quot;버튼3&quot;)

        # [4] 스트레치 설정
        # 버튼1은 stretch=2 &amp;rarr; 2배 넓은 공간 차지
        # 버튼2, 버튼3은 stretch=1 &amp;rarr; 동일 비율로 차지
        h_layout_2 = QHBoxLayout()
        h_layout_2.addWidget(button_1, 1)
        h_layout_2.addWidget(button_2, 1)
        h_layout_2.addWidget(button_3, 2)  # 수정한 부분&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2-5) 라벨과 입력창 사이 간격을 추가하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 텍스트 입력창과 글자 입력창이 조화롭지 않게 보인다. 좀더 보기 좋은 화면을 위해서는 버튼3 크기와 텍스트 입력창 크기를 조절해 줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;라벨과 입력창 사이 간격 추가하기.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oCiFR/dJMcacDijIZ/jwI08a1KQkUrU0ndXd0Zi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oCiFR/dJMcacDijIZ/jwI08a1KQkUrU0ndXd0Zi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oCiFR/dJMcacDijIZ/jwI08a1KQkUrU0ndXd0Zi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoCiFR%2FdJMcacDijIZ%2FjwI08a1KQkUrU0ndXd0Zi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1031&quot; height=&quot;711&quot; data-filename=&quot;라벨과 입력창 사이 간격 추가하기.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(예제 2-6) &lt;b&gt;버튼을 클릭했을 때 QLineEdit 위젯에 입력한 값 터미널 화면에 출력하기&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;수정전 초기&amp;gt;&amp;nbsp;&lt;/b&gt; 문제점 : 코드 수정을 달리하여&amp;nbsp; main.py 화면창만 만들어 졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.setWindowTitle (&quot;위젯 크기 정책(SizePolicy)&quot;) 이 main.py가 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;레이아웃 터미널에 함수로 출력 기능 구현 초기.png&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yeQkA/dJMcahkjzJ4/kZSEhog5nx1gtuzLZ8YP71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yeQkA/dJMcahkjzJ4/kZSEhog5nx1gtuzLZ8YP71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yeQkA/dJMcahkjzJ4/kZSEhog5nx1gtuzLZ8YP71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyeQkA%2FdJMcahkjzJ4%2FkZSEhog5nx1gtuzLZ8YP71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1046&quot; height=&quot;522&quot; data-filename=&quot;레이아웃 터미널에 함수로 출력 기능 구현 초기.png&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;구문 수정 후&amp;gt; &lt;/b&gt;버튼을 클릭했을 때 QLineEdit 위젯에 입력한 값이 터미널 화면에 출력되도록 수정되었다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779874879391&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    def init_ui(self):
        self.setWindowTitle(&quot;QLineEdit 입력값 출력&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XcsrL/dJMcad3bo2Z/f6pIFZ29QHiJh94NR5lu51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XcsrL/dJMcad3bo2Z/f6pIFZ29QHiJh94NR5lu51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XcsrL/dJMcad3bo2Z/f6pIFZ29QHiJh94NR5lu51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXcsrL%2FdJMcad3bo2Z%2Ff6pIFZ29QHiJh94NR5lu51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;929&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779873837479&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QPushButton

class MyWidget(QWidget):

    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle(&quot;QLineEdit 입력값 출력&quot;)
        self.resize(300, 150)
        # 레이아웃 생성
        layout = QVBoxLayout()
#### 버튼을 클릭했을 때 QLineEdit 위젯에 입력한 값 터미널화면에 출력하기
        # QLineEdit 생성
        self.input_line_edit = QLineEdit(self)
        self.input_line_edit.setPlaceholderText(&quot;텍스트를 입력하세요.&quot;)

        # QPushButton 생성
        self.print_btn = QPushButton(&quot;터미널에 출력&quot;, self)
        self.print_btn.clicked.connect(self.print_input_value)

        # 레이아웃에 위젯 추가
        layout.addWidget(self.input_line_edit)
        layout.addWidget(self.print_btn)
        self.setLayout(layout)

    def print_input_value(self):
        # QLineEdit 입력값 변수로 가져오는 방법
        input_data = self.input_line_edit.text().strip()

        # 터미널에 출력
        print(f&quot;입력된 값: {input_data}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/48</guid>
      <comments>https://goodjop79.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 27 May 2026 18:43:27 +0900</pubDate>
    </item>
    <item>
      <title>D+34 [SQL] 프로그램 설치 및 환경 설정 가이드</title>
      <link>https://goodjop79.tistory.com/47</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일&amp;nbsp; 자 : 2026년 5월 26일(화)&lt;/b&gt;&lt;br&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Maria DB&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;MariaDB 서버는 가장 인기 있는 오픈 소스 관계형 데이터베이스 중 하나인데, MySQL의 초기 개발자들이 만들었으며, 앞으로도 오픈 소스로 유지될 것이 보장되어 있다. 클라우드 서비스에 포함되어 있으며, 대부분의 Linux 배포판에서 기본으로 제공된다.&lt;br&gt;MariaDB는 성능, 안정성, 개방성이라는 가치를 기반으로 구축되었으며, MariaDB 재단은 기술적 우수성을 기준으로 기여를 수용할 것을 보장한다.&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br&gt;&lt;b&gt;2. Mysql, MariaDB 실행 환경 설정&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT @golbal.time_zene, @session.time_zone;&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n47VQ/dJMcafzZa8R/K8KRaak0iQjFVfmdATLwrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n47VQ/dJMcafzZa8R/K8KRaak0iQjFVfmdATLwrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n47VQ/dJMcafzZa8R/K8KRaak0iQjFVfmdATLwrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn47VQ%2FdJMcafzZa8R%2FK8KRaak0iQjFVfmdATLwrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;861&quot; height=&quot;220&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 시간대 설정 및 날짜 확인&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;mysql&amp;gt; exit&amp;nbsp;&amp;nbsp; // 나가기
:~$ sudo timedatectl set-timezone 'Asia/Seoul' //터미널에서 시간대 설정
:~$ date&amp;nbsp;&amp;nbsp;// 날짜, 시간 출력
2026. 5. 26.(화) 14:00:20 [kkk]&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;- 접속 실행 하기&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;:~$ sudo mysql -u root&amp;nbsp;&amp;nbsp;// 접속 실행 하기 
mysql&amp;gt; sudo systemctl restart mysqld
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;gt; ET GLOBAL time_zone='+09:00';
mysql&amp;gt;SET time_zone='+09:00';&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;※ 터미널 창에서 입력시&lt;u&gt; :~$&amp;nbsp;&lt;/u&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;SQL루트에서 입력시&amp;nbsp; &lt;u&gt;mysql&amp;gt;&lt;/u&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 1.&amp;nbsp; MariaDB 외부 접속 허용, 방화벽 허용&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;:~$ sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf // 외부 접속 허용
:~$ GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '패스워드'; // 모든 ip 허용
:~$ GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.10.0.%' IDENTIFIED BY '패스워드';&amp;nbsp;&amp;nbsp;// 특정대역대 ip 허용
:~$ GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.10.0.254' IDENTIFIED BY '패스워드'; // 특정 ip 허용
:~$ DELETE FROM mysql.user WHERE Host='%' AND User='아이디'; // ip 허용상태 원복
:~$ FLUSH PRIVILEGES;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // ip 상태 적용하기
:~$ firewall-cmd --list-all-zones // 방화벽 설정 확인
:~$ firewall-cmd --permanent --zone=public --add-port=3306/tcp // 방화벽 허용 설정&lt;/code&gt;&lt;/pre&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br&gt;&lt;b&gt;2. 2. Timezone 설정 하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;:~$ sudo mysql -u root -p&amp;nbsp;&amp;nbsp; // 루트 접속
mysql&amp;gt; SELECT @@global.time_zone, @@session.time_zone;&amp;nbsp;&amp;nbsp;// 접속 시간대 확인
mysql&amp;gt; exit&amp;nbsp;&amp;nbsp;// 종료 나가기
:~$ sudo timedatectl set-timezone 'Asia/Seoul'&amp;nbsp;&amp;nbsp;// 아시아 서울 시간대 설정하기
:~$ date&amp;nbsp;&amp;nbsp; // 설정한 결과 출력하기
:~$ sudo mysql -u root -p&amp;nbsp;&amp;nbsp;// sql 루트 변경&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
mysql&amp;gt; sudo systemctl restart mysqld&amp;nbsp;&amp;nbsp;// sql에서 시간대 설정
mysql&amp;gt; SET GLOBAL time_zone='+09:00';&amp;nbsp;&amp;nbsp;// 09시 표준시간대 설정
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;gt; SET time_zone='+09:00';
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;gt; ctrl + c&amp;nbsp;&amp;nbsp;// 입력 줄 나가기 
mysql&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br&gt;&lt;b&gt;2. 3. MariaDB, root 비밀번호 재설정&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;mysql&amp;gt; update user set plugin='' where User='root';
mysql&amp;gt; set password = password('비번입력');
mysql&amp;gt; flush privileges;&lt;/code&gt;&lt;/pre&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 4. MariaDB, C 컴파일 테스트&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; * MariaDB 공식 홈페이지&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;*&amp;nbsp;C++&amp;nbsp;Programs&amp;nbsp;to&amp;nbsp;MariaDB&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;*&amp;nbsp;C++&amp;nbsp;헤더&amp;nbsp;파일&amp;nbsp;다운&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;*&amp;nbsp;CPP&amp;nbsp;test&amp;nbsp;파일&amp;nbsp;git&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# mysql -h는&amp;nbsp;&amp;nbsp;MySQL서버에 접속할 때, 원격 호스트(IP 주소 또는 도메인)를 지정하는 명령어 옵션, 주로 로컬이 아닌 원격 서버의 데이터베이스에 접근할 때 사용함
:~$ sudo apt install libmysqlclient-dev -y&amp;nbsp;&amp;nbsp;// mysql.h 없을 경우 다운로드 해서 설치한다.
:~$ mysql_config --cflags&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// mysql.h의 위치 찾기&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
:~$ #include &quot;/usr/include/mysql/mysql.h&quot;&amp;nbsp;&amp;nbsp;// mysql.h 경로로 추가하기&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 5. MariaDB Timezone 비밀번호 재설정&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo mysql
mysql&amp;gt; update user set plugin='' where User='root';
mysql&amp;gt; set password = password('비번입력');
mysql&amp;gt; flush privileges;&lt;/code&gt;&lt;/pre&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 6. MariaDB, C 컴파일 테스트&lt;/b&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp; &amp;nbsp; * MariaDB 공식 홈페이지 :&amp;nbsp;&lt;a href=&quot;https://mariadb.org/download/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://mariadb.org/download/&lt;/span&gt;&lt;/a&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;C++&amp;nbsp;Programs&amp;nbsp;to&amp;nbsp;MariaDB&amp;nbsp;:&amp;nbsp;링크&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;C++&amp;nbsp;헤더&amp;nbsp;파일&amp;nbsp;다운&amp;nbsp;:&amp;nbsp;링크&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*&amp;nbsp;CPP&amp;nbsp;test&amp;nbsp;파일&amp;nbsp;git&amp;nbsp;:&amp;nbsp;링크&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 후&amp;nbsp; 기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;Maria DB를 활용한 오픈소스 공동개발자(Frank Karlitscheck. Nextcloud) 대화 영상시청 내용 :&lt;br&gt;- 오픈소스 = 디지털 주도권 이다.&lt;br&gt;- 오픈소스 커뮤티티에는 넥스트 클라우드, 마리아db처럼 고객과 직원, 개발자를 보유하고 오픈 소스 s/w 개발에&lt;br&gt;&amp;nbsp; 도움 주고, 독점 클라우드 서비스에 의존하고 있는 현실에서 오픈소스는 개발자들에게 유용하게 사용되기도 한다.&lt;br&gt;그러나&amp;nbsp; SQL은 수많은 빅데이터를 관리, 분석, 활용하는 만큼 안정성과 팩트 경향성 등이 중요하다. 몇가지 간단한 문법들을 공부하면서 데이터를 분석해서 결과값을 가지고 각종 정책 반영 및 아이디어 제공 등에 활용된다. 공공데이터 개방을 잘 활용하는 것도 그것이다. &quot;도전은 계속된다.&quot;&amp;nbsp;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>환경 설정</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/47</guid>
      <comments>https://goodjop79.tistory.com/47#entry47comment</comments>
      <pubDate>Tue, 26 May 2026 21:01:12 +0900</pubDate>
    </item>
    <item>
      <title>D+34 [아두이노] Arduino '전자키보드 만들기' 실습</title>
      <link>https://goodjop79.tistory.com/46</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일&amp;nbsp; 자 : 2026년 5월 26일(화)&lt;/b&gt;&lt;/h4&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 아두이노 환경에서 전자키보드를 만드는 보드와 회로 연결하기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1761&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xj9YN/dJMcah5EUwM/z1d6pYqxzrnKPK3d84Vwr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xj9YN/dJMcah5EUwM/z1d6pYqxzrnKPK3d84Vwr1/img.png&quot; data-alt=&quot;아두이노 회로와 코드 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xj9YN/dJMcah5EUwM/z1d6pYqxzrnKPK3d84Vwr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxj9YN%2FdJMcah5EUwM%2Fz1d6pYqxzrnKPK3d84Vwr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1761&quot; height=&quot;925&quot; data-origin-width=&quot;1761&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아두이노 회로와 코드 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 예제 [전자 키보드 만들기] 365p

const int melody[] = {&amp;nbsp;&amp;nbsp; // 음계별 주파수를 저장한 변수 선언
&amp;nbsp;&amp;nbsp;262,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 도 주파수
&amp;nbsp;&amp;nbsp;294,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 레 주파수
&amp;nbsp;&amp;nbsp;330,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 미 주파수
&amp;nbsp;&amp;nbsp;349,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 파 주파수
&amp;nbsp;&amp;nbsp;392,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 솔 주파수
&amp;nbsp;&amp;nbsp;440,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 라 주파수
&amp;nbsp;&amp;nbsp;494,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 시 주파수
&amp;nbsp;&amp;nbsp;523,&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 도 주파수
};

const int PIEZO_PIN = 13;&amp;nbsp;&amp;nbsp;// 피에조(검은색 원형 볼륨모양) 스피커에 연결된 디지털 핀번호 

void setup() {

&amp;nbsp;&amp;nbsp;for (int i=6; i&amp;lt;=13; i++) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 디지털 버튼 6-13번 핀
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pinMode(i, INPUT);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 디지털 핀을 입력 모드로 설정&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;Serial.begin(9600);&amp;nbsp;&amp;nbsp;// 직렬 통신 초기화
}

void loop() {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;

&amp;nbsp;&amp;nbsp;int index0fMelody = readNote();&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 누름 버튼을 구분해서 음계 선택
&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;if (index0fMelody &amp;gt; -1)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tone(PIEZO_PIN, melody[index0fMelody], 200);&amp;nbsp;&amp;nbsp;// 선택된 음계를 200ms 동안 재생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;delay(50);&amp;nbsp;&amp;nbsp; // 버튼 누른 시간 고려 일정시간 지연시킨다.
}

int readNote() {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 누른 버튼에 따라 melody 배열의 인덱스값을 반환하는 함수
&amp;nbsp;&amp;nbsp;int index = -1;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//인덱스 초기값

for (int i=6; i&amp;lt;=13; i++) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 디지털 버튼 6번-13번 
&amp;nbsp;&amp;nbsp;int buttonValue = digitalRead(i);&amp;nbsp;&amp;nbsp;// 버튼의 입력값 읽기 반복
&amp;nbsp;&amp;nbsp;if (buttonValue == HIGH)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 버튼을 누르면
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;index = i-6;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // melody의 인덱스값 계산 
}
Serial.println(index);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 직렬 모니터에 인덱스값 출력
return index;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 인덱스값 반환
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: Noto Serif KR;&quot;&gt;&lt;b&gt;기술의 발전&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;전자피아노를 예로 들면 피아노에 기본적인 음악 사운드를 저장해 놓고 건반으로 입력하여 연주를 하기도 한다.&amp;nbsp;전자키보드는 시그널을 보내 소스코드 값을 지정한데로 출력하고, 키보드를 눌러 소리를 낸다.&amp;nbsp;&lt;br&gt;2024년에는 국내 최초로 AI 작곡가(이봄)를 개발하여 배틀에 음악들을 사용했다.&amp;nbsp;빅데이터가 필요한 기존 생성형 AI와는 다른 기술이 적용됐다. 즉 개발자 안 소장이 연구 중인 ‘합성형 AI’ 기술이 활용됐다.&lt;br&gt;이는&amp;nbsp; ”기존 생성형 AI처럼 데이터만 넣고 끝나는 게 아니라 데이터 사용은 최소화하고 이론적인 지식들을 함께 믹스하는 방식“이다.&lt;br&gt;이봄은 음악 콘텐츠만 대량으로 학습하는 게 아니라 음악 이론을 기초로 음을 익히고, 코드를 학습한 뒤 작곡과 연주를 한다.&lt;br&gt;전문가 지식과 노하우를 접목했기 때문에 일반 생성형 AI처럼 많은 데이터가 필요하지 않다. 데이터 사용을 최소화해 에너지 소모를 줄이고 노트북으로도 구동이 가능하다.&amp;nbsp;&amp;nbsp;&lt;br&gt;문자를 만들고 도시 문명을 이루고 수학과 과학 지식을 발견한 인류는 21세기 들어 '로봇자동화'를 통한 스마트 제조, 초고속 무선 인터넷 통신 등 최첨단 기술을 개발했다.&amp;nbsp;&lt;br&gt;아두이노는 이러한 첨단기술에 앞서 어떠한 기계와 프로그램간 연동을 통해 더 낳은 기술에 적용하고 있다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot;&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 프로그램을 개발 하는 구성원&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;프로그램을 개발하는데는 다양한 구성원이 함께 하여 하나의 프로젝트가 완성된다. 이처럼 다양한 직위에 구성원들이 있기에 개발자가 아니더라도 프로그램 '기획'에 대해 이끌어 가는 기획자도 고민해 볼 만 하다. 책과 전공지식 등 개인이 공부 것에 대한 카테고리에 따라 어떤 프로그램을 개발해 볼수 있을까 기획을 해볼 수도 있다. &lt;br&gt;&lt;b&gt;프로그램을 개발하는 팀은&amp;nbsp;기획, 디자인, 개발, 관리, 품질 보증 등 다양한 역할을 가진 전문가들로 구성&lt;/b&gt;&lt;/p&gt;&lt;div data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-hveid=&quot;CAIIAAgHEAA&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 1. 관리 및 기획 (Management &amp;amp; Planning)&lt;/span&gt;&lt;/b&gt;
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;PM (Project Manager / 제품 관리자)&lt;/b&gt;: 프로젝트의 전체 일정, 예산, 리소스를 관리하며 팀 간의 의견을 조율&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;PO (Product Owner / 제품 책임자)&lt;/b&gt;: 제품의 비전과 요구사항을 정의하고, 개발할 기능의 우선순위를 결정&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;기획자 (Product Designer / Planner)&lt;/b&gt;: 사용자 요구사항을 분석하여 프로그램의 구조와 상세 기능 흐름을 설계&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div data-animation-nesting=&quot;&quot; data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 2. 디자인 (Design)&lt;/span&gt;&lt;/b&gt;
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UI/UX 디자이너&lt;/b&gt;: 사용자가 프로그램을 편리하고 아름답게 사용할 수 있도록 화면 구성(UI)과 사용자 경험(UX)을 디자인함&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div data-animation-nesting=&quot;&quot; data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 3. 개발 (Engineering)&lt;/span&gt;&lt;/b&gt;
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;PL (Project Leader / 개발 리더)&lt;/b&gt;: 개발 파트의 리더로서 기술적인 설계를 책임지고 개발자들을 가이드 한다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;프론트엔드 개발자 (Frontend Engineer)&lt;/b&gt;: 사용자가 직접 눈으로 보고 조작하는 웹사이트나 앱의 화면을 개발 한다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;백엔드 개발자 (Backend Engineer)&lt;/b&gt;: 눈에 보이지 않는 서버, 데이터베이스, API를 구축하고 데이터 로직을 처리 한다.&lt;/li&gt;&lt;li&gt;&lt;b&gt;풀스택 개발자 (Full-Stack Engineer)&lt;/b&gt;: 프론트엔드와 백엔드 개발을 모두 수행할 수 있는 개발자&lt;/li&gt;&lt;/ul&gt;&lt;div data-animation-nesting=&quot;&quot; data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sae=&quot;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
 &lt;b&gt;2. 4. 품질 및 운영 (QA &amp;amp; DevOps)&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;
&lt;/div&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;b&gt;QA 엔지니어 / 테스터 (Quality Assurance)&lt;/b&gt;: 완성된 프로그램에 오류가 없는지 테스트, 품질 표준을 만족하는지 검증&lt;/li&gt;&lt;li&gt;&lt;b&gt;데브옵스 엔지니어 (DevOps / Infra)&lt;/b&gt;: 개발된 프로그램이 서버에 안정적으로 배포, 원활하게 운영되도록 시스템 인프라 관리&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 후 기&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;아두이노는 좁은 범위에서부터 큰 범위에 이르기까지 전기전자 및 컴퓨터 공학을 두루 어루러지게 하는 학습이다. 일상생활에서 사용하는 모든 기계와 관련된 용품들이 그렇다. 우리는 편리하게 사용하는 전자제품과 기계 설비 들이 모두 백그라운드에서 상호 연동되어 실시간 프로그램으로 가동되는 것을 알고 있는 사실이다. 그렇다면 좀더 편하고 좀더 저렴하고, 좀더 빠르게 구동할 수 있는 개발 방식은 어떤 것들이 있는지에 대한 의문이 생긴다. 과학의 발전은 호기심에서 시작된다는 말이 있듯이 모든 개발은 호기심과 관심에서 시작된다.&lt;br&gt;프로그램을 개발하는 데는 많든 구성원들이 한팀이 되어 움직이듯 관리, 기획, 디자인, 개발 등 세분화 되어 다양한 업무를 분담하게 된다. 전혀 다른 분야에 새로운 도전은 호기심이 많은 분야이다. 앞으로&amp;nbsp; &quot;도전은 계속된다.&quot;&lt;/p&gt;</description>
      <category>학습 로그/Arduino</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/46</guid>
      <comments>https://goodjop79.tistory.com/46#entry46comment</comments>
      <pubDate>Tue, 26 May 2026 20:39:24 +0900</pubDate>
    </item>
    <item>
      <title>D+33 [SQL]  빅쿼리(BigQuery)가 뭐야?</title>
      <link>https://goodjop79.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ㅇ 일 자 : 2026년 5월 22일(금)&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;0&quot; data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;b&gt;빅쿼리(BigQuery)란?&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;구글 클라우드 플랫폼(GCP)의 가장 강력한 무기이자, &lt;br /&gt;데이터 엔지니어링과 분석 분야에서 업계 표준으로 자리 잡은 빅쿼리(BigQuery) &lt;br /&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;009&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp;빅쿼리(BigQuery)란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글이 내부에서 엄청난 양의 로그 데이터를 처리하기 위해 쓰던 '드레멜(Dremel)'이라는 기술을 외부 사용자들이 쓸 수 있게 상용화한 &lt;b&gt;초고속&amp;middot;대용량 데이터 웨어하우스(Data Warehouse)&lt;/b&gt; 서비스 이다.&lt;br /&gt;쉽게 말해, &quot;엄청나게 거대한 엑셀 파일들을 모아두고, 아무리 양이 많아도 눈 깜짝할 사이에 원하는 데이터를 찾아주는 창고&quot;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 빅쿼리의 압도적인 강점 3가지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) 서버리스 (Serverless) &amp;mdash; &quot;서버 관리가 필요 없다&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 데이터베이스는 데이터가 늘어나면 서버의 컴퓨터 사양을 업그레이드(Scale-up)하거나 서버 대수를 늘리는(Scale-out) 복잡한 엔지니어링 작업이 필요한데 빅쿼리는 서버관리가 필요 없다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빅쿼리는 사용자가 서버를 전혀 신경 쓸 필요가 없다. 구글이 알아서 수천 대의 컴퓨터를 동원해 연산을 처리하므로, 사용자는 오직 데이터와 쿼리문(SQL)에만 집중하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2) 상상을 초월하는 속도 (테라바이트를 몇 초 만에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 데이터베이스에 수십억 건의 데이터(테라바이트 단위를 넘어 페타바이트 단위)를 넣고 검색하면 서버가 멈추거나 몇 시간이 걸린다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빅쿼리는 데이터를 &lt;b&gt;컬럼(Column, 열) 기반&lt;/b&gt;으로 저장하고, 수천 대의 구글 서버가 동시에 나누어 계산(분산 컴퓨팅)하기 때문에, &lt;b&gt;수십억 건의 데이터도 단 몇 초 만에&lt;/b&gt; 분석해 낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3) 친숙한 표준 SQL 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 혁신적인 기술이라도 사용법이 어려우면 쓸 수 없겠죠. 빅쿼리는 개발자나 데이터 분석가들이 기존에 쓰던 표준 SQL(데이터베이스 조회 언어)을 그대로 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT * FROM 테이블 WHERE 조건 같은 &lt;b&gt;기본 문법&lt;/b&gt;만 알면 누구나 이 엄청난 성능을 바로 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 비용 산정 방식 (⚠️ 주의할 점)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리는 서버를 켜놓은 시간에 돈을 받지 않고, &quot;내가 쿼리를 날려서 스캔한 데이터의 양&quot;에 따라 비용을 청구한다. (기본적으로 매달 1TB의 쿼리 스캔은 무료로 제공된다.)&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;  &lt;/span&gt;&lt;b&gt;실무 팁:&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt; 만약 100TB짜리 데이터 정렬창고에서 SELECT * (모든 데이터 다 가져와)를 무심코 실행하면 순식간에 수십만 원의 비용이 청구될 수 있다. 그래서 &lt;u&gt;실무에서는 필요한 열(Column)만 콕 집어서 조회하는 습관&lt;/u&gt;이 필수적임&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 리눅스 / 개발자 과정과의 연결고리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리눅스 환경&lt;/b&gt;이나 &lt;b&gt;파이썬(PyCharm)&lt;/b&gt; 코드에서 &quot;빅쿼리 라이브러리&quot;를 설치하면, 서버에서 발생하는 수많은 로그 데이터를 빅쿼리로 바로 쏘아 보낼 수 있다. 그렇게 쌓인 빅데이터를 빅쿼리로 분석하고, 그 결과를 바탕으로 머신러닝(AI) 모델을 돌리는 것이 현대 백엔드 / 데이터 개발의 표준적인 흐름이다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 후 기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 기본문법을 통해 데이터를 분석해 내는 인공지능 기능을 사용함에 있어서&amp;nbsp; 관련된 데이타 제공에 대한 보안성의 문제는 간과해서는 안될 문제라고 생각한다. 일반적인 빅데이터 분석에 빅쿼리를 활용하는 것은 기업체에서 머신러닝을 어떤 방법으로 활용하는지도 연계해서 생각해 봐야 한다.&lt;br /&gt;어차피 데이터 자동분석도 비용과 연관되어 있는 문제로서 기업 입장에서는 무조건 도입해서 사용하기에는 부담스러울 수도 있다. 그러나 빅데이터 분석이 아주 빈번한 곳에서 3인이 분석하던 것을 빅쿼리 하나가 하고 있다면 기업에서는 도입하지 않을 이유가 없다고 생각한다. 하루가 빠르게 변하고 있는 AI 시대에 빅데이터 분석에 대한 생각을 다시한번 하게 된다. 그러나 기본에 충실하는 &quot;도전은 계속된다.&quot;&lt;/p&gt;</description>
      <category>인공지능 AI</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/45</guid>
      <comments>https://goodjop79.tistory.com/45#entry45comment</comments>
      <pubDate>Tue, 26 May 2026 07:00:43 +0900</pubDate>
    </item>
    <item>
      <title>D+33 [PySide6] 핵심 프레임워크는 무엇 일까?</title>
      <link>https://goodjop79.tistory.com/44</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 22일(금)&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; PySide6 핵심 프레임워크의 방대한 내용을 한눈에 파악하고 구조적으로 이해할 수 있도록 정리 했다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. PySide6 핵심 아키텍처 및 UI 컴포넌트&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.1. PySide6 핵심 아키텍처 &amp;amp; GUI 기본 구조&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BCRA7/dJMcaii8XzM/wLGVKxPeEFc91Y9RtGEkpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BCRA7/dJMcaii8XzM/wLGVKxPeEFc91Y9RtGEkpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BCRA7/dJMcaii8XzM/wLGVKxPeEFc91Y9RtGEkpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBCRA7%2FdJMcaii8XzM%2FwLGVKxPeEFc91Y9RtGEkpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QApplication:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 전체 앱의 생명 주기와 사용자 입력(마우스, 키보드) 등 모든 이벤트를 관리하는 최상위 엔진&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QMainWindow:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 메뉴바, 툴바, 상태바를 기본으로 포함하는 완성형 메인 윈도우 구조틀&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QWidget:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 화면에 표시되는 모든 UI 구성요소(위젯)의 최상위 부모 클래스&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QAction:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 메뉴나 툴바 아이템처럼 사용자의 '명령 행동'을 추상화하여 관리하는 객체&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Signal &amp;amp; Slot (이벤트 처리 구조)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PySide6에서 컴포넌트 간 데이터를 주고받고 이벤트를 처리하는 핵심 소통 방식&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Signal (신호):&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 위젯에 이벤트(클릭, 값 변경 등)가 발생했을 때 외부에 알리는 발생 장치&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Slot (슬롯):&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Signal을 전달받아 실제 비즈니스 로직을 수행하는 연결된 함수(Function)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Signal &amp;amp; Slot 처리 구조&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNN2hq/dJMcabK9wyE/bnOZfzMiLKi2FbQaAHsy40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNN2hq/dJMcabK9wyE/bnOZfzMiLKi2FbQaAHsy40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNN2hq/dJMcabK9wyE/bnOZfzMiLKi2FbQaAHsy40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNN2hq%2FdJMcabK9wyE%2FbnOZfzMiLKi2FbQaAHsy40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 레이아웃 매니저 (Layout Managers)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;화면 크기가 변하더라도 위젯들을 절대 좌표가 아닌, 수학적&amp;middot;구조적 규칙에 따라 자동 배치하는 도구&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레이아웃 클래스&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배치 방식 및 특징&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 활용 예시&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QVBoxLayout&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯을 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;세로(Vertical)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 방향으로 일렬 배치&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메인 메뉴, 입력 폼 세로 정렬&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QHBoxLayout&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯을 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가로(Horizontal)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 방향으로 일렬 배치&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하단 확인/취소 버튼 바&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QGridLayout&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;행(Row)과 열(Column)의 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;바둑판 표&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 형태로 배치&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;계산기 키패드, 대시보드&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QFormLayout&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왼쪽에 레이블, 오른쪽에 입력창을 갖는 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2열 구조&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그인 화면, 회원가입 양식&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중첩 레이아웃:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 레이아웃 안에 또 다른 레이아웃을 넣어 복잡한 화면을 설계하는 방식&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Stretch &amp;amp; Spacing:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;stretch&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 위젯 간의 확장 비율을, &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;spacing&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;은 위젯 사이의 간격(여백)을 조정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 카테고리별 핵심 위젯 (Widgets) 요약&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;① 기본 입력 위젯 (Input Widgets)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QLineEdit:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 단일 행 텍스트 입력창 (ID, 비밀번호 등).&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QTextEdit:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 여러 줄 문장 입력 및 서식(Rich Text) 지원 창 (본문 작성).&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QCheckBox / QRadioButton:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 다중 선택 체크박스 및 택일형 라디오 버튼.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QComboBox:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 공간을 절약하는 드롭다운 선택 상자.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QSpinBox / QDoubleSpinBox:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 각각 정수 및 실수를 화살표나 타이핑으로 입력받는 창.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QDateEdit / QTimeEdit:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 날짜 및 시간 선택 위젯.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;② 표시 및 상태 위젯 (Display Widgets)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QLabel:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 화면에 텍스트, 이미지(&lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;QPixmap&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;), 스타일을 표현하는 출력 위젯.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QProgressBar:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 작업 진행 상태(%)를 시각적으로 보여주는 바.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QLCDNumber:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 디지털시계처럼 숫자만 LCD 형태로 크게 표시하는 위젯.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 고급 기능 및 데이터 프레임워크&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5.1. 컨테이너 및 대화상자 (Containers &amp;amp; Dialogs)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯들을 묶어서 화면을 효율적으로 분할하거나, 별도의 팝업 창을 띄워 소통하는 구조&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;① 컨테이너 위젯 (화면 공간 관리)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QGroupBox:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 연관된 위젯들을 그룹 테두리로 묶고 제목을 붙여 시각적 분류 제공.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QTabWidget:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 상단 탭을 눌러 한 공간에서 여러 페이지를 전환하며 보기.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QStackedWidget:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 탭 없이 코드 제어를 통해 화면 전체를 통째로 전환 (페이지 넘기기)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QScrollArea:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 컨텐츠가 화면보다 클 때 스크롤바를 자동 생성.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QSplitter:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 사용자가 마우스로 경계선을 끌어서 좌우/상하 영역 크기를 조절하는 분할&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;② 표준 다이얼로그 (Standard Dialogs)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QMessageBox:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 경고, 알림, 예/아니오 확인 등 단순 팝업 메시지 창.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QFileDialog:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 운영체제 표준 파일 열기 / 저장 창.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QColorDialog / QFontDialog:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 색상 피커 및 폰트 변경 표준 창.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QInputDialog:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 사용자에게 간단한 한 줄 텍스트나 숫자를 즉시 입력받는 팝업.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 데이터 표시 프레임워크: Widget형 vs Model/View형&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Widget 방식 (QListWidget, QTableWidget, QTreeWidget):&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 데이터와 화면 컴포넌트가 결합하여 코드가 직관적이고 다루기 쉬우나, 데이터가 아주 많아지면 무거워집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Model/View 방식 (QListView, QTableView, QTreeView):&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 데이터 모델(&lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;Model&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;)과 화면 출력(&lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;View&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;)을 철저히 분리합니다. 수만 개 이상의 대용량 데이터를 처리하거나 실시간 필터링(&lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;QSortFilterProxyModel&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;)을 구현할 때 매우 효율적입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필터 정렬 (Filter / Sort) : QSortFilter Proxy Model&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPCdUX/dJMcabK9wzx/d1Bmxqe7omaj1VKGwI6le1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPCdUX/dJMcabK9wzx/d1Bmxqe7omaj1VKGwI6le1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPCdUX/dJMcabK9wzx/d1Bmxqe7omaj1VKGwI6le1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPCdUX%2FdJMcabK9wzx%2Fd1Bmxqe7omaj1VKGwI6le1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 스타일링 및 성능 최적화 (Styling &amp;amp; Threads)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;① UI 디자인 (Style)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;setStyleSheet():&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 웹의 CSS와 유사한 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QSS(Qt Style Sheets)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 문법을 사용하여 위젯의 배경색, 테두리, 둥글기, 폰트 등을 자유롭게 꾸밀 수 있습니다. 애플리케이션 전체에 일괄 적용하여 다크 모드 등을 쉽게 구현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;② 백그라운드 처리 (Performance)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QTimer:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 대기 상태 없이 일정 주기(ms 단위)마다 특정 Slot 함수를 반복 실행합니다. (디지털시계, 카운트다운 등에 필수).&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QThread &amp;amp; Worker 구조:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 대용량 파일 읽기나 네트워크 다운로드처럼 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오래 걸리는 작업&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 메인 GUI 스레드와 분리하여 백그라운드에서 실행합니다. 만약 분리하지 않으면 화면이 얼어붙는(먹통) 현상이 발생합니다. 작업 완료 후 &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;Signal/Slot&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 통해 안전하게 GUI에 결과값이나 진행률을 전달합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 개발 생산성 및 프로젝트 구조화&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현업 수준의 유지보수 가능한 애플리케이션 개발을 위한 관리 기법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;① Qt Designer 도구 체인&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Qt Designer:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 마우스 드래그 앤 드롭으로 UI 화면을 배치하고 &lt;/span&gt;.ui&lt;span style=&quot;color: #000000;&quot;&gt; 파일로 저장하는 시각적 저작 도구.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pyside6-uic:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 터미널 명령어를 통해 &lt;/span&gt;.ui&lt;span style=&quot;color: #000000;&quot;&gt; 파일의 XML 구조를 Python 코드(&lt;/span&gt;.py&lt;span style=&quot;color: #000000;&quot;&gt;)로 자동 변환하는 컴파일러.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;objectName:&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 개발자가 Designer에서 지정한 위젯 고유의 이름으로, 파이썬 코드에서 &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;self.ui.objectName&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 형태로 접근하여 로직을 연결&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;② 이상적인 프로젝트 폴더 구조 (구조화)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단일 파일 코딩의 한계를 극복하고, 파일별 역할을 분리하여 협업과 유지보수를 극대화&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;my_project/ &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;앱&amp;nbsp;실행&amp;nbsp;및&amp;nbsp;QApplication&amp;nbsp;초기화&amp;nbsp;담당 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;widget.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;비즈니스&amp;nbsp;로직&amp;nbsp;및&amp;nbsp;이벤트(Slot)&amp;nbsp;바인딩&amp;nbsp;수행 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;ui_form.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;Qt&amp;nbsp;Designer에서&amp;nbsp;변환된&amp;nbsp;UI&amp;nbsp;클래스&amp;nbsp;파일&amp;nbsp;(.py) &lt;/b&gt;&lt;br /&gt;&lt;b&gt;├──&amp;nbsp;config.json&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;창&amp;nbsp;크기,&amp;nbsp;테마&amp;nbsp;등&amp;nbsp;환경&amp;nbsp;설정&amp;nbsp;데이터 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;└──&amp;nbsp;resources/&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;아이콘,&amp;nbsp;이미지,&amp;nbsp;QSS&amp;nbsp;스타일시트&amp;nbsp;등&amp;nbsp;리소스&amp;nbsp;폴더&lt;/b&gt;&lt;/p&gt;</description>
      <category>IT 컴퓨터</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/44</guid>
      <comments>https://goodjop79.tistory.com/44#entry44comment</comments>
      <pubDate>Tue, 26 May 2026 01:16:32 +0900</pubDate>
    </item>
    <item>
      <title>D+32 [Qt for Python]  Designer 적용 구조 및 Layout 이해</title>
      <link>https://goodjop79.tistory.com/43</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ㅇ 주 제 : 프로그램 및 문서의 전체 흐름 이해 (Layout)&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ㅇ 목 표 :&amp;nbsp; &lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;1. GUI 프로그램 화면 전체의 구조를 설계하는 방법을 이해한다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;2. PySide6 환경에서 Designer 적용구조 및 Layout 을 활용한다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ㅇ 내 용&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 전체 흐름 이해하기&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;가. 각종 회의록&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 요약해서 저장하는 경우&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (예) Slack -----&amp;gt;&amp;nbsp; GPT Wrokspace&amp;nbsp; ------&amp;gt;&amp;nbsp; Google Drive&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;uarr; 협업툴&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;uarr; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Github&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;나. 환경설정은 각 기업체의 특성과 이익과 연계되는 한 해결이 다소 어렵다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각각의 프로그램이 연동되는지가 관건 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;---&amp;gt; 프로그램간 연동을 가능케 하는 버젼의 드라이버를 별도로 설치해야 하는 번거로움&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; -- &amp;gt;&amp;gt; 발전방향 :&amp;nbsp; 모든과정의 자동화&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 시대적 흐름의 변화에 적응하는 자세&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;가. 스스로 노력하고 교육 내용을 이해 하지 않으면 발전이 없다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;나. 인공지능 서버는 편하지만 편한 것만 사용하면서 현실에 안주해서는 안된다. 무언가 발전을 위해 노력을 해야만 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;다. AI가 프롬프트에 의해 코드생성은 해 주지만 본인이 알고 이해할 수 있어야 한다.&amp;nbsp; 직접 작성을 해보고 고민해 봐야 한다.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. PySide6 환경에서 Layout&amp;nbsp; 이해 &amp;nbsp; &amp;nbsp;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.1. 레이아웃 (배치, 구성, 배열 방식)&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;가.&amp;nbsp; 어디에, 어떤 크기로, 어떤 순서로 배치할 것인가?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;나. GUI 프로그램 : 버튼, 라벨, 입력창 같은 위젯을 화면에 조화롭게 배치한다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;다. Qt 레이아웃 시스템 : 프로그램 부모위젯 안에서 자식 위젯 들의 위치, 크기를 자동관리 해주는 구조 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (직접 좌표로 배치하는 방식이 아니라, 위젯 위치/크기를 &quot;자동&quot;관리)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;라. 레이아웃은 창 크기 변화에 맞춰 화면을 자동 정리 해주는 역할&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(예) 아이디 입력창, 비밀번호 입력창, 로그인 버튼, 기타 안내문구 등&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;마.&amp;nbsp; 창의 바탕화면(=부모 위젯),&amp;nbsp; 바탕에 버튼, 라벨, 입력창, 문구 등(=자식 위젯)&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;b&gt;3.2. 레이아웃 종류&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- QVBoxLayout : 세로 배치형&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- QHBoxLayout : 가로 배치형&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- QGridLayotut : 표 형태&amp;nbsp; (계산기 버튼과 같이 규칙적인 표 구조에 유용)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- QFormLayout : 라벨 + 입력폼 (회원가입, 로그인, 설정화면 등 입력창 구조 적합)&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;b&gt;3.3. 레이아웃 중첩과 공간 제어&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp; 중첩&amp;nbsp; :&amp;nbsp; 복잡한 GUI화면에서 레이아웃 안에 또다른 레이아웃을 포함시키는 중첩 방식 사용&amp;nbsp; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (예) 전체화면 세로방향(QVBoxLayout),&amp;nbsp; 버튼 묶음들은 가로방향(QHBoxLayout )&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 공간 제어&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Stretch Facto : 위젯 간 공간비율 조절 ( 1:2:1 공간 배분)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SpacerIteam : 빈 공간을 늘어나게 함&amp;nbsp; (버튼 사이 간격 벌이기)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SizePolicy : 위젯의 가로,세로 확장 정책 (Fixed, Expanding)&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;※ 프로그램 개발시 복잡한 화면 구성을 할 때에는 레이아웃 종류와 중첩, 공간 제어를 통합해서 사용하는 것이 효과적이다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;[예제 1] Layout 이해하기 실습&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; 파이참에서 코드를 실행하면 &quot;윈도우젯 레이아웃 살펴보기&quot; 화면이 생성된다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;987&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdcfW/dJMcabYCgOm/TKOnloLsRcGeJHC32NfpK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdcfW/dJMcabYCgOm/TKOnloLsRcGeJHC32NfpK1/img.png&quot; data-alt=&quot;파이참 에서 &amp;quot;레이아웃 살펴보기&amp;quot; 실행 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdcfW/dJMcabYCgOm/TKOnloLsRcGeJHC32NfpK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdcfW%2FdJMcabYCgOm%2FTKOnloLsRcGeJHC32NfpK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;841&quot; height=&quot;987&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;987&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파이참 에서 &quot;레이아웃 살펴보기&quot; 실행 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 단계별 분석&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 98.7209%; height: 671px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단 계 별&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구 문&amp;nbsp; 활 용&amp;nbsp; 분 석&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1단계&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 클래스 &lt;br /&gt;가져오기&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt; from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;from PySide6.QtWidgets(파이사이드 환경에서 기본창 만들기)&amp;nbsp; import QApplication, QWidget, QLabel(텍스트 표시), QPushButton(버튼 만듬), QVBoxLayout(위젯 세로배치), QHBoxLayout(버튼 가로배치)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2단계&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위젯 생성&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;title_label = QLabel(&quot;레이아웃 예제&quot;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;description_label = QLabel(&quot;버튼을 가로로 배치하고, 전체 화면은 세로로 구성합니다.&quot;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;button_ok = QPushButton(&quot;확인&quot;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;button_cancel = QPushButton(&quot;취소&quot;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;// 화면에 표시할 위젯 생생 후 아직 화면에 배치된 것이 아니므로 레이아웃에 추가해서 실제 화면 구조가 만들어짐&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3단계&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;버튼 영영 가로 레이아웃 만들기&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;button_layout = QHBoxLayout() &amp;nbsp; // 레이아웃 가로 배치&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;button_layout.addWidget(button_ok)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;button_layout.addWidget(button_cancel)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ok 확인버튼과 cancel 취소버튼의 레이아웃을 가로로 배치한다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4단계&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 화면 세로 레이아웃 만들기&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;main_layout = QVBoxLayout()&amp;nbsp; &amp;nbsp; // 레이아웃 세로 배치&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;main_layout.addWidget(title_label)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;main_layout.addWidget(description_label)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;main_layout.addLayout(button_layout)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 화면을 세로 방향으로 구성한다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 , title제목, description 설명, button버튼을 레이아웃에 추가한다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5단계&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부모 위젯에 레이아웃 적용&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;window.setLayout(main_layout)&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;셋레이아웃(부모위젯에 레이아웃 적용)을 호출하면 메인 레이아웃이 윈도우 레이아웃으로 적용된다.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;5.&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QT Creator 환경에서 Layout 구현하기&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5.1. Widget Design&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;새프로젝트 만들기 --&amp;gt; 파일명 작성 --&amp;gt; Widget class명을 선택 --&amp;gt; 인터프리터를 PySide6을 선택 --&amp;gt; Kits 선택 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;--&amp;gt; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;파일 목록 확인 후 마침 --&amp;gt; &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;Design Layout&amp;nbsp;&lt;/span&gt;&lt;br /&gt;① 새프로젝트 만들기 : Qt for Python - Window UI 선택 (윈도우 화면에 구현할 버튼을 포함한 Layout 준비를 한다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;833&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0c5ty/dJMcaicn4aq/KFRNWZtKDWwiF8kV1vVWwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0c5ty/dJMcaicn4aq/KFRNWZtKDWwiF8kV1vVWwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0c5ty/dJMcaicn4aq/KFRNWZtKDWwiF8kV1vVWwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0c5ty%2FdJMcaicn4aq%2FKFRNWZtKDWwiF8kV1vVWwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1258&quot; height=&quot;833&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;833&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;② 사용할 파일명을 작성 입력 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/phgHs/dJMcaaekLG8/ahaeTlkOf3yAnZ3VHfUgGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/phgHs/dJMcaaekLG8/ahaeTlkOf3yAnZ3VHfUgGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/phgHs/dJMcaaekLG8/ahaeTlkOf3yAnZ3VHfUgGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FphgHs%2FdJMcaaekLG8%2FahaeTlkOf3yAnZ3VHfUgGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;876&quot; height=&quot;613&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;③ Widget class명을 선택한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uHXaF/dJMcaayDB9V/c6yqtjp1oYiPUyus7N23k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uHXaF/dJMcaayDB9V/c6yqtjp1oYiPUyus7N23k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uHXaF/dJMcaayDB9V/c6yqtjp1oYiPUyus7N23k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuHXaF%2FdJMcaayDB9V%2Fc6yqtjp1oYiPUyus7N23k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;989&quot; height=&quot;620&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;④ 인터프리터를 PySide6을 선택한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFQgi8/dJMcaf0XwHc/KLIHwtj2YgkKAszoNR4BNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFQgi8/dJMcaf0XwHc/KLIHwtj2YgkKAszoNR4BNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFQgi8/dJMcaf0XwHc/KLIHwtj2YgkKAszoNR4BNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFQgi8%2FdJMcaf0XwHc%2FKLIHwtj2YgkKAszoNR4BNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;989&quot; height=&quot;620&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑤ Kits 선택하기 : 파이썬 버전을 선택한다. 나중에 파일분리 할 때&amp;nbsp; install 창이 뜨면 클릭하여 설치를&amp;nbsp; 적용할 수 있다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/61Bgl/dJMcadPz1Q0/7Nj5IS2C6kKQPzike2gIMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/61Bgl/dJMcadPz1Q0/7Nj5IS2C6kKQPzike2gIMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/61Bgl/dJMcadPz1Q0/7Nj5IS2C6kKQPzike2gIMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F61Bgl%2FdJMcadPz1Q0%2F7Nj5IS2C6kKQPzike2gIMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;989&quot; height=&quot;620&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑥ 하단 목록에서 추가할 파일 목록 확인 후 추가 없으면 None 선택 후 마침 (Finish)&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GYHSG/dJMb990OsTQ/mB8B8zCGs6QTSGNkgOYJKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GYHSG/dJMb990OsTQ/mB8B8zCGs6QTSGNkgOYJKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GYHSG/dJMb990OsTQ/mB8B8zCGs6QTSGNkgOYJKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGYHSG%2FdJMb990OsTQ%2FmB8B8zCGs6QTSGNkgOYJKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;973&quot; height=&quot;624&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑦ Widget.py 파일이 생성됨 : 이때 파이사이드 6에서 설치 버전이 필요하면 install 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oB0Dv/dJMcadouFOJ/bIsTTbFcqNaATQC6JMTk6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oB0Dv/dJMcadouFOJ/bIsTTbFcqNaATQC6JMTk6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oB0Dv/dJMcadouFOJ/bIsTTbFcqNaATQC6JMTk6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoB0Dv%2FdJMcadouFOJ%2FbIsTTbFcqNaATQC6JMTk6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1043&quot; height=&quot;707&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑧ 좌측에 Design 버튼 클릭하여 화면으로 이동해서 Layout 구성하자 (이때&lt;b&gt; &lt;u&gt;.ui 파일이 선택&lt;/u&gt;&lt;/b&gt;되어 있어야만 디자인 버튼이 활성화)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaDO9N/dJMcadB0G9N/UjB2p07p6uXUnGrkbP7Sq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaDO9N/dJMcadB0G9N/UjB2p07p6uXUnGrkbP7Sq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaDO9N/dJMcadB0G9N/UjB2p07p6uXUnGrkbP7Sq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaDO9N%2FdJMcadB0G9N%2FUjB2p07p6uXUnGrkbP7Sq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1043&quot; height=&quot;707&quot; data-origin-width=&quot;1043&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑨&amp;nbsp; Design Layout 화면 : 만들고 싶은 디자인을 좌측 버튼에서 마우스로 끌어와서 중앙 Layout 화면에 배치하여 구성한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oQr73/dJMcadvgn1F/odupkf70JrQc3v0lQxf73k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oQr73/dJMcadvgn1F/odupkf70JrQc3v0lQxf73k/img.png&quot; data-alt=&quot;디자인 선택 화면 (form.ui@PySide Widgets)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oQr73/dJMcadvgn1F/odupkf70JrQc3v0lQxf73k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoQr73%2FdJMcadvgn1F%2Fodupkf70JrQc3v0lQxf73k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1064&quot; height=&quot;960&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디자인 선택 화면 (form.ui@PySide Widgets)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgUYf/dJMcafNvueG/xRO9VEIKp20w71khbhxQRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgUYf/dJMcafNvueG/xRO9VEIKp20w71khbhxQRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgUYf/dJMcafNvueG/xRO9VEIKp20w71khbhxQRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgUYf%2FdJMcafNvueG%2FxRO9VEIKp20w71khbhxQRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1091&quot; height=&quot;784&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;⑩&amp;nbsp; Layout에 디자인이 되었다면 좌측상단에 Edit를 클릭해서 Widget 화면으로 돌아간다.&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZkyDZ/dJMcabxu0sn/npwQBDfPKhRqeGdrtvrbRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZkyDZ/dJMcabxu0sn/npwQBDfPKhRqeGdrtvrbRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZkyDZ/dJMcabxu0sn/npwQBDfPKhRqeGdrtvrbRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZkyDZ%2FdJMcabxu0sn%2FnpwQBDfPKhRqeGdrtvrbRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1091&quot; height=&quot;784&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5.2. &amp;nbsp;Widget.py 에서 main.py 파일을 분리하여 만들기&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;① 메인파일 만들기 :&amp;nbsp; Qt 메뉴창에서 File -- New file (ctrl + n) 선택한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbdI3i/dJMcaiQVfhm/Uq4oOtxQLpV4IVy1UCwojk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbdI3i/dJMcaiQVfhm/Uq4oOtxQLpV4IVy1UCwojk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbdI3i/dJMcaiQVfhm/Uq4oOtxQLpV4IVy1UCwojk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbdI3i%2FdJMcaiQVfhm%2FUq4oOtxQLpV4IVy1UCwojk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1071&quot; height=&quot;783&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;② main.py 파일일 생성 후 분리하기 : 메인 파일 선택 후 화면과 같이 Split Side by Side 선택한다. 그러면 파일이 분리된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRPLLr/dJMb990Os4z/QOu7iKbYucODGOeFZ3MoL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRPLLr/dJMb990Os4z/QOu7iKbYucODGOeFZ3MoL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRPLLr/dJMb990Os4z/QOu7iKbYucODGOeFZ3MoL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRPLLr%2FdJMb990Os4z%2FQOu7iKbYucODGOeFZ3MoL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1085&quot; height=&quot;754&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;③ Widget.py와 main.py 분리하기 : 위젯 파일에서 메인파이의 코드만 잘라내어 붙인 후 좌하단의 모니터를 실행시켜 본다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1ndR/dJMcaayDEfi/6eqedk8hxzp0ojzTmI1DW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1ndR/dJMcaayDEfi/6eqedk8hxzp0ojzTmI1DW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1ndR/dJMcaayDEfi/6eqedk8hxzp0ojzTmI1DW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1ndR%2FdJMcaayDEfi%2F6eqedk8hxzp0ojzTmI1DW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1070&quot; height=&quot;954&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;④ 실행시켰을 때 &quot;애러&quot; 메시지가 발생한다면 main.py에서 문법에 맞게 코드를 수정해 주어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNMBT/dJMcacpGqNn/GwzbP9a4mTa7hewiJSdK30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNMBT/dJMcacpGqNn/GwzbP9a4mTa7hewiJSdK30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNMBT/dJMcacpGqNn/GwzbP9a4mTa7hewiJSdK30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNMBT%2FdJMcacpGqNn%2FGwzbP9a4mTa7hewiJSdK30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1038&quot; height=&quot;847&quot; data-origin-width=&quot;1038&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑤ Widget.py 와 main.py가 분리되었으니 모두 저장(Save All) 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dp5OUW/dJMb99T2kI8/52uXIDdZHBKdQHkVIPpt9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dp5OUW/dJMb99T2kI8/52uXIDdZHBKdQHkVIPpt9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dp5OUW/dJMb99T2kI8/52uXIDdZHBKdQHkVIPpt9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdp5OUW%2FdJMb99T2kI8%2F52uXIDdZHBKdQHkVIPpt9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;683&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;⑥ main.py를 선택하고 좌하단의 ▷ 실행버튼을 클릭하면 아래 화면과 같이 윈도우 Widget가 실행되었다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; 이것은 윈도우 웹브라우저에서 화면의 Layout를 표현해 준다.&amp;nbsp;이때 from &lt;u&gt;파일경로 꼭 확인&lt;/u&gt; import *&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgLuHi/dJMcacb7REC/lih36DHvWKKi8qbqRXmhu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgLuHi/dJMcacb7REC/lih36DHvWKKi8qbqRXmhu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgLuHi/dJMcacb7REC/lih36DHvWKKi8qbqRXmhu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgLuHi%2FdJMcacb7REC%2Flih36DHvWKKi8qbqRXmhu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1212&quot; height=&quot;954&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;5.2.1. 실습 간 오류 수정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클래스 정의 추가 : &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;Ui_Widget&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;은 디자인 틀일 뿐이며, 이를 화면에 띄우려면 &lt;/span&gt;&lt;span style=&quot;color: #188038;&quot;&gt;QWidget&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 &lt;i&gt;&lt;b&gt;상속받은 실제 클래스가 필요하다&lt;/b&gt;&lt;/i&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #188038;&quot;&gt;setupUi(self)&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 호출&lt;/b&gt;: 이 메서드를 실행해야만 Qt Designer에서 배치한 버튼, 텍스트 상자 등의 컴포넌트가 화면에 실제로 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인스턴스 접근 : 추후 버튼 클릭 이벤트 등을 연결할 때는 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #188038;&quot;&gt;self.ui.버튼이름&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 형태로 접근하게 된다&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# This Python file uses the following encoding: utf-8
import sys

from PySide6.QtWidgets import QApplication, QWidget

# Important:
# You need to run the following command to generate the ui_form.py file
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pyside6-uic form.ui -o ui_form.py, or
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pyside2-uic form.ui -o ui_form.py
from ui_form import Ui_Widget

class Widget(QWidget):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self, parent=None):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__(parent)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ui = Ui_Widget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.ui.setupUi(self)


if __name__ == &quot;__main__&quot;:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app = QApplication(sys.argv)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;widget = Widget()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;widget.show()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sys.exit(app.exec())&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ㅇ 교육 후기&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; - 파이썬과 C++ 언어의 수많은 코드들을 작성하고 다 외울수는 없으나, 이제 이해하고 해석은 할 수 있겠다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; class의 사용이 다양한&amp;nbsp; 환경과 프로그램에서 사용되듯이 대다수 많은 반복되는 프로그램 환경들이 그렇겠다는 생각이 든다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; 처음에는 낯선 용어들이 점차 익숙해지고, 복잡한 UI 환경이 조금씩 이해되면서 우리가 흔히 사용하고 있는 인터넷 웹브라우저 환경, 응용프로그램 들이 모두 유사한 코드로 구현되고 있겠구나 생각이 든다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; - &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;웹브라우저 화면을 이해하고 구현하는데 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파이참, Qt 환경에서 class,&amp;nbsp; 모듈, 라이브러리, Signal, Slot, Layout를 복합적으로 활용할 수 있다면 교육의 성과는 있었다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; - 비전공자로서 각종 함수 문법을 잘 작성하지는 못해도 찾아보고 이해하는 데는 많은 도움이 되었다. 앞으로 어떤 환경에서 프로그램 개발을 위한 일원이 될지 알 수 없지만 &quot;도전은 계속된다.&quot;&lt;/span&gt;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/43</guid>
      <comments>https://goodjop79.tistory.com/43#entry43comment</comments>
      <pubDate>Thu, 21 May 2026 19:38:16 +0900</pubDate>
    </item>
    <item>
      <title>D+31 [아두이노] Arduino 환경에서 조건문, 함수 활용한 제어 실습</title>
      <link>https://goodjop79.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 20일(수)&lt;/b&gt;&lt;br&gt;&lt;b&gt;ㅇ 아두이노 환경에서 조건문과 함수 활용한 제어 실습&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1.&amp;nbsp; 반복문으로 디지털 반딧불이 만들기 (교재 222P)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [반복문으로 디지털 반딧불이 만들기]교재224p

void setup() 
{
&amp;nbsp;&amp;nbsp;pinMode(11,OUTPUT);
&amp;nbsp;&amp;nbsp;pinMode(10,OUTPUT);
&amp;nbsp;&amp;nbsp;pinMode(9,OUTPUT);
}

void loop() 
{&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;int i = random(3);&amp;nbsp;&amp;nbsp; // 임의의 수 반환 (0~(N-1)
&amp;nbsp;&amp;nbsp;int pinNumber = i+9;&amp;nbsp;&amp;nbsp;// PWM을 지원하는 핀 번호 가져오기
&amp;nbsp;&amp;nbsp;int intensity = 0;

&amp;nbsp;&amp;nbsp;while (intensity &amp;lt;= 255) {&amp;nbsp;&amp;nbsp; // 서서히 밝아지게 만들기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;analogWrite(pinNumber, intensity++);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay(10);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;while (intensity &amp;gt;=0) {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 서서히 어두워지게 만들기 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;analogWrite(pinNumber, intensity--);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay(10);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;841&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kSSPv/dJMcabxuaYF/yZsehr3KDOAMsTHbUQc6mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kSSPv/dJMcabxuaYF/yZsehr3KDOAMsTHbUQc6mK/img.png&quot; data-alt=&quot;디지털 반딧물 만들기 (아두이노, 회로 팅거링 화면)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kSSPv/dJMcabxuaYF/yZsehr3KDOAMsTHbUQc6mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkSSPv%2FdJMcabxuaYF%2FyZsehr3KDOAMsTHbUQc6mK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;841&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;841&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디지털 반딧물 만들기 (아두이노, 회로 팅거링 화면)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2.&amp;nbsp; DC 모터의 방향 제어하기 (237P)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [DC 모터의 방향 제어하기] 교재237P
// DC모터는 기본 시계방향으로 회전하는데 단자에 연결된 선의 위치를 바꿔서, 
// ,모터의 회전을 반시계 방향으로 바꾸어서 제어하기

void setup() 
{
&amp;nbsp;&amp;nbsp;// PWM을 지원하는 디지털 9번, 10번 핀을 출력 모드로 설정
&amp;nbsp;&amp;nbsp;pinMode(9, OUTPUT);
&amp;nbsp;&amp;nbsp;pinMode(10, OUTPUT);

 // 스위치의 입력을 받기 위해 디지털 8번 핀을 입력 모드로 설정
&amp;nbsp;&amp;nbsp;pinMode(8, INPUT);
}

void loop() 
{
&amp;nbsp;&amp;nbsp;// 가변저항의 입력값 범위를 map() 함수로 변환
&amp;nbsp;&amp;nbsp;int inputValue = analogRead(A0);
&amp;nbsp;&amp;nbsp;int convertedValue = map(inputValue, 0, 1023, 0, 255);

&amp;nbsp;&amp;nbsp;//스위치의 입력값에 따라 DC 모터의 방향을 제어
&amp;nbsp;&amp;nbsp;int inputSwitch = digitalRead(8);
&amp;nbsp;&amp;nbsp;if (inputSwitch == LOW) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;analogWrite(9, convertedValue);&amp;nbsp;&amp;nbsp;// LOW값을 읽으면 9번 핀에 전원을 공급
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;analogWrite(10, 0);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;}
else {
&amp;nbsp;&amp;nbsp;analogWrite(9, 0);
&amp;nbsp;&amp;nbsp;analogWrite(10, convertedValue);&amp;nbsp;&amp;nbsp;// LOW 값을 읽지 않으면 10번 핀에 전원 공급
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;877&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6HTZB/dJMcabxua1J/BfJz6qcLumuPfHDsQUG8cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6HTZB/dJMcabxua1J/BfJz6qcLumuPfHDsQUG8cK/img.png&quot; data-alt=&quot;모터방향 제어하기 (아두이노, 회로 팅거링 화면)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6HTZB/dJMcabxua1J/BfJz6qcLumuPfHDsQUG8cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6HTZB%2FdJMcabxua1J%2FBfJz6qcLumuPfHDsQUG8cK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1844&quot; height=&quot;877&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;877&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모터방향 제어하기 (아두이노, 회로 팅거링 화면)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3.&amp;nbsp; 초음파 센서로 거리 측정하기 (249P)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [물체 사이의 거리에 반응하는 LED 만들기] 249p

void setup() 
{
&amp;nbsp;&amp;nbsp;pinMode(7, OUTPUT);
&amp;nbsp;&amp;nbsp;pinMode(6, OUTPUT);
&amp;nbsp;&amp;nbsp;pinMode(5, OUTPUT);
}
&amp;nbsp;&amp;nbsp;
void loop() 
{
 // 초음파 신호 송신
 
&amp;nbsp;&amp;nbsp;pinMode(9, OUTPUT);
&amp;nbsp;&amp;nbsp;digitalWrite(9, LOW);
&amp;nbsp;&amp;nbsp;delayMicroseconds(2);
&amp;nbsp;&amp;nbsp;digitalWrite(9, HIGH);
&amp;nbsp;&amp;nbsp;delayMicroseconds(5);
&amp;nbsp;&amp;nbsp;digitalWrite(9, LOW);

// 초음파 신호 수신

&amp;nbsp;&amp;nbsp;pinMode(9, INPUT);
&amp;nbsp;&amp;nbsp;double duration= pulseIn(9, HIGH);
&amp;nbsp;&amp;nbsp;double cm = duration * 340 / 10000 / 2;

// 물체의 측정 거리에 따라 LED 제어

&amp;nbsp;&amp;nbsp;if (cm &amp;lt; 20) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(7, HIGH);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(6, LOW);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(5, LOW);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;else if (cm &amp;lt; 60) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(7, LOW);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(6, HIGH);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(5, LOW);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(7, LOW);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(6, LOW);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;digitalWrite(5, HIGH);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;delay(100);
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1805&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGtoY/dJMcahRZUHk/jMHi9Mk5j22Vhw0rqeb2Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGtoY/dJMcahRZUHk/jMHi9Mk5j22Vhw0rqeb2Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGtoY/dJMcahRZUHk/jMHi9Mk5j22Vhw0rqeb2Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGtoY%2FdJMcahRZUHk%2FjMHi9Mk5j22Vhw0rqeb2Fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1805&quot; height=&quot;896&quot; data-origin-width=&quot;1805&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;4. 네오픽셀 다루기 (교재397p)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [네오픽셀 다루기 1] 교재397&amp;nbsp;&amp;nbsp;

// 가. 네오픽셀 라이브러리 설치하기 (툴,라이브러리관리자 - 검색어 NeoPixel - Adafruit NeoPixel 선택후 설치)
// 나. 회로 구성하기 (구성요소 기본 - 검색어 neo - 구성요소에서 활용)
// 다. 스케치 코드 작성&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;라. 스케치 코드읽기

# include &amp;lt;Adafruit_NeoPixel.h&amp;gt;

#define PIN&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 제어를 위한 디지털 핀 번호
#define NUMPIXELS&amp;nbsp;&amp;nbsp;1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 네오픽셀의 LED 수

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
&amp;nbsp;&amp;nbsp;// 네오픽셀 라이브러리 초기화
&amp;nbsp;&amp;nbsp;pixels.begin();
&amp;nbsp;&amp;nbsp;
}

void loop() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setColor();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay(100);
}

// 임의의 RGB 색상을 변경하는 함수
void setColor() {
&amp;nbsp;&amp;nbsp;int redColor = random(0, 256);
&amp;nbsp;&amp;nbsp;int greenColor = random(0, 256);
&amp;nbsp;&amp;nbsp;int blueColor = random(0, 256);
&amp;nbsp;&amp;nbsp;pixels.setPixelColor(0, pixels.Color(redColor,greenColor,blueColor));
&amp;nbsp;&amp;nbsp;pixels.show();&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2pAj0/dJMcaaSU2uE/QuRoRzd8TxVMckyuuNxvy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2pAj0/dJMcaaSU2uE/QuRoRzd8TxVMckyuuNxvy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2pAj0/dJMcaaSU2uE/QuRoRzd8TxVMckyuuNxvy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2pAj0%2FdJMcaaSU2uE%2FQuRoRzd8TxVMckyuuNxvy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1822&quot; height=&quot;838&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;5. 네오픽셀 다루기 2 (교재 400p)&lt;/b&gt;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// [스틱형 네오픽셀 다루기 2] 교재400&amp;nbsp;&amp;nbsp;

// 가. 네오픽셀 라이브러리 설치하기 (툴,라이브러리관리자 - 검색어 NeoPixel - Adafruit NeoPixel 선택후 설치)
// 나. 회로 구성하기 (구성요소 기본 - 검색어 neo - 구성요소에서 활용)
// 다. 스케치 코드 작성&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;라. 스케치 코드읽기

# include &amp;lt;Adafruit_NeoPixel.h&amp;gt;

#define PIN&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 제어를 위한 디지털 핀 번호
#define NUMPIXELS&amp;nbsp;&amp;nbsp; 4&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 네오픽셀의 LED 수

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

void setup() {
&amp;nbsp;&amp;nbsp;// 네오픽셀 라이브러리 초기화
&amp;nbsp;&amp;nbsp;pixels.begin();
&amp;nbsp;&amp;nbsp;
}

void loop() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;setColor();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;delay(100);
}

// 임의의 RGB 색상을 변경하는 함수
void setColor() {

&amp;nbsp;&amp;nbsp;for (int i=0; i&amp;lt;4; i++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int redColor = random(0, 256);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int greenColor = random(0, 256);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int blueColor = random(0, 256);
&amp;nbsp;&amp;nbsp;pixels.setPixelColor(0, pixels.Color(redColor,greenColor,blueColor));
&amp;nbsp;&amp;nbsp;pixels.show();&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMKrAV/dJMcajvwUlT/23dlUzEtIhvh6d90W4alPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMKrAV/dJMcajvwUlT/23dlUzEtIhvh6d90W4alPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMKrAV/dJMcajvwUlT/23dlUzEtIhvh6d90W4alPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMKrAV%2FdJMcajvwUlT%2F23dlUzEtIhvh6d90W4alPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1831&quot; height=&quot;894&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;6. 실습 후기&lt;/b&gt;&lt;br&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;&lt;/b&gt;아두이노 환경에서 실습간&amp;nbsp; 포트설정이 잘못되어 애러가 잠시 있었고, 보드를 연결하여 실습하였다. 물론 보드 구성품이 교재와&amp;nbsp; 다르거나 완전하지 않아서 시뮬레이션으로 몇가지는 진행했다. 잘되는 부분도 있었지만 '모터방향 제어하기' 부분은 시뮬레이션만으로는 확인이 제한되었다. 각각의 제품 라이브러리 마다 상이한 경우 해당 버젼 파일을 모두 설치해 주어야 하는 번거로움도 있었다.&lt;br&gt;&amp;nbsp; 조건문과 함수, C언어를 사용하여 구현하는 부분에서는 다소 애로사항이 있어서 교재와 웹브라우저를 활용, 찾아보면서 이해하려고 하였다.&lt;br&gt;&amp;nbsp; 그러나 여러가지 실습을 통해 작동원리 및 산업현장에서 기계와 프로그램이 연동되어 어떻게 작동하는지 원리를 조금은 이해할 수 있게 되었다. &quot; 도전은 계속된다&quot;&lt;/p&gt;</description>
      <category>학습 로그/Arduino</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/42</guid>
      <comments>https://goodjop79.tistory.com/42#entry42comment</comments>
      <pubDate>Wed, 20 May 2026 20:22:47 +0900</pubDate>
    </item>
    <item>
      <title>D+30 [아두이노] AI 시대 Arduino 기업에서 활용 어떻게 해?</title>
      <link>https://goodjop79.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;☆ 일자 : 2026년 5월 19일(화)&amp;nbsp;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;009&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/009.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;교육 30일 차가 되었다. 그동안 컴퓨터는 전공해 본 적이 없는 비전공자로써 공부습관을 들이는 게 먼저였고,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;낯선 컴퓨터 용어와 코딩 소스코드들을 작성하면서 이해하기 바빴다.&amp;nbsp; PC 저장장치 교체, 리눅스 환경 설정,&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;우분투 환경에서 파이참 설치 및 UI 설정, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;개인 블로그 개설 및 학습로그 기록, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;팀 프로젝트, 클래스 이해, &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;모듈&amp;amp;패키지&amp;amp;라이브러리 이해 및 활용, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;아두이노까지 바쁘게 시간은 흘러갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;그럼&lt;b&gt; AI 시대에 아두이노를 기업에서는 어떻게 활용해야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 사물인터넷(IoT : Internet of Things)&lt;/b&gt;에 사용할 기기, 즉 스마트 디바이스를 만들기 위해 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;소형 컴퓨터 장치와 &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;3D 프린팅 기술 등이 발달하면서 메이커들도 자기만의 스마트 디바이스를 만들기 시작했는데&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;이것이 오픈소스 하드웨어인 &lt;b&gt;아두이노(Arduino)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-streaming-container=&quot;&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;aEjgbf_45&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-tr-rsts=&quot;false&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-scope-id=&quot;turn&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-subtree=&quot;aimc&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIAhAA&quot; data-ved=&quot;2ahUKEwj_2IfPk8WUAxWTf_UHHX2FMpwQ2O0OegYIAAgCEAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;FGTBUe_3&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-ved=&quot;2ahUKEwj_2IfPk8WUAxWTf_UHHX2FMpwQ3KYQegYIAAgCEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-xid=&quot;VpUvz&quot; data-container-id=&quot;main-col&quot;&gt;
&lt;blockquote data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-root=&quot;c&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #1b711d;&quot;&gt;AI 시대 아두이노(Arduino)의 효율성&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;강력한 엣지 컴퓨팅 구현&lt;/b&gt;&lt;br /&gt;TinyML(초소형 머신러닝)의 발전, 아두이노 보드 자체에서 가벼운 AI 모델을 직접 실행할 수 있어 데이터 처리 속도가 빠르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초고속 프로토타이핑&lt;/b&gt;&lt;br /&gt;Gemini, ChatGPT, Claude 같은 생성형 AI에게 명령하면 아두이노 소스 코드(C++)와 회로도를 몇 초 만에 짜주므로 개발 시간이 대폭 단축된다. 그러나 코딩의 코드 이해가 선행되어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지능형 IoT 허브 역할&lt;/b&gt;&lt;br /&gt;아두이노가 수집한 환경 데이터를 클라우드 AI로 보내고, AI의 분석 결과를 받아 기계를 정밀 제어하는 가성비 좋은 '손발' 역할을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;≫&amp;nbsp; AI 시대의 아두이노는 복잡한 코딩과 시행착오를 AI가 대신 해결해 주기 때문에 과거보다 개발 효율성이 향상되었다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 500; margin: 0px 8px 0px 0px; text-decoration: none; border-bottom: 0px rgb(86, 89, 94);&quot; data-processed=&quot;true&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 500; margin: 0px 8px 0px 0px; text-decoration: none; border-bottom: 0px rgb(86, 89, 94);&quot; data-processed=&quot;true&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인터넷에 없는 독립형 AI 기계&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 24px 0px 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-tr-rsts=&quot;false&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-streaming-container=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-scope-id=&quot;turn&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-subtree=&quot;aimc&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIARAA&quot; data-ved=&quot;2ahUKEwiIkO_yk8WUAxVZgq8BHV8rJjkQ2O0OegYIAAgBEAA&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;gRbR0e_3&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-ved=&quot;2ahUKEwiIkO_yk8WUAxVZgq8BHV8rJjkQ3KYQegYIAAgBEAE&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-xid=&quot;VpUvz&quot; data-container-id=&quot;main-col&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;부품 선택&lt;/b&gt;&lt;br /&gt;일반 아두이노 Uno 대신 AI 연산이 가능한 고성능 프로세서와 카메라, 마이크가 내장된 &lt;b&gt;Arduino Nano 33 BLE Sense&lt;/b&gt; 또는 &lt;b&gt;ESP32-S3&lt;/b&gt; 보드를 주로 사용한다.&amp;nbsp; 물론 개발자 자신에게 맞는 최적화된 개발도구가 우선이다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;개발 도구&lt;/b&gt;&lt;br /&gt;&lt;u&gt;&lt;b&gt;Edge Impulse&lt;/b&gt;&lt;/u&gt; 같은 무료 TinyML 플랫폼을 사용하면 데이터 수집, AI 모델 학습, 아두이노 코드 변환까지 코딩 없이 마우스 클릭만으로 구현할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 장점&lt;/b&gt;&lt;br /&gt;인터넷 연결이 필요 없어 &lt;b&gt;보안성&lt;/b&gt;이 뛰어나고, 데이터 전송 지연이 없는 &lt;b&gt;실시간 제어&lt;/b&gt; 가능, 통신 부품이 빠져 &lt;b&gt;소형화와 전력 절감&lt;/b&gt;에 유리하다.&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-streaming-container=&quot;&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;Pvhddd_45&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-tr-rsts=&quot;false&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-scope-id=&quot;turn&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-subtree=&quot;aimc&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIARAC&quot; data-ved=&quot;2ahUKEwisoP3tqsWUAxWsr1YBHfA1MZsQ2O0OegYIAAgBEAI&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;WpH0m_e&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-ved=&quot;2ahUKEwisoP3tqsWUAxWsr1YBHfA1MZsQ3KYQegYIAAgBEAM&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-xid=&quot;VpUvz&quot; data-container-id=&quot;main-col&quot;&gt;
&lt;blockquote data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAhAA&quot; data-sfc-root=&quot;c&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;&lt;u&gt;제조업의 AI 도입 트렌드 :&amp;nbsp; 엣지 AI가 바꾸는 공장의 미래&lt;/u&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAxAA&quot; data-sfc-root=&quot;c&quot; data-ke-size=&quot;size16&quot;&gt;인플레이션으로 인한 비용 상승과 노동력 부족에 직면한 제조업계가 '엣지 AI(Edge AI)'를 돌파구로 삼고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAxAA&quot; data-sfc-root=&quot;c&quot; data-ke-size=&quot;size16&quot;&gt;데이터가 생성되는 현장에서 실시간으로 지능형 처리를 수행하는 엣지 AI는 스마트 제조의 핵심 기반으로 자리 잡고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAxAA&quot; data-sfc-root=&quot;c&quot; data-ke-size=&quot;size16&quot;&gt;글로벌 리더들을 대상으로 한 설문조사 결과를 바탕으로&lt;b&gt; &quot;제조업 AI 도입의 5가지 핵심 트렌드&quot;&lt;/b&gt;를 요약한 내용이다.&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIAxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;1. 생산 최적화와 품질 향상이 최우선 목표&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;핵심 영역&lt;/b&gt;: 제조업체들은 제조 공정(1위), 생산 품질(2위), 장비 관리(3위)를 AI 도입의 최우선 영역으로 꼽았다. (p. 3).&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBRAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;도입 동인&lt;/b&gt;: 일반 시장 기업은 '제품 품질(67%)'을, 기술 전문 기업들은 '혁신(56%)'을 가장 큰 도입 비즈니스 등 (p. 4). 품질을 유지하면서 더 빠르고 똑똑하게 생산하는 것이 핵심이다 (p. 3).&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;2. 현장 실시간 데이터 기반의 수익성 개선&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;실시간 대응&lt;/b&gt;: 클라우드를 거치지 않고 현장에서 실시간으로 데이터에 반응하여 사소한 문제가 커지기 전에 개입한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIBxAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;주요 측정 항목&lt;/b&gt;: 공장 내 온도, 공기 품질, 습도, 소음, 시각 이벤트(컴퓨터 비전) 등을 센서로 정밀하게 모니터링한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;3. 작업자 안전 및 신체 건강 모니터링 강화&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;안전 솔루션&lt;/b&gt;: 개인보호장비(PPE) 착용 여부 준수, 낙상 감지, 위험 구역 접근 제어 등에 AI가 적극적으로 도입되고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICRAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;신체 모니터링&lt;/b&gt;: 근무 중인 작업자의 체온, 심박수 등 신체적 건강 상태를 파악하여 현장 안전성을 높이고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;4. 성공의 걸림돌과 조직적 과제&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICxAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;최대 장벽&lt;/b&gt;: 예산 제약이 가장 큰 걸림돌이며, 기존 시스템과의 통합 어려움 및 내부 전문성 부족이 그 뒤를 이었다 (p. 6).&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICxAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;전문 조직 부재&lt;/b&gt;: 전담 AI 팀을 보유한 기업은 10명 중 1명에 불과, 여전히 CTO/CIO나 IT 부서가 겸임하는 구조가 많다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;5. 앞으로 주목해야 할 AI 도입 계획&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; 가. &lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span data-subtree=&quot;aimfl&quot; data-processed=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 600; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;내부 전문성 확보와 조직 체계화가 AI 도입의 핵심 성공 열쇠다 (pp. 2, 6).&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(1) &lt;b&gt;최대 걸림돌&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 예산 제약이 가장 큰 장벽이며, 기존 시스템과의 통합 어려움과&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내부 전문 인력 부족&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이 그 뒤를 이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(2) &lt;/span&gt;&lt;b&gt;전담 팀 부재&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: AI 전담 팀을 갖춘 기업은&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;10명 중 1명&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에 불과하며, 여전히 CTO, CIO, IT 부서가 겸임하고 있다. (p. 6).&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(3) &lt;/span&gt;&lt;b&gt;파트너십 요구&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot; data-sfc-cp=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cb=&quot;&quot; data-complete=&quot;true&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot;&gt;: 제조업체의&amp;nbsp;&lt;b&gt;70%&lt;/b&gt;는 자사뿐만 아니라&amp;nbsp;&lt;/span&gt;&lt;b&gt;공급망 파트너사들도 AI 프로세스를 도입&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;해야 한다고 강조한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(4) &lt;/span&gt;&lt;b&gt;성공 요인&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 복잡한 전체 머신러닝 프로세스(MLOps)를 이해하고 현장에 통합할 수 있는&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내부 도메인 전문가 육성&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이 필수적&lt;/span&gt;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDRAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&amp;nbsp; &amp;nbsp;** 향후 제조업체들이 도입할 가능성이 매우 높은 주요 기술 순위&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;예측 유지보수 (59%) (p. 7)&amp;nbsp;&lt;/span&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAB&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;배송 및 물류 (57%) (p. 7)&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAC&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;자산 및 재고 추적&amp;middot;관리 (55%) (p. 7)&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAD&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: Arial, sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(10, 10, 10);&quot; data-complete=&quot;true&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot;&gt;제품&amp;middot;직원 시각 검사 (55%) (p. 7)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/mFhCJ/dJMcacb6eKQ/y86tbmfqT0UojkQMmQBjfk/_%20%EC%A0%9C%EC%A1%B0%EC%97%85_AI%EB%8F%84%EC%9E%85_5%EB%8C%80_%ED%8A%B8%EB%A0%8C%EB%93%9C_%ED%95%9C%EA%B8%80%EB%B2%88%EC%97%AD.docx.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;_ 제조업_AI도입_5대_트렌드_한글번역.docx.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.41MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;☆ 결&amp;nbsp; 론&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 인공지능 응용시스템 개발자 양성교육 한 달째를 맞이하면서 프로그래밍의 기본 원리를 이해하고, AI를 적용하는 다양한 웹브라우저와 응용프로그램 들이 구현되는 과정을 이해하려고 한다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; 개발자, 기획자, PM, 빅데이터 분석가 등등 기업이 필요로 하고 사용자가 요구하는 프로그램을 구현하도록 하는 것이 이 과정의 취지가 아닐까 다시 한번 생각하게 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;AI 시대 아두이노(Arduino)를 기업에서 어떻게 활용할 것인지는 이익을 추구하는 기업에서는 투자 대비 이익을 고려하여 판단할 것이다.&amp;nbsp; &quot;배움과 활용의 도전은 계속된다.&quot;&lt;/p&gt;</description>
      <category>IT 컴퓨터</category>
      <category>AI #아두이노 #사물인터넷 #기업 #Edge Impulse</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/41</guid>
      <comments>https://goodjop79.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 19 May 2026 21:54:32 +0900</pubDate>
    </item>
    <item>
      <title>D+30 [아두이노] 시뮬레이터+키트 를 활용한 &amp;quot;실습&amp;quot;</title>
      <link>https://goodjop79.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 19일(화)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 내 용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 아두이노 회로란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 메인 보드(기판)와 다양한 센서, 액추에이터, &lt;u&gt;전자 부품들을 전선으로 연결하여 전기가 흐르도록 만든 통로&lt;/u&gt;이다. &lt;b&gt;아두이노 회로를 만들고 보드에 프로그래밍 코드를 입력하면, 외부 환경의 데이터를 읽거나 기계를 직접 움직이는 고유한 기능을 수행&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp;* 액추에이터(Actuator)는 전기, 공압, 유압 등의 에너지를 받아 기계적인 물리적 움직임으로 변환하는 구동장치로, AI나 제어장치(두뇌)의 명령을 받아 실제 동작을 수행하는 기계의 핵심 부품 1.1. 아두이노 회로를 이해하기 위한 핵심 개념&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 1.&amp;nbsp;아두이노&amp;nbsp;회로를&amp;nbsp;이해하기&amp;nbsp;위한&amp;nbsp;핵심&amp;nbsp;개념&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.8605%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심 개념&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.1395%; text-align: center;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내&amp;nbsp; &amp;nbsp; &amp;nbsp;용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.8605%; text-align: center;&quot;&gt;&lt;b&gt;전류의 흐름&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.1395%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전기는 수압이 높은 곳에서 낮은 곳으로 물이 흐르듯, 전압이 높은 곳(5V 또는 3.3V)에서 낮은 곳(GND, 0V)으로 흐른다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.8605%; text-align: center;&quot;&gt;&lt;b&gt;회로의 폐쇄성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.1395%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전기가 출발하여 아두이노 내부나 외부 소자를 거쳐 반드시 GND(그라운드) 핀으로 돌아와야만 회로가 정상 작동한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.8605%; text-align: center;&quot;&gt;&lt;b&gt;극성 주의&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.1395%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전원과 그라운드를 반대로 연결하는 역극성이 발생하면 부품이 타거나 고장 날 수 있어 주의해야 한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;[ tinkercad.com 회로 디자인 ]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YXFT4/dJMcabqKyKY/6QcLHPKfCxXMbM2Cn2xOcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YXFT4/dJMcabqKyKY/6QcLHPKfCxXMbM2Cn2xOcK/img.png&quot; data-alt=&quot;회로 디자인 만들기 (tinkercad)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YXFT4/dJMcabqKyKY/6QcLHPKfCxXMbM2Cn2xOcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYXFT4%2FdJMcabqKyKY%2F6QcLHPKfCxXMbM2Cn2xOcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1214&quot; height=&quot;589&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회로 디자인 만들기 (tinkercad)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tASXi/dJMcacQGICo/uHAqg4UujfDQRw0kREpEtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tASXi/dJMcacQGICo/uHAqg4UujfDQRw0kREpEtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tASXi/dJMcacQGICo/uHAqg4UujfDQRw0kREpEtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtASXi%2FdJMcacQGICo%2FuHAqg4UujfDQRw0kREpEtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;452&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 아두이노를 활용한 회로도 구성 및 제어 실습&lt;/b&gt;&lt;br /&gt;&lt;b&gt;2.1.&amp;nbsp; 슬라이드 스위치로 LED 제어하기 (교재 170P)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; - 새 회로 만들기 &amp;rarr; 스위치 배치하기 &amp;rarr;스케치 코드 입력 하기 &amp;rarr; 실행 결과 확인하기&lt;br /&gt;&amp;nbsp; &amp;nbsp; - 시리얼모니터(시뮬레이션 시작, 코드클릭, 우하단 참조)에 0 (low), 1 (high) 값이 출력 된다.&amp;nbsp;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779185791934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//[슬라이드 스위치로 LED 제어하기]
void setup() 
{
  Serial.begin(9600);   // 시리얼 최초 통신 초기화

  pinMode(8, OUTPUT);   // 8번 디지털 핀을 출력 모드로 설정
  pinMode(7, INPUT);    // 7번 디지털 핀을 입력 모드로 설정

}

 void loop()
 {
  int readValue = digitalRead(7);  // 입력 핀의 값을 읽어서 변수에 저장
  Serial.println(readValue);    // 변수 값을 시리얼 모니터에 출력

  if (readValue ==  HIGH) {    // 입력값에 따라 LED 출력값 제어
    digitalWrite(8,HIGH);   // 조건문이 참이라면 HIGH 실행
  }
  else {
    digitalWrite(8, LOW);   // 조건문이 거짓이라면 LOW 실행
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3image03.png&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kLRYW/dJMcaf0VSRM/KBH1wlh7HGnUoLtB9y0yh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kLRYW/dJMcaf0VSRM/KBH1wlh7HGnUoLtB9y0yh0/img.png&quot; data-alt=&quot;슬라이드 스위치 제어하기 (회로 및 코드)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kLRYW/dJMcaf0VSRM/KBH1wlh7HGnUoLtB9y0yh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkLRYW%2FdJMcaf0VSRM%2FKBH1wlh7HGnUoLtB9y0yh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1828&quot; height=&quot;790&quot; data-filename=&quot;3image03.png&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬라이드 스위치 제어하기 (회로 및 코드)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2. 2. 가변저항으로 LED 깜빡임 속도 조절하기 (교재 188P)&lt;/b&gt;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779185928350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [가변저항으로 LED 깜빡임 속도 조절하기]

void setup() 
{
  Serial.begin(9600);    // 직렬 통신 초기화
  
  pinMode(13, OUTPUT);  
}

 
void loop() 
{
  int readValue = digitalRead(A0);  // A0 핀에서 읽은 값을 readValue에 저장
  Serial.println(readValue);   

  digitalWrite(13, LOW);
  delay(readValue);         //readValue 값만큼 코드 샐행을 기다린다.
  digitalWrite(13, HIGH);  
  delay(readValue);         //readValue 값만큼 코드 실행을 기다린다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4image04.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0wImz/dJMcahRY5i3/wUdgDEGihNxKBAKxg8rh41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0wImz/dJMcahRY5i3/wUdgDEGihNxKBAKxg8rh41/img.png&quot; data-alt=&quot;가변저항 값 조정을 통한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0wImz/dJMcahRY5i3/wUdgDEGihNxKBAKxg8rh41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0wImz%2FdJMcahRY5i3%2FwUdgDEGihNxKBAKxg8rh41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1824&quot; height=&quot;786&quot; data-filename=&quot;4image04.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가변저항 값 조정을 통한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2.3. 조도 센서로 스마트 가로등 만들기 (교재 199P)&lt;/b&gt;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779186019930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [조도센서로 스마트 가로등 회로 만들기] 

void setup() 
{
  Serial.begin(9600);    // 시리얼 직렬 통신 초기화
  pinMode(13, OUTPUT);  // 디지털 13번 핀을 출력 모드로 설정(연결)한다.
}

 
void loop() 
{
  int photoresistor = analogRead(A0);  // 조도 센서값 측정
  Serial.println(photoresistor);   //  조도 센서값 출력

  if (photoresistor &amp;gt; 100)     // 조도 센서값이 100보다 크면 on 
    digitalWrite(13, HIGH);    // 어두울 때 가로등을 켠다
  else
    digitalWrite(13, LOW);    // 밝을 때 가로등을 끈다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5image05.png&quot; data-origin-width=&quot;1811&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdNGwp/dJMcaiwBBgn/csvuiDikoKK1Ks47hE7Fb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdNGwp/dJMcaiwBBgn/csvuiDikoKK1Ks47hE7Fb0/img.png&quot; data-alt=&quot;조도센서 값을 통한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdNGwp/dJMcaiwBBgn/csvuiDikoKK1Ks47hE7Fb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdNGwp%2FdJMcaiwBBgn%2FcsvuiDikoKK1Ks47hE7Fb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1811&quot; height=&quot;837&quot; data-filename=&quot;5image05.png&quot; data-origin-width=&quot;1811&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;조도센서 값을 통한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.4. 온도 센서로 온도계 만들기&amp;nbsp;&amp;nbsp;(교재 204P)&lt;/b&gt;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779186080018&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//[온도 센서로 온도계 만들기]

void setup() 
{
  Serial.begin(9600);   // 시리얼 통신 초기화  
}

void loop()
{
  int analogInput = analogRead(A0);  // 온도 센서(TMP36) 값 측정
  float voltage = analogInput*5.0/1023.0;  // 전압값 변환
  float temperature = voltage*100-50;     // 온도값 변환
  
  Serial.print(&quot;Temperature : &quot;);
  Serial.println(temperature);          // 온도값 출력
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6image06.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnC7kk/dJMb990MJUC/CsL2NIZHMQHwPkFbct0WEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnC7kk/dJMb990MJUC/CsL2NIZHMQHwPkFbct0WEK/img.png&quot; data-alt=&quot;온도 값 변화를 통한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnC7kk/dJMb990MJUC/CsL2NIZHMQHwPkFbct0WEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnC7kk%2FdJMb990MJUC%2FCsL2NIZHMQHwPkFbct0WEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1798&quot; height=&quot;742&quot; data-filename=&quot;6image06.png&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;온도 값 변화를 통한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2.5. 화재경보기 (교재 207P)&lt;/b&gt;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779186145049&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [화재 경보기 만들기]

const int sensorPin = A0; // TMP36 온도 센서 핀
const int ledPin = 13;    // LED 경보 핀

// 화재 경보를 울릴 기준 온도 (주변 환경에 따라 변경 가능)
const float FIRE_THRESHOLD = 40.0; 

void setup() {
  pinMode(ledPin, OUTPUT); // LED 핀을 출력으로 설정
  Serial.begin(9600);      // 모니터링을 위한 시리얼 통신 시작
}

void loop() {
  int sensorVal = analogRead(sensorPin);      // 센서 값 읽기
  float voltage = (sensorVal / 1023.0) * 5.0; // 전압 변환 (0V ~ 5V)
  float temperature = (voltage - 0.5) * 120.0; // 섭씨 온도 계산
  // 온도가 120℃를 넘으면 LED를 깜빡이는 화재경보기
  // 현재 온도 시리얼 모니터 출력
  Serial.print(&quot;Current Temp: &quot;);
  Serial.print(temperature);
  Serial.println(&quot; C&quot;);

  // 화재 경보 조건 검사
  if (temperature &amp;gt;= FIRE_THRESHOLD) {
    // 화재 발생! LED를 빠르게 깜빡여 경보를 알림
    digitalWrite(ledPin, HIGH);
    delay(50);
    digitalWrite(ledPin, LOW);
    delay(50);
  } else {
    // 정상 상태: LED 꺼짐
    digitalWrite(ledPin, LOW);
    delay(500); // 평상시에는 0.5초 간격으로 온도 체크
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7image07.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PIBxC/dJMcac4dxIQ/0aFNr6qPgHVcdxeAouuIok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PIBxC/dJMcac4dxIQ/0aFNr6qPgHVcdxeAouuIok/img.png&quot; data-alt=&quot;화재경보기 온도 설정값을 통한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PIBxC/dJMcac4dxIQ/0aFNr6qPgHVcdxeAouuIok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPIBxC%2FdJMcac4dxIQ%2F0aFNr6qPgHVcdxeAouuIok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1840&quot; height=&quot;844&quot; data-filename=&quot;7image07.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화재경보기 온도 설정값을 통한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2.6. 초음파 센서로 거리 측정하기 (교재 241p)&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;lt;&amp;gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1779186207656&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// [초음파 센서로 거리 측정하기]
// 초음파가 물체에 반사되어 돌아올 때까지의 '시간'을 측정함. 
// 초음파의 속력은 이미 정해져 있고 시간과 속력을 공식적용하여 물체 사이의 거리를 구한다.
// 초음파센서기는 사물의 위치에 따라 출력값이 달라진다.

void setup() {
  Serial.begin(9600);

}

void loop() {
  pinMode(9, OUTPUT);
  digitalWrite(9, LOW);
  delayMicroseconds(2);
  digitalWrite(9, HIGH);
  delayMicroseconds(5);
  digitalWrite(9, LOW);

  pinMode(9, INPUT);
  double duration= pulseIn(9, HIGH);
  double cm = duration * 340 / 10000 / 2;

  Serial.println(cm);
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 초음파 센서기를 마우스로 클릭하면 센서범위가 나타나며, 움직임에 따라 값이 변하면서 '시리얼모니터'에 변화값이 표시됨&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8image08.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R97DY/dJMcaf0VS5k/IoWlMPOQjKCvsfCNmc2fDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R97DY/dJMcaf0VS5k/IoWlMPOQjKCvsfCNmc2fDK/img.png&quot; data-alt=&quot;초음파가 물체에 도달후 반사되는 시간값을 통한 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R97DY/dJMcaf0VS5k/IoWlMPOQjKCvsfCNmc2fDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR97DY%2FdJMcaf0VS5k%2FIoWlMPOQjKCvsfCNmc2fDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1810&quot; height=&quot;789&quot; data-filename=&quot;8image08.png&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초음파가 물체에 도달후 반사되는 시간값을 통한 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝.&lt;/p&gt;</description>
      <category>학습 로그/Arduino</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/40</guid>
      <comments>https://goodjop79.tistory.com/40#entry40comment</comments>
      <pubDate>Tue, 19 May 2026 20:11:51 +0900</pubDate>
    </item>
    <item>
      <title>D+29 [아두이노]  PIR 센서로 현관등 만들기</title>
      <link>https://goodjop79.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 18일(월)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 주 제 : 아두이노를 활용한 사물인터넷 실습&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ㅇ 내 용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 사물인터넷( Iot : (I&lt;/b&gt;nternet of Things)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 각종 사물에 센서와 통신 기능을 내장하여 인터넷에 연결하는 기술 또는 이를 활용해&amp;nbsp;제공하는 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. PIR 센서로 현관등 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779072011055&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//[방법 1] 단순히 조건에 맞게 high, low 출력값만 제어 

void setup() 
{
  Serial.begin(9600);   // 직렬 통신 초기화

  pinMode(8, OUTPUT);   // 8번 디지털 핀을 출력 모드로 설정
  pinMode(7, INPUT);   // 7번 디지털 핀을 입력 모드로 설정
}

void loop() 
{
  int readValue = digitalRead(7);  // 입력 핀의 값을 읽어 변수에 저장
  Serial.println(readValue);     // 변수의 값을 시리얼 모니터에 출력

  if (readValue == HIGH) {      // 입력값에 따라 LED 출력값 제어

    digitalWrite(8,HIGH);
  }
  else {
    digitalWrite(8, LOW);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;그림1 아두이 노회 로및 코드스크린샷 2026-05-18 10-08-34.png&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHJGps/dJMcagS54lC/Qqrh3ISu2A9GIs5YVNaKf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHJGps/dJMcagS54lC/Qqrh3ISu2A9GIs5YVNaKf0/img.png&quot; data-alt=&quot;방법1. 단순하게 HIGH, LOW 값만 출력 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHJGps/dJMcagS54lC/Qqrh3ISu2A9GIs5YVNaKf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHJGps%2FdJMcagS54lC%2FQqrh3ISu2A9GIs5YVNaKf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1796&quot; height=&quot;982&quot; data-filename=&quot;그림1 아두이 노회 로및 코드스크린샷 2026-05-18 10-08-34.png&quot; data-origin-width=&quot;1796&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;방법1. 단순하게 HIGH, LOW 값만 출력 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1779072031278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//[방법 2] 단순히 5초 켜두고 다른 작업이 필요 없는 경우

void loop() 
{
  int readValue = digitalRead(7);  // 입력 핀의 값을 읽어 변수에 저장
  Serial.println(readValue);     // 변수의 값을 시리얼 모니터에 출력

  if (readValue == HIGH) {       // 신호가 감지되면
    digitalWrite(8, HIGH);       // 8번 핀을 켜고
    delay(5000);                 // 5초(5000ms) 동안 이 상태를 유지 (대기)
  }
  else {
    digitalWrite(8, LOW);        // 신호가 없으면 끔
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779072043518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//[방법 3.] 5초 동안에도 아두이노가 멈추지 않고 계속 작동해야 하는 경우

unsigned long turnOnTime = 0; // 켜진 시간을 저장할 변수

bool isTimerActive = false;   // 타이머가 작동 중인지 확인하는 변수

void loop() 
{
  int readValue = digitalRead(7);
  Serial.println(readValue);

  // 1. 신호가 감지되면 불을 켜고 타이머 시작
  if (readValue == HIGH) {
    digitalWrite(8, HIGH);
    turnOnTime = millis();    // 현재 아두이노가 켜진 후 지난 시간을 저장
    isTimerActive = true;
  }

  // 2. 타이머가 작동 중일 때, 5초가 지났는지 수시로 체크
  if (isTimerActive &amp;amp;&amp;amp; (millis() - turnOnTime &amp;gt;= 5000)) {
    digitalWrite(8, LOW);     // 5초가 지나면 불을 끔
    isTimerActive = false;    // 타이머 종료
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tinkercad 센서등 스크린샷 2026-05-18 10-46-57.png&quot; data-origin-width=&quot;1851&quot; data-origin-height=&quot;909&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YVM4k/dJMcabjYNYk/mCNsRpbCNYsuNuKTXxNgfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YVM4k/dJMcabjYNYk/mCNsRpbCNYsuNuKTXxNgfK/img.png&quot; data-alt=&quot;방법3. 아두이노, tinkercad.com/ 활용한 회로도 구성 및 코드 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YVM4k/dJMcabjYNYk/mCNsRpbCNYsuNuKTXxNgfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYVM4k%2FdJMcabjYNYk%2FmCNsRpbCNYsuNuKTXxNgfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1107&quot; height=&quot;544&quot; data-filename=&quot;tinkercad 센서등 스크린샷 2026-05-18 10-46-57.png&quot; data-origin-width=&quot;1851&quot; data-origin-height=&quot;909&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;방법3. 아두이노, tinkercad.com/ 활용한 회로도 구성 및 코드 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;3. 가변저항 값 측정하기&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.1. 측정하는 이유&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가변 저항&lt;span data-processed=&quot;true&quot; data-subtree=&quot;aimfl,mfl&quot;&gt;(Potentiometer)의 값을 측정하는 주된 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;회로의 안전성을 확인하고, 원하는 목표 전압/전류를 정확하게 제어 및&amp;nbsp; 보정(Calibration)하기 위함&lt;/b&gt;입니다.&lt;span data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;kvQSuc_k,kvQSuc_l&quot; data-sfc-root=&quot;c&quot;&gt;&lt;span data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;kvQSuc_k,kvQSuc_l&quot; data-sfc-root=&quot;c&quot;&gt;&lt;span data-processed=&quot;true&quot; data-wiz-attrbind=&quot;class=kvQSuc_j/TKHnVd&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; aria-hidden=&quot;true&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;kvQSuc_k,kvQSuc_l&quot; data-sfc-root=&quot;c&quot;&gt;&lt;span data-processed=&quot;true&quot; data-sfc-cb=&quot;&quot; data-wiz-uids=&quot;kvQSuc_k,kvQSuc_l&quot; data-sfc-root=&quot;c&quot;&gt;&lt;span data-processed=&quot;true&quot; data-wiz-attrbind=&quot;class=kvQSuc_j/TKHnVd&quot; data-animation-atomic=&quot;&quot;&gt;&lt;span data-processed=&quot;true&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 현재 저항값 확인 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다이얼(와이퍼) 위치에 따라 저항이 어떻게 변하는지 파악, 회로가 요구하는 적절한 저항 범위(&lt;span style=&quot;letter-spacing: 0px; caret-color: transparent;&quot;&gt;)에 있는지 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 최대 저항값 검증 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가변저항의 양 끝 단자를 측정해 해당 부품의 공칭 스펙&lt;span style=&quot;letter-spacing: 0px; caret-color: transparent;&quot;&gt;과 일치하는지, 혹은 부품 불량이나 단선이 없는지 점검&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 정밀한 전압 분배 :&lt;/b&gt; 가변저항은 전압 분배기 역할, 다이얼 조정에 따른 출력 전압 변화율(비례 관계)을 측정해 회로의 오작동 방지&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;저항값 측정스크린샷 2026-05-18 11-33-48.png&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IRxMw/dJMcabc77pB/u6UUMR7c0jVxqGkGn7Bin1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IRxMw/dJMcabc77pB/u6UUMR7c0jVxqGkGn7Bin1/img.png&quot; data-alt=&quot;가변저항 측정 회로구성 및 코드 실행 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IRxMw/dJMcabc77pB/u6UUMR7c0jVxqGkGn7Bin1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIRxMw%2FdJMcabc77pB%2Fu6UUMR7c0jVxqGkGn7Bin1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1854&quot; height=&quot;908&quot; data-filename=&quot;저항값 측정스크린샷 2026-05-18 11-33-48.png&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가변저항 측정 회로구성 및 코드 실행 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 교육 후기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;가. 회로도를 구성하여 코드를 생성하고, 아두이노 환경에서 실행해 본 결과 여러가지 상황을 가정했을 때 방법 1부터 3까지 차이를 확인해 보았다. 코드를 수정하고 회로를 조정하여 다양한 환경에서 사용자 요구조건을 수용할 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;나. 센서등이 사물의 움직임에 따라 반응하는 시간과 조건에 따른 실행 차이를 둠으로써 출력값에 따라 다양한 환경이 구현된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(예) 센서등이 사물을 감지했을때 계속 켜져 있거나, 켜진후 사물이 있어도 움직임을 감지하지 않으면 꺼지는 경우&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;다. 인터넷 사물에 센서와 통신 기능을 내장하여 인터넷에 연결하는 기술 또는 이를 활용해 제공하는 서비스는 인공지능(AI)와 결합해 더욱 진화하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; (예) 중국 : 거리의 모든 사람의 얼굴을 거리 카메라로 인공지능이 인식하여 분석해 낸 후 데이타들을 다방면으로 활용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;미국 :&amp;nbsp; 이란(중동)전에서 제거대상의 사물을 식별하여 선별타격 하는 인공지능 기술들을 활용하였다.&lt;/p&gt;</description>
      <category>학습 로그/Arduino</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/39</guid>
      <comments>https://goodjop79.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 18 May 2026 12:05:17 +0900</pubDate>
    </item>
    <item>
      <title>D+28 [PyQt] Qt Creator 활용 및 class 이해</title>
      <link>https://goodjop79.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ㅇ 일 자 : 2026년 5월 15일(금)&lt;br /&gt;ㅇ 주 제 :&amp;nbsp;&amp;nbsp;Qt Creator 활용과 클래스 이해&lt;br /&gt;ㅇ 내 용&lt;br /&gt;&amp;nbsp;&lt;br /&gt;1.&amp;nbsp; &quot;클래스(class)에&amp;nbsp;대한&amp;nbsp;이해&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;가.&amp;nbsp;외부&amp;nbsp;수정을&amp;nbsp;제한하기&amp;nbsp;위해&amp;nbsp;변수는&amp;nbsp;하나의&amp;nbsp;자료형으로&amp;nbsp;'캡슐화'한다.&amp;nbsp;상속을&amp;nbsp;받아서&amp;nbsp;확장해서&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;나.&amp;nbsp;클래스&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;원본에&amp;nbsp;영향을&amp;nbsp;주지&amp;nbsp;않는&amp;nbsp;객체지향&amp;nbsp;프로그래밍&amp;nbsp;핵심,&amp;nbsp;똑같은&amp;nbsp;모양의&amp;nbsp;객체를&amp;nbsp;만들어&amp;nbsp;내기&amp;nbsp;위한&amp;nbsp;설계도(빵틀)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;데이터(속성)과&amp;nbsp;함수(메서드)를&amp;nbsp;한데&amp;nbsp;묶는다&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;다.&amp;nbsp;상속(Inheritance):&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;상속이란&amp;nbsp;기존의&amp;nbsp;클래스를&amp;nbsp;재활용하여&amp;nbsp;새로운&amp;nbsp;클래스를&amp;nbsp;작성하는&amp;nbsp;자바의&amp;nbsp;문법&amp;nbsp;요소를&amp;nbsp;의미&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;상속은&amp;nbsp;클래스의&amp;nbsp;확장&amp;nbsp;개념이다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;예)&amp;nbsp;클래스1을&amp;nbsp;만들면&amp;nbsp;만약&amp;nbsp;중복되는&amp;nbsp;부분을&amp;nbsp;하나의&amp;nbsp;클래스로&amp;nbsp;만들어&amp;nbsp;놓고&amp;nbsp;여러개의&amp;nbsp;묶음을&amp;nbsp;생성할&amp;nbsp;수&amp;nbsp;있다&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;라.&amp;nbsp;추상화(Abstration):&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;클래스에서의&amp;nbsp;추상화는&amp;nbsp;모든&amp;nbsp;사물들을&amp;nbsp;클래스로&amp;nbsp;코드화를&amp;nbsp;해서&amp;nbsp;만드는&amp;nbsp;것&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;사물이나&amp;nbsp;표상을&amp;nbsp;어떤&amp;nbsp;성질,&amp;nbsp;공통성,&amp;nbsp;본질에&amp;nbsp;착안하여&amp;nbsp;그것을&amp;nbsp;추출하여&amp;nbsp;파악하는&amp;nbsp;것&amp;nbsp;(공통성과&amp;nbsp;본질을&amp;nbsp;모아&amp;nbsp;추출)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;예)지하철&amp;nbsp;노선도&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;마.&amp;nbsp;의존성의&amp;nbsp;문제&amp;nbsp;:&amp;nbsp;의지해야만&amp;nbsp;존재하는&amp;nbsp;코드,&amp;nbsp;클래스에&amp;nbsp;다른&amp;nbsp;클래스가&amp;nbsp;있어야만&amp;nbsp;존재하는&amp;nbsp;데&amp;nbsp;유지관리가&amp;nbsp;어려운&amp;nbsp;문제가&amp;nbsp;발생한다&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;위젯안에&amp;nbsp;위젯&amp;nbsp;동물들을&amp;nbsp;더&amp;nbsp;추가하고자&amp;nbsp;할때&amp;nbsp;의존성&amp;nbsp;문제로&amp;nbsp;애러가&amp;nbsp;발생한다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;위젯클래스&amp;nbsp;:&amp;nbsp;추가부분,&amp;nbsp;실행부분을&amp;nbsp;모두&amp;nbsp;수정해야&amp;nbsp;하는데&amp;nbsp;실행하는&amp;nbsp;부분까지&amp;nbsp;찾아가서&amp;nbsp;수정을&amp;nbsp;해야&amp;nbsp;하는데&amp;nbsp;여러명이&amp;nbsp;공동개발한&amp;nbsp;프로그램이라면&amp;nbsp;문제가&amp;nbsp;발생한다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;개발자1은&amp;nbsp;'소'를&amp;nbsp;추가하고,&amp;nbsp;개발자2는&amp;nbsp;'호랑이'를&amp;nbsp;추가했을&amp;nbsp;때&amp;nbsp;:&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;추가된&amp;nbsp;양과&amp;nbsp;소가&amp;nbsp;통합되어서&amp;nbsp;저장되야&amp;nbsp;하고,&amp;nbsp;동물원&amp;nbsp;클래스에는&amp;nbsp;양과&amp;nbsp;소가&amp;nbsp;있어야&amp;nbsp;하는데&amp;nbsp;동물원class가&amp;nbsp;서로&amp;nbsp;달라&amp;nbsp;누군가는&amp;nbsp;통합해야&amp;nbsp;하는&amp;nbsp;것이다.&amp;nbsp;근데&amp;nbsp;호랑이class를&amp;nbsp;더&amp;nbsp;추가해야&amp;nbsp;할&amp;nbsp;때&amp;nbsp;추가부분,&amp;nbsp;실행부분을&amp;nbsp;함께&amp;nbsp;self&amp;nbsp;수정해야&amp;nbsp;하는데&amp;nbsp;여러명이&amp;nbsp;동시에&amp;nbsp;수정하게&amp;nbsp;된다면&amp;nbsp;기존에&amp;nbsp;만들어진&amp;nbsp;실행부까지&amp;nbsp;찾아가서&amp;nbsp;수정해&amp;nbsp;주어야&amp;nbsp;하니&amp;nbsp;관리가&amp;nbsp;매우&amp;nbsp;어렵게&amp;nbsp;된다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100.0%; text-align: justify;&quot;&gt;&amp;nbsp;&amp;nbsp;예)&amp;nbsp;Dependency&amp;nbsp;Inversion&amp;nbsp;:&amp;nbsp;클래스안에&amp;nbsp;클래스가&amp;nbsp;존재해야지만&amp;nbsp;동작한다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;High&amp;nbsp;level(동물원class)&amp;nbsp;-&amp;gt;&amp;nbsp;Low&amp;nbsp;level&amp;nbsp;Module&amp;nbsp;-&amp;gt;&amp;nbsp;(강아지class,고양이class,&amp;nbsp;사자class,늑구...)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;High&amp;nbsp;level(동물원class)&amp;nbsp;-&amp;gt;&amp;nbsp;Animal(추상화)&amp;nbsp;-&amp;gt;&amp;nbsp;강아지class,고양이class,사자,늑구...)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;※&amp;nbsp;의존성&amp;nbsp;문제&amp;nbsp;해결방법&amp;nbsp;:&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ㅇ&amp;nbsp;추상화된&amp;nbsp;인터페이스를&amp;nbsp;생성한다.&amp;nbsp;(클래스1&amp;nbsp;---&amp;nbsp;인터페이스&amp;nbsp;&amp;nbsp;---&amp;nbsp;클래스2)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ㅇ&amp;nbsp;추상화라는&amp;nbsp;것을&amp;nbsp;사용하여&amp;nbsp;공통된&amp;nbsp;특성을&amp;nbsp;가진&amp;nbsp;코드들을&amp;nbsp;추상화&amp;nbsp;시킨다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;예)&amp;nbsp;사람얼굴&amp;nbsp;사진&amp;nbsp;--&amp;gt;&amp;nbsp;스케치&amp;nbsp;그림&amp;nbsp;--&amp;gt;&amp;nbsp;이모티콘&amp;nbsp;버튼&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ㅇ&amp;nbsp;클래스&amp;nbsp;추상화를&amp;nbsp;활용하여&amp;nbsp;외부라이브러리에서&amp;nbsp;실행부를&amp;nbsp;받아&amp;nbsp;사용해도&amp;nbsp;문제가&amp;nbsp;발행하지&amp;nbsp;않는다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;(예시&amp;nbsp;1)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;#&amp;nbsp;from&amp;nbsp;파일경로&amp;nbsp;import&amp;nbsp;class에&amp;nbsp;해당하는&amp;nbsp;코드를&amp;nbsp;하단에서&amp;nbsp;사용(class,ded함수&amp;nbsp;단위)&lt;br /&gt;#&amp;nbsp;외부&amp;nbsp;라이브러리에서&amp;nbsp;상속받은&amp;nbsp;클래스(QMainWindow...)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication, QMainWindow, QLabel

class MyWindow(QMainWindow):&amp;nbsp;&amp;nbsp; #생성자 객체가 만들어질때 실행되는 초기화 함수
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def __init__(self):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# def함수 추가 확장, class에 객체들 가져와서 사용 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;super().__init__()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setWindowTitle(&quot;QMainWindow Example&quot;)&amp;nbsp;&amp;nbsp;# self 속성 
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;label = QLabel(&quot;Hello QMainWindow!&quot;)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.setCentralWidget(label)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # self 속성
 
app = QApplication([])
window = MyWindow()
window.show()
app.exec()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;(예시 2)&lt;br /&gt;# tests/test_app.py&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import unittest&amp;nbsp;&amp;nbsp; 
from app import app&amp;nbsp;&amp;nbsp;# from 파일경로 import class에 해당하는 코드를 하단에서 사용(class,def 함수 단위)

# 클래스 변수는 하나의 자료형으로 캡슐화한다.&amp;nbsp;&amp;nbsp; 
class TestAppRoutes(unittest.TestCase):&amp;nbsp;&amp;nbsp; #생성자 객체가 만들어질때 실행되는 초기화 함수 

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def setUp(self):&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#추상화 된 &quot;&quot;인터페이스&quot;&quot;를 생성한다.&amp;nbsp;&amp;nbsp; 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.app = app.test_client()&amp;nbsp;&amp;nbsp;# self 의존성: 클래스에 다른 클래스가 있어야만 존재하는 것
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.app.testing = True

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def test_hello_route(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response = self.app.get('/')
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.status_code, 200)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.data.decode('utf-8'), &quot;Hello, Flask!&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def test_add_item_route(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response = self.app.post('/items', json={&quot;name&quot;: &quot;item1&quot;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.status_code, 201)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.get_json(), {'message': 'Item added successfully'})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def test_get_item_route(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response = self.app.get('/items/0')
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.status_code, 200)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.get_json(), {'item': {'name': 'item1'}})

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;def test_get_nonexistent_item_route(self):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response = self.app.get('/items/1')
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.status_code, 404)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;self.assertEqual(response.get_json(), {'error': 'Item not found'})

if __name__ == '__main__':
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;unittest.main()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#2.&amp;nbsp;Signal&amp;nbsp;&amp;amp;&amp;nbsp;Slot&amp;nbsp;구조&amp;nbsp;이해&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;가.&amp;nbsp;시그널&amp;nbsp;:&amp;nbsp;어떤&amp;nbsp;일이&amp;nbsp;발생했음을&amp;nbsp;알려주는&amp;nbsp;신호,&amp;nbsp;버튼&amp;nbsp;클릭시&amp;nbsp;시그널&amp;nbsp;발생한다&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;나.&amp;nbsp;슬롯&amp;nbsp;:&amp;nbsp;함수&amp;nbsp;혼자서는&amp;nbsp;자동실행이&amp;nbsp;되지&amp;nbsp;않는다.&amp;nbsp;시그널과&amp;nbsp;연결(connect)되서&amp;nbsp;버튼&amp;nbsp;클릭시&amp;nbsp;실행된다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;button.clicked.connect(button_clickde)&amp;nbsp;&amp;nbsp;#&amp;nbsp;버튼&amp;nbsp;클릭해서&amp;nbsp;신호가&amp;nbsp;발생하면,&amp;nbsp;함수가&amp;nbsp;자동실행된다.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp;(예시1)&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from PySide6.QtWidgets import QApplication, QPushButton

# [슬롯으로 사용할 함수] : 버튼 클릭(signal)을 받으면 실행될 함수
def button_clicked():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(&quot;버튼을 클릭하셨네요!&quot;)
# [1] Qt 애플리케이션 생성
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 모든 Qt 프로그램은 QApplication 객체가 반드시 하나 필요함
app = QApplication()

# [2] 버튼 위젯 생성
button = QPushButton(&quot;Press Me&quot;)&amp;nbsp;&amp;nbsp; # QApplication 위 버튼에 표시될 텍스트

# [3] 시그널과 슬롯 연결
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - QPushButton은 기본적으로 'clicked' Signal 이 구현되어 있음
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - 사용자가 버튼을 클릭하면 clicked Signal 이 자동으로 발생(emit)
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - connect()를 통해 clicked &amp;rarr; button_clicked 함수(슬롯) 연결
button.clicked.connect(button_clicked)

# [4] 버튼 화면에 표시
button.show()

# [5] 이벤트 루프 실행
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - GUI 프로그램이 종료될 때까지 사용자 입력(클릭, 키보드 등)을 처리함
app.exec()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(예시2)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import QtQuick
import QtQuick.Window

Window {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;width: 640
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;height: 480
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;visible: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title: qsTr(&quot;Hello World&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;☆파이썬에서 버튼 만들기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;637&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uLquq/dJMcacXFPIS/h9yS17TLzStZlsrt5kKxqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uLquq/dJMcacXFPIS/h9yS17TLzStZlsrt5kKxqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uLquq/dJMcacXFPIS/h9yS17TLzStZlsrt5kKxqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuLquq%2FdJMcacXFPIS%2Fh9yS17TLzStZlsrt5kKxqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;637&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;637&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;☆ Qt에서 디자인 활용하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPqBDu/dJMcabYxLpx/FvaX1gfzK9EhlsUhQRRI1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPqBDu/dJMcabYxLpx/FvaX1gfzK9EhlsUhQRRI1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPqBDu/dJMcabYxLpx/FvaX1gfzK9EhlsUhQRRI1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPqBDu%2FdJMcabYxLpx%2FFvaX1gfzK9EhlsUhQRRI1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1092&quot; height=&quot;1006&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;☆파이썬에서 Qt 위젯 이해 : 이동바를 실행시켜&amp;nbsp;&amp;nbsp;슬라이드값 출력값의 이동함을 보여준다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C3WXH/dJMcab5xqer/ht21n43jXe0Uldb0rnMDRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C3WXH/dJMcab5xqer/ht21n43jXe0Uldb0rnMDRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C3WXH/dJMcab5xqer/ht21n43jXe0Uldb0rnMDRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC3WXH%2FdJMcab5xqer%2Fht21n43jXe0Uldb0rnMDRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;893&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;☆ 버튼을 만들어 레이아웃 살펴보기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;705&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGGCkb/dJMcacpSaxL/X3cGNBSlIBKUKeOTlKsXBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGGCkb/dJMcacpSaxL/X3cGNBSlIBKUKeOTlKsXBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGGCkb/dJMcacpSaxL/X3cGNBSlIBKUKeOTlKsXBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGGCkb%2FdJMcacpSaxL%2FX3cGNBSlIBKUKeOTlKsXBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;897&quot; height=&quot;705&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;705&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;2. Signal &amp;amp; Layout&lt;br /&gt;☆ Qt디자인에서 버튼을 끌어놓으면 정렬이 안되었다면 Ctrl+a을 눌러 개체 전부 선택후&amp;nbsp;&amp;nbsp;Layout정렬을 시켜준다. 버튼정렬됨&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZwqhz/dJMcabYxLpv/E1gj66t0RKRJBKKWavPZ2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZwqhz/dJMcabYxLpv/E1gj66t0RKRJBKKWavPZ2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZwqhz/dJMcabYxLpv/E1gj66t0RKRJBKKWavPZ2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZwqhz%2FdJMcabYxLpv%2FE1gj66t0RKRJBKKWavPZ2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;792&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GCcBF/dJMcajhVcyX/ImIPjB18TBE8p6MAjYZ2bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GCcBF/dJMcajhVcyX/ImIPjB18TBE8p6MAjYZ2bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GCcBF/dJMcajhVcyX/ImIPjB18TBE8p6MAjYZ2bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGCcBF%2FdJMcajhVcyX%2FImIPjB18TBE8p6MAjYZ2bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;546&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>학습 로그/Python3</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/38</guid>
      <comments>https://goodjop79.tistory.com/38#entry38comment</comments>
      <pubDate>Sat, 16 May 2026 22:30:10 +0900</pubDate>
    </item>
    <item>
      <title>D+27 [가상 환경venv] 우분투 파이썬, Qt Creator</title>
      <link>https://goodjop79.tistory.com/37</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 14일(목)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 주 제 : 가상환경 (우분투 파이썬, Qt Creator) 설정 및 실습&lt;/b&gt;&lt;br /&gt;&lt;b&gt;ㅇ 내&amp;nbsp; 용&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 우분투 파이썬 &quot;가상환경&quot;이 필요한 이유&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.1. &lt;b&gt;버전 충돌 방지 및 의존성 관리&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 프로젝트 A (Django 3), 프로젝트 B (Django 4)가 필요할 때, 하나의 환경에 설치하면 충돌이 발생한다&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. 가상환경 사용 : 프로젝트마다 필요한 패키지와 버전을 개별적으로 설치하여 관리 가능하다&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 파이썬 가상환경을 만들어서 project를 시행하고,&amp;nbsp; 파이썬&amp;nbsp;버젼을&amp;nbsp;확인한&amp;nbsp;후&amp;nbsp;작업한다&lt;br /&gt;1.2. &lt;b&gt;시스템 파이썬 보호&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 우분투는 자체 시스템 도구들이 apt를 통해 설치된 파이썬 라이브러리에 의존 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. sudo pip 등을 이용해 시스템 영역에 패키지를 설치하다가 실수로 시스템 패키지를 덮어쓰면 우분투 시스템이 망가질 우려,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;가상환경 내에서만 라이브러리를 설치하는 것이 안전하다.&amp;nbsp;&lt;br /&gt;1.3. &lt;b&gt;독립적 환경 구성 &lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 프로젝트마다 파이썬 버전이나 패키지를 다르게 설정해도 다른 프로젝트에 전혀 영향을 미치지 않는 &quot;독립적인 환경&quot; 제공&lt;br /&gt;1.4. &lt;b&gt;간&lt;/b&gt;&lt;b&gt;편한 환경 재현&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가.&amp;nbsp; &lt;b&gt;requirements.txt&lt;/b&gt; 파일을 사용해 다른 컴퓨터나 서버에서도 동일한 가상환경을 쉽게 재현할 수 있다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나.&amp;nbsp;우분투에서 python3 -m venv &amp;lt;가상환경명&amp;gt; 명령어를 통해 쉽게 가상환경을 생성하고, 활성화하여 사용할 수 있다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 우분투(디렉토리), 윈도우(파일탐색기) 의미, 그러나 파이썬은 모두 파일로 인식한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 라. &lt;u&gt;Thunar(파일관리자)&amp;nbsp;설치하기&lt;/u&gt;&amp;nbsp;:&amp;nbsp;&lt;b&gt;&amp;nbsp;sudo&amp;nbsp;apt&amp;nbsp;install&amp;nbsp;thunar&amp;nbsp;-y&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 마. 시스템과 가상환경에서 Python, pip, 패키지 예시&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- 시스템 파일 : home / ......&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt;- 가상 환경 : home / project.. / 폴더경로_&lt;span style=&quot;color: #ee2323;&quot;&gt;venv(가상환경)&lt;/span&gt; /&lt;span style=&quot;color: #8a3db6;&quot;&gt; bin(파이썬실행파일, pip 실행파일)&lt;/span&gt; / &lt;span style=&quot;color: #006dd7;&quot;&gt;lib(가상환경패키지)&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;2&lt;/b&gt;&lt;b&gt;. 파이썬 가상환경 설정 이해&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1. 파이썬 가상환경 설치 및 활성화 (터미널 입력창에서 설정)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt; 가. 가상환경을 만들고 활성화&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; :~$&amp;nbsp;&lt;/span&gt; python3 -m venv test_venv&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. 다른 버젼의 가상환경 만들고 활성화&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;:~$&lt;/span&gt; &lt;/b&gt;&lt;b&gt;python3.11 -m venv test_venv2&lt;/b&gt; &amp;nbsp;&amp;nbsp;&amp;nbsp;# 버젼을 달리하여 설정할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다.&amp;nbsp; (.venv 가상환경 괄호표시 ) 위치에서 나가기 &amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt; ~(.venv ) :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;b&gt;deactivate&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;b&gt;※ 가상 환경?&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;새로운 Python을 다시 설치하는 것이 아니라, 기본 Python을 기반으로 Python 작업에 필요한 환경을 복사해서 &lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;별도의 작업공간을 생성하는 방식이다.&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;이때 프로젝트에서 사용하는 Python 실행파일, pip, 패키지 설치 공간이 준비된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 라. &amp;nbsp;&lt;b&gt;어떤 작업을 하다가 ( 괄호) 가상환경이 풀리는 경우가 있다. 그러면 가상환경을 다시 설정해 준다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 마. 파이썬 실행 파일 : 시스템 실행파일, 가상환경 실행파일은 작업환경을 다르게 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 바.&amp;nbsp; 시스템 환경을 그대로 복사해서 &lt;u&gt;가상환경에서 개발 작업&lt;/u&gt;을 한다. 시스템 파일은 보호한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;사. 가상환경 설정 확인하기&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(1) 우분투 가상환경 구현 터미널 창 : 가상디렉토리를 test로 만들어(mkdir) 실행하며 위치가 (test_venv) 바뀐다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 가상환경 설정할 때 (괄호)가 없으면 가상환경에서 벗어난 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명 : ~$ &lt;span style=&quot;color: #8a3db6;&quot;&gt;mkdir -p ~/Dev/python&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명 : ~$ &lt;span style=&quot;color: #8a3db6;&quot;&gt;cd ~/Dev/python&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$&amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;python3 -m venv&amp;nbsp; 가상환경폴더명_venv&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$&amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;source 가상환경폴더명_venv/bin/activate&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$&amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;deactivate&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$&amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;which python3&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$&amp;nbsp;&lt;span style=&quot;color: #8a3db6;&quot;&gt; which pip3&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (가상환경폴더명_venv) 사용자 경로명: ~/Dev/pyton$ &lt;span style=&quot;color: #8a3db6;&quot;&gt;/usr/bin/python3&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;gt;&amp;gt;&amp;gt; &lt;span style=&quot;color: #8a3db6;&quot;&gt;/usr/bin/pip3&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(2) .py 파일 생성후 로직 구현 예시&amp;nbsp; (touch)&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; touch test_study&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(3) 가상환경 및 패키지 : 가상환경(venv), 패키지 파일(.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;home / project.. / 폴더경로_&lt;span style=&quot;color: #ee2323;&quot;&gt;venv(가상환경)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;bin(파이썬실행파일, pip 실행파일)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;lib(가상환경패키지)&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #777777;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(4) &amp;nbsp;가상환경 활성화 전.후 파이썬 실행 차이&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;활성화 전&amp;nbsp; &amp;nbsp;/usr/bin/python3&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;활성화 후&amp;nbsp; &lt;span style=&quot;color: #1b711d;&quot;&gt;~/Dev&lt;/span&gt;/Python/test_venv/bin/python&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtXJBU/dJMcaffAaGN/KbiYX1KLIXG5cfSuArQMm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtXJBU/dJMcaffAaGN/KbiYX1KLIXG5cfSuArQMm1/img.png&quot; data-alt=&quot;가상환경 폴더&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtXJBU/dJMcaffAaGN/KbiYX1KLIXG5cfSuArQMm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtXJBU%2FdJMcaffAaGN%2FKbiYX1KLIXG5cfSuArQMm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;268&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상환경 폴더&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.2. 특정버젼 파이썬 설치&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 우분투 환경에서 구버젼 설치하고, 시스템 파일을 건들지 않아야 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. 프로젝트 시행간 원하는 다른 파이썬 버전이 필요할 때 설치 활용&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 특정버젼에서 가상환경 설정 및 프로젝트 생성&amp;nbsp; (교육간 사용할&amp;nbsp; PPA 설치)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #8a3db6; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;:~$ &lt;/b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;sudo apt install python3.11 python3.11-venv python3.11-dev -y&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;ver 3.10,&amp;nbsp; 3.11,&amp;nbsp; 3.12, 3.14 등 필요한 버전을 선택하여 입력 설치&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; * PPA 추가를 위한 필수 패키지 설치&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;sudo apt update&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;sudo apt install software-properties-common&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.3. 외부 패키지 설치하는 법&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;가.&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 프로젝트를 완료 후 동일한 버젼을 모두 넘겨줄때 &quot;외부 패키지&quot;를 활용한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;python -m pip install Pyside6&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt;나. 가상환경 공유 금지에 대한 이해&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 기본적인 용량이 너무 커서 가상환경 전체를 공유하지 않는다. &quot;&quot;소스코드 문서, 패키지 리스트 문서&quot;&quot;, 가벼운 몇개만 공유,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 프로젝트 전체 파일을 공유하는 것은 아니다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (예시) Dev&amp;nbsp;-&amp;nbsp;Project&amp;nbsp;-&amp;nbsp;-&amp;gt;&amp;nbsp;가상환경&amp;nbsp;+&amp;nbsp;패키지&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2.4. 패키지 공유 구현&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;가. 호환성을 위한 환경설정 필요성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 라이브러리&amp;nbsp;버젼이&amp;nbsp;다른&amp;nbsp;경우,&amp;nbsp;공유된&amp;nbsp;가상환경을&amp;nbsp;별도로&amp;nbsp;설정&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (예) 패키지가 다른 문건들이 있을 경우 과거 버젼 문서가 현재 신버젼에서 실행하고자 할때,&amp;nbsp;과거 버젼을 모두 다 조건에 맞게 가상환경을 설정 해 주어야 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;나. 프로젝트 협업 단계&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 시스템 파이선 가상환경 설정 --&amp;gt; 가상환경 구현 임의버젼 설치 확인&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; --&amp;gt; 가상환경에 외부패키지 구현&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; --&amp;gt; 타인도 공유할수 있도록 패키지 공유 구현&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt; &amp;nbsp;다. Python 프로젝트를 다른 사람과 공유할 때 일반적인 흐름&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (1) (Online) GitHub 에서 프로젝트를,&amp;nbsp; (Online) 작업할 pc로 가져온&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;뒤,&amp;nbsp; Python 버젼에 맞춰 가상환경을 만들고, 패키지 목록을 저장&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;i&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;※ 가상환경 폴더를 직접 공유하지 않는 것, 프로젝트 소스코드는 GitHub로&lt;/span&gt;&lt;/i&gt;&lt;i&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&amp;nbsp;공유하고 필요한 패키지 목록은 txt 파일포함&lt;/span&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (2) 공동 개발자와 동일한 환경을 구현하고, 전달하고자 할때 TXT 파일로 패키지 버젼들을 포함 작성한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; requirement.txt&amp;nbsp; &amp;nbsp;(패키지 목록 저장)&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(.venv) 로컬@:~/Dev/Project $ &lt;span style=&quot;color: #8a3db6;&quot;&gt;터미널에서 가상환경 위치에서 입력 python -m pip freeze &amp;gt; requirements.txt&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt;라. 프로젝트 결과 웹사이트와 연동 구현&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (1) 버젼관리에 있는 프로젝트 선택&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xZpwj/dJMcaiciz8v/OwOwM69wgCQcgFpAGCEtpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xZpwj/dJMcaiciz8v/OwOwM69wgCQcgFpAGCEtpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xZpwj/dJMcaiciz8v/OwOwM69wgCQcgFpAGCEtpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxZpwj%2FdJMcaiciz8v%2FOwOwM69wgCQcgFpAGCEtpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;160&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(2) 저장소 URL 입력 : 저장소를 복제하는 단계로 URL과 디렉토리를 지정 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(3) 터미널창에서 입력하여 실행한다.&amp;nbsp; :~$&lt;b&gt;&amp;nbsp; python -m pip install -r requirements.txt&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(4) 외부 웹사이트와 연결하기&amp;nbsp; &amp;nbsp;&lt;b&gt;run.py&lt;/b&gt;&amp;nbsp; 실행하면 http 주소링크가 보이면 연결 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;(5) &quot;&quot;예시&quot;&quot;&amp;nbsp; urn.py를 실행하면 서버가 정상 실행중이며 연결이 되었으니 웹브라우저에서 주소를 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;예시) 000.0.0.0.4500&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bunHqR/dJMcagMrwOF/ELkgGqc93kPl67wS9gYs8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bunHqR/dJMcagMrwOF/ELkgGqc93kPl67wS9gYs8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bunHqR/dJMcagMrwOF/ELkgGqc93kPl67wS9gYs8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbunHqR%2FdJMcagMrwOF%2FELkgGqc93kPl67wS9gYs8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;632&quot; height=&quot;434&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 5.&amp;nbsp; 가상환경(Virtual&amp;nbsp;Environment)&amp;nbsp;내부&amp;nbsp;폴더&amp;nbsp;구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 가. 파이썬(Python) 개발 프로그램(PyCharm)에서 프로젝트 할때 가상환경을 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; 나.&amp;nbsp; 가상환경 .venv ?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-27 12-00-17.png&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8GK9c/dJMb990RYvm/SzKblhLiN4jyIs0hLzZwSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8GK9c/dJMb990RYvm/SzKblhLiN4jyIs0hLzZwSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8GK9c/dJMb990RYvm/SzKblhLiN4jyIs0hLzZwSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8GK9c%2FdJMb990RYvm%2FSzKblhLiN4jyIs0hLzZwSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;431&quot; height=&quot;623&quot; data-filename=&quot;스크린샷 2026-05-27 12-00-17.png&quot; data-origin-width=&quot;431&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;이미지에 .venv라는 폴더가 보인다. (가상환경 &quot;예시&quot;)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;내 컴퓨터 전체에 영향을 주지 않고, 이 프로젝트만을 위해 &lt;u&gt;독립적으로 만든 '작은 파이썬 방&lt;/u&gt;'이며, 각 프로젝트마다 필요한 프로그램 버전이 다를 수 있어 방을 분리해서 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 6. 폴더별 역할&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;&lt;b&gt;bin&amp;nbsp;(바이너리)&amp;nbsp;폴더&lt;/b&gt;&lt;br /&gt;현재 선택되어 있는 폴더로, 이 가상환경을 움직이는 실행 파일(명령어)들이 들어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- python, python3, python3.11&lt;/b&gt; : 프로젝트를 실행할 때 사용하는 파이썬 프로그램 본체 (버전은 3.11을 사용 중)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- pip, pip3&lt;/b&gt; : 파이썬에서 다른 사람들이 만들어둔 유용한 기능(라이브러리)을 다운로드 설치해 주는 '앱스토어' 같은 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;nbsp;include&amp;nbsp;폴더&lt;/b&gt;&lt;br /&gt;파이썬이 C언어 등 다른 언어와 연결될 때 사용하는 내부 설정 파일들이 담기는 곳. 초보 단계에서는 전혀 수정할 일이 없는 폴더&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;lib&amp;nbsp;(라이브러리)&amp;nbsp;폴더&lt;/b&gt;&lt;br /&gt;프로젝트에서 사용할 모든 기능 패키지(코드 묶음)들이 저장되는 창고&lt;br /&gt;- site-packages : 내가 pip 명령어로 추가 다운로드한 외부 프로그램들이 실제로 저장되는 핵심 공간. 기본적으로 설치되어 있는 pip 프로그램 자체의 소스코드가 보여진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;  Tip&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ① &quot;파이썬 3.11 버전으로 독립된 개발 방(.venv)을 만들었으며, 실행 도구(bin)와 기능 창고(lib)가 준비되어 있는 상태&quot;&lt;br /&gt;&amp;nbsp; 파이썬 개발의 표준적인 형태이므로, &lt;u&gt;이 안의 파일들을 직접 수정하거나 삭제하지 않고 그대로 둔 채로 다른 파일을 만들어서 코딩&lt;/u&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ②&amp;nbsp; &lt;u&gt;.venv 나 bin 폴더 안에 패키지 파일을 만들면 안 된다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 상위 폴더 우클릭 : 이미지 가장 상단에 있는 Layout 폴더(또는 프로젝트 이름)를 마우스 우클릭 해서 파일 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ③ 외부 기능(라이브러리) 설치하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;파이썬은 전 세계 개발자들이 만든 유용한 도구 (예: 데이터 분석용 pandas, 웹 크롤링용 beautifulsoup4 등) 를 가져다 쓰기&amp;nbsp; 유용한데 bin&amp;nbsp; 폴더 안의 pip가 하는 역할이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;④&amp;nbsp;PyCharm 화면 가장 아래쪽을 보면 Terminal(터미널)이라는 탭을 클릭하고, 명령어 입력 하여 설치하기&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;color: #8a3db6;&quot;&gt;pip install [라이브러리 이름]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.7. 호환성 문제&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;가. 라이브러리 외부에서 만든 기능을 많이 사용&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;나.&amp;nbsp; 버젼 호환성 에 대한 이해&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;다. 워드, 파워포인트, 엑셀 파일 (2000년대) 을 2026년 신버젼으로 호환 활용&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;라. 특정기능이 동작하지 않을수도 있다. 100% 동작하리라는 보장이 없다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;마. 너무 오래된 문서들도 호환이 안될 수도 있다. 파이썬도 마찬가지다&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;바. 모든것을 고려해서 환경설정을 해 주어야 하는데 조금 복잡하다&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;b&gt;3. 모듈, 패키지, 라이브러리에 대한 이해&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; 모듈(파일) &amp;lt; 패키지(모듈 묶음) &amp;lt; 라이브러리(모듈과 패키지 모음)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;b&gt;4. 프로그램 개발자의 인식과 시대적 변화&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;4.1. 개발 단위의 확장 (&lt;/b&gt;코드에서 '기능'으로)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 과거에는 개발자가 작은 코드 조각(Snippet)을 작성하고 이를 직접 이어 붙이는 데 집중했다면, 이제는 AI(Claude Code,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Codex 등)를 활용해 큰 작업 단위를 처리하는 시대로 변하고 있다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;※ 기존 : 코드 작성 위주, 사람이 모든 조각을 조립.&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 현재 : 기능 구현, 리팩토링, 조사, 테스트까지 포함된 하나의 '완성된 단위'를 AI와 함께 수행.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4.2. 바이브코딩과 에이젠틱 엔지니어링 (Vibe Coding &amp;amp; Agentic Engineering)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 가. 단순한 코딩을 넘어 시스템의 전체 설계와 운영을 AI 에이전트가 주도하는 흐름&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 나. 설계 범위 : DB 설계, 로그인, 결제, 권한 관리, 배포, 운영, 장애 복구 등 시스템 전반.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 다. 에이전트의 역할 : 과거 아키텍트(사람)가 결정하던 시스템 구조를 AI가 잡기 시작함.(향후 변화와 허용범위는?)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 라. 핵심 역량 : 에이전트가 도출한 결과물을 검증(Verification)하고 고도화하는 능력이 중요해짐.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4.3. 남아있는 과제 (책임과 설계의 주체)&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;가. 안드레 카파시(Andrej Karpathy)가 언급한 것처럼, 기술적 가능성과는 별개로 관리적 측면의 의문이 남아 있다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;나. 지침과&amp;nbsp;책임:&amp;nbsp;개발&amp;nbsp;방식의&amp;nbsp;가이드라인을&amp;nbsp;누가&amp;nbsp;줄&amp;nbsp;것이며,&amp;nbsp;결과물에&amp;nbsp;대한&amp;nbsp;책임은&amp;nbsp;누가&amp;nbsp;질&amp;nbsp;것인가?&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;다. 설계의 자율성: 시스템 설계라는 고도의 의사결정까지 AI에게 전적으로 맡길 것인가에 대한 문제는 여전히 논의 중인 영역&lt;br /&gt;&amp;nbsp;&lt;br /&gt;※ 개발자는 코드를 쓰는 '타이피스트'가 아니라, AI 에이전트가 설계하고, 만든 결과물을 검증하고 조율하는 '시스템 감독관'의&amp;nbsp;역할로 변모하고 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;5. QT 설치 및 환경설정 후 실행&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; (1)&amp;nbsp;&lt;/span&gt;Qt 파일을 웹브라우저에서 다운로드 후 다운 받은 파일 위치에서 터미널에서 열기 선택후 실행하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/310V5/dJMcabRG8TF/iUgytprerDG6oFglKvk2Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/310V5/dJMcabRG8TF/iUgytprerDG6oFglKvk2Y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/310V5/dJMcabRG8TF/iUgytprerDG6oFglKvk2Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F310V5%2FdJMcabRG8TF%2FiUgytprerDG6oFglKvk2Y1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;321&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; (2) 환경설정 : 실행할 .py를 &quot;위젯&quot;으로 할지 &quot;메인&quot;으로 할지 사용자 환경에 맞게 설정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x0uVk/dJMcadPCiPj/BtvFzBABaF5yOLELVcasQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x0uVk/dJMcadPCiPj/BtvFzBABaF5yOLELVcasQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x0uVk/dJMcadPCiPj/BtvFzBABaF5yOLELVcasQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx0uVk%2FdJMcadPCiPj%2FBtvFzBABaF5yOLELVcasQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;824&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; (3) main.py 분리하기 화면 : .uI 파일이 .py 파일로 변환 되었다. (변환시&amp;nbsp; Qt창에서 메세지옆 install 을 눌러서 버전 3.12를 실행해 준다)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;(4) 파일 변환후 xml 을 확인 하는 화면 : 예시)&amp;nbsp; form.ui 파일을 실행후 Edit 클리후 xml 내용을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp;(5)&amp;nbsp; xml의 들여쓰기 개념 이해 :&amp;nbsp; &quot;&lt;/span&gt;들여쓰기&quot;는 디렉토리 한묶음을 의미, 들여쓰기로 문법 구문을&amp;nbsp;분리하여 이해한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;b&gt;5.3. Qt 실행 실습&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;b&gt;(1) 디자이너 적용 구조 이해&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Qt6 디자이너에서 디자인을 하여 ui로 저장을 하고, 위젯에서 main.py로 파일을 분리해서 패키지로 통합한 후 파이썬으로 실행하면 최종 파이썬앱이 실행된다,&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rz9Z8/dJMcajoJIQA/EI2ddVrzwLG5RzPBD8jDa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rz9Z8/dJMcajoJIQA/EI2ddVrzwLG5RzPBD8jDa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rz9Z8/dJMcajoJIQA/EI2ddVrzwLG5RzPBD8jDa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frz9Z8%2FdJMcajoJIQA%2FEI2ddVrzwLG5RzPBD8jDa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;768&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;※ 안드레 카파시(Andrej Karpathy)&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp; 안드레이 카르파티는 슬로바키아계 캐나다인 AI 연구자로, 테슬라 에서 인공지능과 오토파일럿 비전의 책임자로 일했다. &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp; 오픈AI에서 공동 창립자이자 이전에 일했으며, 그곳에서 딥 러닝과 컴퓨터 비전을 전문으로 했다. &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp; 2025년 2월 Karpathy는 AI 도구가 취미 활동가들이 프롬프트를 입력하기만 하면 앱과 웹사이트를 구축할 수 있도록 하는 방식을&amp;nbsp; &amp;nbsp;설명하기 위해 &quot;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;vibe coding&quot;이라는&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp;용어를 만들었다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>환경 설정</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/37</guid>
      <comments>https://goodjop79.tistory.com/37#entry37comment</comments>
      <pubDate>Thu, 14 May 2026 21:33:25 +0900</pubDate>
    </item>
    <item>
      <title>D+26 [아두이노] Arduino 환경설정 및 보드 실습</title>
      <link>https://goodjop79.tistory.com/36</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 일 자 : 2026년 5월 13일(수)&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ㅇ 주 제 : 아두이노(Arduino) 환경설정 및 예제 실습&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;ㅇ 목 표 : &lt;/b&gt;&lt;b&gt;&amp;nbsp;아두이노를 활용한 실습을 통해 제품 개발 원리를 이해 할 수 있다.&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;ㅇ 내 용&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;1. 아두이노(Arduino)&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;1.1.&amp;nbsp;의미&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;오픈&amp;nbsp;소스를&amp;nbsp;기반으로&amp;nbsp;만든&amp;nbsp;마이크로컨트롤러&amp;nbsp;보드와&amp;nbsp;관련된&amp;nbsp;개발&amp;nbsp;도구&amp;nbsp;및&amp;nbsp;환경이다.&lt;br /&gt;아두이노를&amp;nbsp;사용하면&amp;nbsp;특정한&amp;nbsp;동작을&amp;nbsp;하는&amp;nbsp;도구를&amp;nbsp;간단하게&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;br /&gt;(예)&amp;nbsp;아두이노&amp;nbsp;보드에&amp;nbsp;입력을&amp;nbsp;담당하는&amp;nbsp;센서와&amp;nbsp;출력을&amp;nbsp;담당하는&amp;nbsp;LED&amp;nbsp;(발광&amp;nbsp;다이오드)를&amp;nbsp;연결한&amp;nbsp;다음&amp;nbsp;프로그래밍으로&amp;nbsp;동작을&amp;nbsp;제어하면&amp;nbsp;화재경보기를&amp;nbsp;만들수&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.2.&amp;nbsp;아두이노로&amp;nbsp;무엇을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있을까?&lt;/b&gt;&lt;br /&gt;-&amp;nbsp;&amp;nbsp;자전거의&amp;nbsp;속력&amp;nbsp;측정&amp;nbsp;:&amp;nbsp;자전거에&amp;nbsp;속력계를&amp;nbsp;달아서&amp;nbsp;속력을&amp;nbsp;이동거리&amp;nbsp;걸린시간으로&amp;nbsp;나누어&amp;nbsp;값을&amp;nbsp;구하여&amp;nbsp;속력계에&amp;nbsp;표시하는&amp;nbsp;장치를&amp;nbsp;자전거에&amp;nbsp;달았다.&lt;br /&gt;&amp;nbsp; (예) 현재는 스마트 워치에 개인이 걷는 걸음수와 맥박수 등을 알려주는 헬스케어 제품들도 등장하였다.&amp;nbsp;&lt;br /&gt;-&amp;nbsp;&amp;nbsp;그림&amp;nbsp;그리기&amp;nbsp;:&amp;nbsp;아두노이를&amp;nbsp;이용해&amp;nbsp;수채화&amp;nbsp;그림을&amp;nbsp;그리는&amp;nbsp;장치로&amp;nbsp;그림의&amp;nbsp;좌표를&amp;nbsp;입력해서&amp;nbsp;붓의&amp;nbsp;위치를&amp;nbsp;도화지의&amp;nbsp;특정&amp;nbsp;좌표로&amp;nbsp;이동하게&amp;nbsp;하여&amp;nbsp;그림을&amp;nbsp;그려준다.&lt;br /&gt;&amp;nbsp; (예) 상업적으로는 대형 지도를 제작하거나 대형 도면, 포스터를 제작할때 기계가 이용되고 있다.&lt;br /&gt;-&amp;nbsp;&amp;nbsp;농작물&amp;nbsp;키우기&amp;nbsp;:&amp;nbsp;자동으로&amp;nbsp;땅에&amp;nbsp;씨앗을&amp;nbsp;뿌리고&amp;nbsp;물을&amp;nbsp;주며,&amp;nbsp;잡초를&amp;nbsp;제거하는&amp;nbsp;기계도&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;XYZ&amp;nbsp;축으로&amp;nbsp;모터를&amp;nbsp;움직여&amp;nbsp;원하는&amp;nbsp;장소에&amp;nbsp;파종을&amp;nbsp;할수도&amp;nbsp;급수를&amp;nbsp;할수&amp;nbsp;도&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp; (예) 현재는 스마트팜 농업이 발전하였고, 그에 따른 기술도 발전하였다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;미국의 경우 AI를 활용한 농법기계를 개발하여 활용하고 있다. 농작물중에 잡초만을 식별하여 드론을 통한 차별적 제초약을 살포하는 등의 사례가 있다.&lt;br /&gt;-&amp;nbsp;&amp;nbsp;안경으로&amp;nbsp;데이터&amp;nbsp;보기,&amp;nbsp;오또로봇&amp;nbsp;만들기,&amp;nbsp;드론과&amp;nbsp;RC&amp;nbsp;카&amp;nbsp;만들기&amp;nbsp;등&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.3.&amp;nbsp;아두이노&amp;nbsp;통합&amp;nbsp;개발&amp;nbsp;환경&amp;nbsp;설정하기&lt;/b&gt;&lt;br /&gt;① 통합 개발환경 파일 내려받기 : &lt;a href=&quot;http://www.arduino.cc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;http://www.arduino.cc&lt;/span&gt;&lt;/a&gt; 에서 맨 아래로 스크롤 해서 소프트웨어 클릭, 리눅스 환경 버젼파일을&lt;br /&gt;내려받기 (아두이노&amp;nbsp; IDE 2.3.8)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSjEvy/dJMcaaeeoc0/Q1D7F8ZGdFs2DUZagOs0q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSjEvy/dJMcaaeeoc0/Q1D7F8ZGdFs2DUZagOs0q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSjEvy/dJMcaaeeoc0/Q1D7F8ZGdFs2DUZagOs0q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSjEvy%2FdJMcaaeeoc0%2FQ1D7F8ZGdFs2DUZagOs0q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;811&quot; height=&quot;516&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;② 다운로드한 파일 설치하기 (터미널에서 실행)&lt;br /&gt;&amp;nbsp; &amp;nbsp; 다운로드한 폴더에서&amp;nbsp; &amp;nbsp;&lt;b&gt;./install.sh&amp;nbsp;&lt;/b&gt; 입력하고 설치 실행하기&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 96.7442%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50.0%; text-align: justify;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMnC8T/dJMcagrXn53/zXPJjn319BGkdqmKbAkBn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMnC8T/dJMcagrXn53/zXPJjn319BGkdqmKbAkBn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMnC8T/dJMcagrXn53/zXPJjn319BGkdqmKbAkBn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMnC8T%2FdJMcagrXn53%2FzXPJjn319BGkdqmKbAkBn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;309&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&amp;nbsp;&lt;br /&gt;&lt;br /&gt;* 다운로드한 파일에서 마우스 우클릭후 &quot;터미널에서 열기&quot;&lt;br /&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 46.7442%; text-align: justify;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkTZSm/dJMcaja3vS5/gvJnTFtjDR9O7qmvRv7clk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkTZSm/dJMcaja3vS5/gvJnTFtjDR9O7qmvRv7clk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkTZSm/dJMcaja3vS5/gvJnTFtjDR9O7qmvRv7clk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkTZSm%2FdJMcaja3vS5%2FgvJnTFtjDR9O7qmvRv7clk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;604&quot; data-origin-width=&quot;590&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;③ 만약 파일 다운로드후 &quot;한글이 깨짐&quot; 현상이 있다면 한글파일 다시 설치 하기&amp;nbsp;&amp;nbsp; &lt;b&gt;sudo apt-get install nanum*&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnVdCu/dJMcahqTzto/Q7AG0A29E6XacN01rVUe7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnVdCu/dJMcahqTzto/Q7AG0A29E6XacN01rVUe7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnVdCu/dJMcahqTzto/Q7AG0A29E6XacN01rVUe7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnVdCu%2FdJMcahqTzto%2FQ7AG0A29E6XacN01rVUe7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;131&quot; data-origin-width=&quot;837&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;④ 아두이노 보드와 컴퓨터 연결하기&amp;nbsp; &amp;nbsp; &amp;nbsp;sudo usermod -a -G dialout $USER&amp;nbsp; &amp;nbsp;실행후 응용프로그램 &quot;아이콘&quot; 클릭 실행&lt;br /&gt;**만약 포트에 문제가 있다면 가장 기본적인 보드와 USB포트를 PC와 연결한다. 연결된 포트를 확인할수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beekD3/dJMcaa6nbLE/ffZ99uI93vWkln3C6UssU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beekD3/dJMcaa6nbLE/ffZ99uI93vWkln3C6UssU1/img.png&quot; data-alt=&quot;현재 로그인한 사용자 dialout 그룹에 추가 및 시리얼포트(com, USB 시리얼 장치 등) sudo 권한 부여&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beekD3/dJMcaa6nbLE/ffZ99uI93vWkln3C6UssU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeekD3%2FdJMcaa6nbLE%2FffZ99uI93vWkln3C6UssU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;787&quot; height=&quot;465&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 로그인한 사용자 dialout 그룹에 추가 및 시리얼포트(com, USB 시리얼 장치 등) sudo 권한 부여&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-complete=&quot;true&quot; data-ved=&quot;2ahUKEwjAzcKd7rWUAxWBsVYBHbNOLJgQ3s0SeggIAggACAcQAw&quot; data-sfc-cb=&quot;&quot; data-sfc-root=&quot;c&quot; data-sfc-cp=&quot;&quot; data-animation-skip=&quot;&quot;&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock floatRight&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;149&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M9EIh/dJMcaaSPPMR/DxLe2zdxBpuYuyMbLskPK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M9EIh/dJMcaaSPPMR/DxLe2zdxBpuYuyMbLskPK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M9EIh/dJMcaaSPPMR/DxLe2zdxBpuYuyMbLskPK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM9EIh%2FdJMcaaSPPMR%2FDxLe2zdxBpuYuyMbLskPK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;76&quot; height=&quot;75&quot; data-origin-width=&quot;149&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;1.4. 회로 연결하여 통합 환경에서 실습하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; color: #8a3db6;&quot;&gt;※ 만약 코드애러중 문법애러가 아니고 포트나 보드 문제일 경우에 설정이 제대로 되었는지 재확인 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; color: #8a3db6;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;그리고, 포트는 보드가 PC와 USB선으로 연결이 되어있어야 정상적으로 작동이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5pDdm/dJMcaja3zXd/r32yxSF9YVStU6yqMItj50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5pDdm/dJMcaja3zXd/r32yxSF9YVStU6yqMItj50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5pDdm/dJMcaja3zXd/r32yxSF9YVStU6yqMItj50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5pDdm%2FdJMcaja3zXd%2Fr32yxSF9YVStU6yqMItj50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1082&quot; height=&quot;442&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt1lyX/dJMcadIGlmU/eX5FXsM9oDJ30QrqRiT5qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt1lyX/dJMcadIGlmU/eX5FXsM9oDJ30QrqRiT5qK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt1lyX/dJMcadIGlmU/eX5FXsM9oDJ30QrqRiT5qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt1lyX%2FdJMcadIGlmU%2FeX5FXsM9oDJ30QrqRiT5qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1018&quot; height=&quot;371&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;[예시 1]&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779720653656&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// C++언어

int status = 0;   // LED의 상태를 나타내는 변수, 0이면 OFF, 1이면 ON

void setup()
{
  pinMode(13, OUTPUT);
  Serial.begin(9600);
}

void loop()
{
  char inputValue = Serial.read();       // 시리얼 모니터에서 사용자 입력을 받는다
  if (status == 0 &amp;amp;&amp;amp; inputValue == '1')  // 사용자 입력을 비교해 LED를 켠다
  {
    digitalWrite(13, HIGH);
    status = 1;                         // LED의 상태를 ON 상태로 변경
  }
  else if (status == 1 &amp;amp;&amp;amp; inputValue == '0') // 사용자 입력을 비교해 LED를 끈다
  {
    digitalWrite(13, LOW);
    status = 0;                         // LED의 상태를 OFF 상태로 변경
  }
}

//&quot;&quot;결과&quot;&quot;는 시리얼 모니터에서 1을 전송하면 LED가 계속 켜져있고, 0을 전송하면 LED가 꺼진다.
// 시리얼모니터에서 기본 9600보드레이트 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;** 실행을 확인하려면 시리얼 모니터에서 값을 입력하면 불빛이 on/off 을 실행한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1563&quot; data-origin-height=&quot;865&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k93pA/dJMcabc5gSl/ufEsv5KRSTjOOb3CH2Y2vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k93pA/dJMcabc5gSl/ufEsv5KRSTjOOb3CH2Y2vk/img.png&quot; data-alt=&quot;&amp;quot;시리얼 모니터&amp;quot;에서 값을 입력한다. 0은( LED OFF), 1은(LED ON)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k93pA/dJMcabc5gSl/ufEsv5KRSTjOOb3CH2Y2vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk93pA%2FdJMcabc5gSl%2FufEsv5KRSTjOOb3CH2Y2vk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;835&quot; height=&quot;462&quot; data-origin-width=&quot;1563&quot; data-origin-height=&quot;865&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;시리얼 모니터&quot;에서 값을 입력한다. 0은( LED OFF), 1은(LED ON)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;[예시 2]&lt;/p&gt;
&lt;pre id=&quot;code_1779720698012&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//보드핀은 D13에 접지하고 USB포트 연결한다.

void setup()
{
  Serial.begin(9600);    // 시리얼 통신 초기화

  pinMode(8, OUTPUT);  // 8번 디지털 핀을 출력 모드로 실행
  pinMode(7, INPUT_PULLUP);  // 7번 디지털 핀을 입력 모드로 설정
}

void loop()
{
 
  int readValue = digitalRead(7);  // 입력 핀의 값을 읽어 변수에 저장
  Serial.println(readValue);   // 변숫값을 시리얼 모니터에 출력

  if (readValue == HIGH) {   // 14행이 참이면 실행
    digitalWrite(8, LOW);
  }
  else {
    digitalWrite(8, HIGH);    // 14행이 참 아니면 실행
  }
}

// 결과는 - -- - LED가 시간간격을 달리하여 리듬을 타듯 깜박이고 자동 종료.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;[예시 3]&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779720715718&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 아두노이의 13번에 핀을 접지하고 실행한다.

void setup()
{
  pinMode(13, OUTPUT);  // 아두이노의 13번 핀을 출력 모드로 설정
}                       // 아두이노의 12번 핀을 출력 모드로 설정

void loop()
{
  digitalWrite(13, HIGH); // 아두이노의 13번 핀에 HIGH 신호를 전달
                       
  delay(1000);           // 1초동안 지연

  digitalWrite(13, LOW);  // 아두이노의 13번 핀에 LOW 신호를 전달
  delay(1000);            // 1초 동안 지
}

// 결과는 LED가 1초 간격으로 켜졌다가 꺼지기를 계속 반복한다.
// 종료를 해 주어야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1865&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by4qZD/dJMcaiQOC82/T5vHQKknsyfO3RO5DhDhN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by4qZD/dJMcaiQOC82/T5vHQKknsyfO3RO5DhDhN1/img.png&quot; data-alt=&quot;아두이노 통합 환경에서 실습 화면 [예시 1~3]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by4qZD/dJMcaiQOC82/T5vHQKknsyfO3RO5DhDhN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby4qZD%2FdJMcaiQOC82%2FT5vHQKknsyfO3RO5DhDhN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;853&quot; height=&quot;368&quot; data-origin-width=&quot;1865&quot; data-origin-height=&quot;805&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아두이노 통합 환경에서 실습 화면 [예시 1~3]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;[예시 4]&lt;/p&gt;
&lt;pre id=&quot;code_1779720912212&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 헤더와 주소값이 회로기기 제품과 일치해야 실행시 오류가 없다.

#include &amp;lt;LiquidCrystal_I2C.h&amp;gt;   // 헤더 #include &amp;lt;LiquidCrystal_I2C.h&amp;gt; 

LiquidCrystal_I2C lcd(0x27, 16, 2);  // LCD 주소값이 블록편집기기와 일치해야 한다.

void setup()
{
   lcd.begin();   
}

void loop()
{
  lcd.setCursor(5, 0);
  lcd.print(&quot;Hi ^^&quot;);    // Hi ^^ 를 LCD에 출력한다.
  delay(1000);
  lcd.setCursor(3, 1);
  lcd.print(&quot;Codingrun&quot;);
  delay(1000);       // 1초마다 블록편집기 LCD창에 문자가 출력된다.
  lcd.clear();       // 문자를 출력하고 나면 창을 깨끗하게 하고 사각블럭만 남는다.
  delay(1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lsUqD/dJMcagFxhCH/xdbd8rhLdptSmGY1sMd7c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lsUqD/dJMcagFxhCH/xdbd8rhLdptSmGY1sMd7c1/img.png&quot; data-alt=&quot;아두이노 환경에서 LCD 블록 화면에 문자 출력하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lsUqD/dJMcagFxhCH/xdbd8rhLdptSmGY1sMd7c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlsUqD%2FdJMcagFxhCH%2Fxdbd8rhLdptSmGY1sMd7c1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;854&quot; height=&quot;932&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아두이노 환경에서 LCD 블록 화면에 문자 출력하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOP4fx/dJMb990Idtp/gYLXfvUOMkdWniGqxzw2sk/tfile.heic&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOP4fx/dJMb990Idtp/gYLXfvUOMkdWniGqxzw2sk/tfile.heic&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOP4fx/dJMb990Idtp/gYLXfvUOMkdWniGqxzw2sk/tfile.heic&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOP4fx%2FdJMb990Idtp%2FgYLXfvUOMkdWniGqxzw2sk%2Ftfile.heic&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2252&quot; height=&quot;4000&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pz3O6/dJMcadBUW74/zRRUCdNtkRSJ2Km2dB2ut1/tfile.heic&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pz3O6/dJMcadBUW74/zRRUCdNtkRSJ2Km2dB2ut1/tfile.heic&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pz3O6/dJMcadBUW74/zRRUCdNtkRSJ2Km2dB2ut1/tfile.heic&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpz3O6%2FdJMcadBUW74%2FzRRUCdNtkRSJ2Km2dB2ut1%2Ftfile.heic&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2252&quot; height=&quot;4000&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: justify;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/56CYH/dJMcadBUW77/9uhNKpGiacLq5SuFhDGPV1/tfile.heic&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/56CYH/dJMcadBUW77/9uhNKpGiacLq5SuFhDGPV1/tfile.heic&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/56CYH/dJMcadBUW77/9uhNKpGiacLq5SuFhDGPV1/tfile.heic&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F56CYH%2FdJMcadBUW77%2F9uhNKpGiacLq5SuFhDGPV1%2Ftfile.heic&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2252&quot; height=&quot;4000&quot; data-origin-width=&quot;2252&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;아두노이 블록 편집기&amp;nbsp; 실제 연결 실습&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;2. 교육 후기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp;가. 아두이노가 오픈 소스를 기반으로 만든 마이크로컨트롤러 보드와 관련된 개발 도구 및 환경에서 특정한 동작을 하는 도구를&amp;nbsp; 간단하게 만들수 있다는데 직접 해보기 전까지는 이해가 쉽지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp; 나. 문제점 : 환경설정에서 컴퓨터와 연동하는 부분에서 한글이 깨진다든지, c언어 명령어와 기계값이 일치하지 않아 애러가 발생한다 든지 회로 연결을 잘못해서 실행이 안된다거나 하는 문제는 해결해 가는 과정에서 초보자에게는 교육이 많이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp; 다. 처음엔 어려울 거라고만 생각 했지만 직접 실행해 보니까 원리를 이해할 수 있었다. 그러나 C언어는 초보자에겐 아직 공부해야 할 부분이 많은것은 사실이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp; 라. 우리가 실생활에서 많이 활용하고 있는 인터폰, 화재경보기, 도어록, 공사장 경고등, 0과 1로 인지되어 자동으로 사용되고 있는 수많은 기계 및 모델들이 그렇다. 이밖에도 많은 기계와 자동화된 동작들이 인간생활을 편리하게 해 주고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&amp;nbsp; 마. AI 시대에 비전공자들도 코딩하고 스마트한 기계와 로봇들이 나날이 발전하고 있다고는 하지만 소프트웨어의 기본원리를 모르고서는 좀더 확장된 사고를 하기에는 다소 어려울 것이라 생각한다.&amp;nbsp; &quot;&quot;우리의 도전은 계속된다.&quot;&quot;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>환경 설정</category>
      <author>goodjob^^</author>
      <guid isPermaLink="true">https://goodjop79.tistory.com/36</guid>
      <comments>https://goodjop79.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 13 May 2026 18:29:19 +0900</pubDate>
    </item>
  </channel>
</rss>