<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko">
	<id>https://devcafe.co.kr/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Devcafe</id>
	<title>데브카페 - 사용자 기여 [ko]</title>
	<link rel="self" type="application/atom+xml" href="https://devcafe.co.kr/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Devcafe"/>
	<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/%ED%8A%B9%EC%88%98:%EA%B8%B0%EC%97%AC/Devcafe"/>
	<updated>2026-05-19T06:57:38Z</updated>
	<subtitle>사용자 기여</subtitle>
	<generator>MediaWiki 1.42.1</generator>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=AutoIt%EC%97%90%EC%84%9C_sqlplus.exe_%ED%98%B8%EC%B6%9C_%EA%B2%B0%EA%B3%BC%EB%A5%BC_%EA%B7%B8%EB%A6%AC%EB%93%9C&amp;diff=2412</id>
		<title>AutoIt에서 sqlplus.exe 호출 결과를 그리드</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=AutoIt%EC%97%90%EC%84%9C_sqlplus.exe_%ED%98%B8%EC%B6%9C_%EA%B2%B0%EA%B3%BC%EB%A5%BC_%EA%B7%B8%EB%A6%AC%EB%93%9C&amp;diff=2412"/>
		<updated>2026-05-18T10:16:31Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서:  SET MARKUP CSV ON QUOTE ON으로 sqlplus 출력을 CSV로 받아 파싱 → JSON으로 변환하는 방식입니다. 자격증명은 conn.ini에 저장해서 HTTP body로 평문 전송하지 않도록 했습니다.  구조 추가 &amp;lt;source lang=bash&amp;gt;  project/ ├── main.au3 ├── conn.ini          ← 새로 추가 (접속정보) ├── sqlite3.dll └── web/index.html &amp;lt;/source&amp;gt;   conn.ini (예시) &amp;lt;source lang=bash&amp;gt;  [prod] user=scott pass=tiger tns=ORCL nls=KORE...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
SET MARKUP CSV ON QUOTE ON으로 sqlplus 출력을 CSV로 받아 파싱 → JSON으로 변환하는 방식입니다. 자격증명은 conn.ini에 저장해서 HTTP body로 평문 전송하지 않도록 했습니다.&lt;br /&gt;
&lt;br /&gt;
구조 추가&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
&lt;br /&gt;
project/&lt;br /&gt;
├── main.au3&lt;br /&gt;
├── conn.ini          ← 새로 추가 (접속정보)&lt;br /&gt;
├── sqlite3.dll&lt;br /&gt;
└── web/index.html&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
conn.ini (예시)&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[prod]&lt;br /&gt;
user=scott&lt;br /&gt;
pass=tiger&lt;br /&gt;
tns=ORCL&lt;br /&gt;
nls=KOREAN_KOREA.AL32UTF8&lt;br /&gt;
&lt;br /&gt;
[dev]&lt;br /&gt;
user=hr&lt;br /&gt;
pass=hr&lt;br /&gt;
tns=XEPDB1&lt;br /&gt;
nls=KOREAN_KOREA.AL32UTF8&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
tns는 tnsnames.ora의 엔트리명, 또는 //host:port/service 형태도 가능.&lt;br /&gt;
&lt;br /&gt;
main.au3 에 추가 (상단 include 아래)&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
#include &amp;lt;Constants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global $g_sConnIni = @ScriptDir &amp;amp; &amp;quot;\conn.ini&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Switch $sPath 안에 케이스 두 개 추가:&lt;br /&gt;
&lt;br /&gt;
Case &amp;quot;/api/oracle/connections&amp;quot;&lt;br /&gt;
    _ApiOraConns($iSock)&lt;br /&gt;
Case &amp;quot;/api/oracle/query&amp;quot;&lt;br /&gt;
    _ApiOraQuery($iSock, $sBody)&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
그리고 파일 끝에 함수 추가:&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; ─────────────────────────────────────────────&lt;br /&gt;
; Oracle 연동&lt;br /&gt;
; ─────────────────────────────────────────────&lt;br /&gt;
Func _ApiOraConns($iSock)&lt;br /&gt;
    Local $aSec = IniReadSectionNames($g_sConnIni)&lt;br /&gt;
    Local $s = &#039;{&amp;quot;ok&amp;quot;:true,&amp;quot;connections&amp;quot;:[&#039;&lt;br /&gt;
    If IsArray($aSec) Then&lt;br /&gt;
        For $i = 1 To $aSec[0]&lt;br /&gt;
            If $i &amp;gt; 1 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
            $s &amp;amp;= _JsonStr($aSec[$i])&lt;br /&gt;
        Next&lt;br /&gt;
    EndIf&lt;br /&gt;
    $s &amp;amp;= &#039;]}&#039;&lt;br /&gt;
    _SendJson($iSock, $s)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ApiOraQuery($iSock, $sBody)&lt;br /&gt;
    Local $aSql  = StringRegExp($sBody, &#039;&amp;quot;sql&amp;quot;:&amp;quot;((?:\\.|[^&amp;quot;\\])*)&amp;quot;&#039;, 1)&lt;br /&gt;
    Local $aConn = StringRegExp($sBody, &#039;&amp;quot;conn&amp;quot;:&amp;quot;((?:\\.|[^&amp;quot;\\])*)&amp;quot;&#039;, 1)&lt;br /&gt;
    If Not IsArray($aSql) Or Not IsArray($aConn) Then&lt;br /&gt;
        _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:false,&amp;quot;err&amp;quot;:&amp;quot;sql/conn 누락&amp;quot;}&#039;)&lt;br /&gt;
        Return&lt;br /&gt;
    EndIf&lt;br /&gt;
    Local $sSQL  = _JsonUnesc($aSql[0])&lt;br /&gt;
    Local $sName = _JsonUnesc($aConn[0])&lt;br /&gt;
&lt;br /&gt;
    Local $sUser = IniRead($g_sConnIni, $sName, &amp;quot;user&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
    Local $sPass = IniRead($g_sConnIni, $sName, &amp;quot;pass&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
    Local $sTns  = IniRead($g_sConnIni, $sName, &amp;quot;tns&amp;quot;,  &amp;quot;&amp;quot;)&lt;br /&gt;
    Local $sNls  = IniRead($g_sConnIni, $sName, &amp;quot;nls&amp;quot;,  &amp;quot;KOREAN_KOREA.AL32UTF8&amp;quot;)&lt;br /&gt;
    If $sUser = &amp;quot;&amp;quot; Or $sTns = &amp;quot;&amp;quot; Then&lt;br /&gt;
        _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:false,&amp;quot;err&amp;quot;:&amp;quot;conn.ini에 [&#039; &amp;amp; $sName &amp;amp; &#039;] 없음&amp;quot;}&#039;)&lt;br /&gt;
        Return&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; 임시 SQL 스크립트&lt;br /&gt;
    Local $sTmp = @TempDir &amp;amp; &amp;quot;\au_oraq_&amp;quot; &amp;amp; @AutoItPID &amp;amp; &amp;quot;.sql&amp;quot;&lt;br /&gt;
    Local $sClean = StringRegExpReplace($sSQL, &amp;quot;[\r\n;\s]+$&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
    Local $sScript = _&lt;br /&gt;
        &amp;quot;SET MARKUP CSV ON QUOTE ON&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET FEEDBACK OFF&amp;quot;           &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET HEADING ON&amp;quot;             &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET PAGESIZE 50000&amp;quot;         &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET LINESIZE 32767&amp;quot;         &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET LONG 4000&amp;quot;              &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET TRIMSPOOL ON&amp;quot;           &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;SET SERVEROUTPUT OFF&amp;quot;       &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;WHENEVER SQLERROR EXIT 1&amp;quot;   &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        $sClean &amp;amp; &amp;quot;;&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;EXIT;&amp;quot; &amp;amp; @CRLF&lt;br /&gt;
&lt;br /&gt;
    Local $hF = FileOpen($sTmp, 2 + 128) ; overwrite + UTF-8&lt;br /&gt;
    FileWrite($hF, $sScript)&lt;br /&gt;
    FileClose($hF)&lt;br /&gt;
&lt;br /&gt;
    ; NLS_LANG 지정 후 sqlplus -S 실행&lt;br /&gt;
    EnvSet(&amp;quot;NLS_LANG&amp;quot;, $sNls)&lt;br /&gt;
    Local $sConnStr = $sUser &amp;amp; &#039;/&amp;quot;&#039; &amp;amp; $sPass &amp;amp; &#039;&amp;quot;@&#039; &amp;amp; $sTns&lt;br /&gt;
    Local $sCmd = &#039;sqlplus -S -L &#039; &amp;amp; $sConnStr &amp;amp; &#039; @&amp;quot;&#039; &amp;amp; $sTmp &amp;amp; &#039;&amp;quot;&#039;&lt;br /&gt;
&lt;br /&gt;
    Local $iPID = Run(@ComSpec &amp;amp; &#039; /c &#039; &amp;amp; $sCmd, @ScriptDir, _&lt;br /&gt;
                     @SW_HIDE, $STDOUT_CHILD + $STDERR_CHILD)&lt;br /&gt;
    Local $sOut = &amp;quot;&amp;quot;, $sErr = &amp;quot;&amp;quot;&lt;br /&gt;
    While ProcessExists($iPID)&lt;br /&gt;
        $sOut &amp;amp;= StdoutRead($iPID)&lt;br /&gt;
        $sErr &amp;amp;= StderrRead($iPID)&lt;br /&gt;
        Sleep(30)&lt;br /&gt;
    WEnd&lt;br /&gt;
    $sOut &amp;amp;= StdoutRead($iPID)&lt;br /&gt;
    $sErr &amp;amp;= StderrRead($iPID)&lt;br /&gt;
    FileDelete($sTmp)&lt;br /&gt;
&lt;br /&gt;
    ; ORA-/SP2- 오류 감지&lt;br /&gt;
    Local $aOraErr = StringRegExp($sOut &amp;amp; @CRLF &amp;amp; $sErr, _&lt;br /&gt;
        &amp;quot;((?:ORA|SP2|TNS)-\d+[^\r\n]*)&amp;quot;, 1)&lt;br /&gt;
    If IsArray($aOraErr) Then&lt;br /&gt;
        _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:false,&amp;quot;err&amp;quot;:&#039; &amp;amp; _JsonStr($aOraErr[0]) &amp;amp; &#039;}&#039;)&lt;br /&gt;
        Return&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    _SendJson($iSock, _CsvOutToJson($sOut))&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; sqlplus CSV 출력 → JSON&lt;br /&gt;
Func _CsvOutToJson($sRaw)&lt;br /&gt;
    Local $aLines = StringSplit(StringStripCR($sRaw), @LF)&lt;br /&gt;
    Local $aCols[0], $aRows[0]&lt;br /&gt;
    Local $bHeader = False&lt;br /&gt;
    For $i = 1 To $aLines[0]&lt;br /&gt;
        Local $sLine = $aLines[$i]&lt;br /&gt;
        If StringStripWS($sLine, 3) = &amp;quot;&amp;quot; Then ContinueLoop&lt;br /&gt;
        ; CSV 라인은 보통 &amp;quot;로 시작하거나 ,를 포함. 그 외 잡라인 스킵&lt;br /&gt;
        If StringLeft($sLine, 1) &amp;lt;&amp;gt; &#039;&amp;quot;&#039; And Not StringInStr($sLine, &amp;quot;,&amp;quot;) Then&lt;br /&gt;
            ContinueLoop&lt;br /&gt;
        EndIf&lt;br /&gt;
        Local $aFields = _ParseCsvLine($sLine)&lt;br /&gt;
        If Not $bHeader Then&lt;br /&gt;
            $aCols = $aFields&lt;br /&gt;
            $bHeader = True&lt;br /&gt;
        Else&lt;br /&gt;
            ReDim $aRows[UBound($aRows) + 1]&lt;br /&gt;
            $aRows[UBound($aRows) - 1] = $aFields&lt;br /&gt;
        EndIf&lt;br /&gt;
    Next&lt;br /&gt;
&lt;br /&gt;
    Local $s = &#039;{&amp;quot;ok&amp;quot;:true,&amp;quot;columns&amp;quot;:[&#039;&lt;br /&gt;
    For $i = 0 To UBound($aCols) - 1&lt;br /&gt;
        If $i &amp;gt; 0 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
        $s &amp;amp;= _JsonStr($aCols[$i])&lt;br /&gt;
    Next&lt;br /&gt;
    $s &amp;amp;= &#039;],&amp;quot;rows&amp;quot;:[&#039;&lt;br /&gt;
    For $i = 0 To UBound($aRows) - 1&lt;br /&gt;
        If $i &amp;gt; 0 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
        $s &amp;amp;= &amp;quot;[&amp;quot;&lt;br /&gt;
        Local $aRow = $aRows[$i]&lt;br /&gt;
        For $j = 0 To UBound($aRow) - 1&lt;br /&gt;
            If $j &amp;gt; 0 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
            $s &amp;amp;= _JsonStr($aRow[$j])&lt;br /&gt;
        Next&lt;br /&gt;
        $s &amp;amp;= &amp;quot;]&amp;quot;&lt;br /&gt;
    Next&lt;br /&gt;
    $s &amp;amp;= &#039;]}&#039;&lt;br /&gt;
    Return $s&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ParseCsvLine($sLine)&lt;br /&gt;
    Local $aOut[256], $iN = 0&lt;br /&gt;
    Local $sCur = &amp;quot;&amp;quot;, $bQ = False&lt;br /&gt;
    Local $iLen = StringLen($sLine)&lt;br /&gt;
    For $i = 1 To $iLen&lt;br /&gt;
        Local $c = StringMid($sLine, $i, 1)&lt;br /&gt;
        If $bQ Then&lt;br /&gt;
            If $c = &#039;&amp;quot;&#039; Then&lt;br /&gt;
                If $i &amp;lt; $iLen And StringMid($sLine, $i + 1, 1) = &#039;&amp;quot;&#039; Then&lt;br /&gt;
                    $sCur &amp;amp;= &#039;&amp;quot;&#039;&lt;br /&gt;
                    $i += 1&lt;br /&gt;
                Else&lt;br /&gt;
                    $bQ = False&lt;br /&gt;
                EndIf&lt;br /&gt;
            Else&lt;br /&gt;
                $sCur &amp;amp;= $c&lt;br /&gt;
            EndIf&lt;br /&gt;
        Else&lt;br /&gt;
            If $c = &#039;&amp;quot;&#039; Then&lt;br /&gt;
                $bQ = True&lt;br /&gt;
            ElseIf $c = &amp;quot;,&amp;quot; Then&lt;br /&gt;
                $aOut[$iN] = $sCur&lt;br /&gt;
                $iN += 1&lt;br /&gt;
                $sCur = &amp;quot;&amp;quot;&lt;br /&gt;
            Else&lt;br /&gt;
                $sCur &amp;amp;= $c&lt;br /&gt;
            EndIf&lt;br /&gt;
        EndIf&lt;br /&gt;
    Next&lt;br /&gt;
    $aOut[$iN] = $sCur&lt;br /&gt;
    $iN += 1&lt;br /&gt;
    ReDim $aOut[$iN]&lt;br /&gt;
    Return $aOut&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
web/index.html — 툴바와 그리드 사이에 SQL 패널 추가&lt;br /&gt;
&lt;br /&gt;
&amp;lt;h2&amp;gt; 다음에 삽입:&lt;br /&gt;
&amp;lt;source lang=html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div id=&amp;quot;sqlPanel&amp;quot; style=&amp;quot;background:#fff;border:1px solid #ddd;&lt;br /&gt;
     border-radius:6px;padding:10px;margin-bottom:10px;&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div style=&amp;quot;display:flex;gap:8px;align-items:center;margin-bottom:6px;&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;label&amp;gt;접속:&amp;lt;/label&amp;gt;&lt;br /&gt;
    &amp;lt;select id=&amp;quot;connSel&amp;quot; style=&amp;quot;padding:5px;&amp;quot;&amp;gt;&amp;lt;/select&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;runSQL()&amp;quot;&amp;gt;▶ 실행 (Ctrl+Enter)&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;document.getElementById(&#039;sqlPanel&#039;).style.display=&#039;none&#039;&amp;quot;&amp;gt;접기&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;textarea id=&amp;quot;sqlArea&amp;quot; rows=&amp;quot;5&amp;quot;&lt;br /&gt;
    style=&amp;quot;width:100%;font-family:Consolas,monospace;font-size:13px;&lt;br /&gt;
           padding:6px;box-sizing:border-box;&amp;quot;&lt;br /&gt;
    placeholder=&amp;quot;SELECT * FROM dual&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;button onclick=&amp;quot;document.getElementById(&#039;sqlPanel&#039;).style.display=&#039;block&#039;&amp;quot;&lt;br /&gt;
        style=&amp;quot;margin-bottom:8px;&amp;quot;&amp;gt;🔌 SQL&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
그리고 &amp;lt;script&amp;gt; 안 loadAll() 위에 추가:&lt;br /&gt;
&amp;lt;source lang=html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
async function loadConns(){&lt;br /&gt;
  const j = await (await fetch(&#039;/api/oracle/connections&#039;)).json();&lt;br /&gt;
  const sel = document.getElementById(&#039;connSel&#039;);&lt;br /&gt;
  sel.innerHTML = j.connections.map(c=&amp;gt;`&amp;lt;option&amp;gt;${c}&amp;lt;/option&amp;gt;`).join(&#039;&#039;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function runSQL(){&lt;br /&gt;
  const sql  = document.getElementById(&#039;sqlArea&#039;).value.trim();&lt;br /&gt;
  const conn = document.getElementById(&#039;connSel&#039;).value;&lt;br /&gt;
  if (!sql) return;&lt;br /&gt;
  setStatus(&#039;Oracle 실행중...&#039;);&lt;br /&gt;
  const t0 = Date.now();&lt;br /&gt;
  const r = await fetch(&#039;/api/oracle/query&#039;, {&lt;br /&gt;
    method:&#039;POST&#039;,&lt;br /&gt;
    headers:{&#039;Content-Type&#039;:&#039;application/json&#039;},&lt;br /&gt;
    body: JSON.stringify({sql, conn})&lt;br /&gt;
  });&lt;br /&gt;
  const j = await r.json();&lt;br /&gt;
  if (!j.ok){&lt;br /&gt;
    setStatus(&#039;❌ &#039; + j.err);&lt;br /&gt;
    alert(j.err);&lt;br /&gt;
    return;&lt;br /&gt;
  }&lt;br /&gt;
  // 그리드에 결과 표시: 1행=헤더, 이후=데이터&lt;br /&gt;
  const data = [j.columns, ...j.rows];&lt;br /&gt;
  // 빈 행 패딩&lt;br /&gt;
  while (data.length &amp;lt; 30) data.push(Array(j.columns.length).fill(&#039;&#039;));&lt;br /&gt;
  const el = document.getElementById(&#039;sheet&#039;);&lt;br /&gt;
  el.innerHTML = &#039;&#039;;&lt;br /&gt;
  sheet = jspreadsheet(el, {&lt;br /&gt;
    data,&lt;br /&gt;
    columns: j.columns.map((c,i)=&amp;gt;({title:String.fromCharCode(65+i), width:130})),&lt;br /&gt;
    minDimensions: [Math.max(j.columns.length,10), Math.max(data.length,30)]&lt;br /&gt;
  });&lt;br /&gt;
  setStatus(`✅ ${j.rows.length}행 / ${((Date.now()-t0)/1000).toFixed(2)}s`);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// Ctrl+Enter 단축키&lt;br /&gt;
document.addEventListener(&#039;keydown&#039;, e=&amp;gt;{&lt;br /&gt;
  if (e.ctrlKey &amp;amp;&amp;amp; e.key === &#039;Enter&#039; &amp;amp;&amp;amp;&lt;br /&gt;
      document.activeElement.id === &#039;sqlArea&#039;){&lt;br /&gt;
    e.preventDefault(); runSQL();&lt;br /&gt;
  }&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
loadConns();&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
확인 / 주의사항&lt;br /&gt;
&lt;br /&gt;
	•	sqlplus PATH: Oracle Client 또는 Instant Client의 sqlplus.exe가 PATH에 있어야 함. 없으면 Run 호출에서 sqlplus 대신 전체 경로(C:\app\client\bin\sqlplus.exe) 사용.&lt;br /&gt;
	•	NLS_LANG: 한글 깨지면 conn.ini의 nls 값을 DB 캐릭터셋에 맞게 조정. UTF-8 DB면 AL32UTF8, KO16MSWIN949 DB면 KOREAN_KOREA.KO16MSWIN949 + 프론트에서도 인코딩 매칭 필요.&lt;br /&gt;
	•	단일 SELECT 전용: 현재 구조는 SELECT 결과 표시용. DML/DDL은 결과 행이 없어 grid가 비게 됨 — 필요 시 SET FEEDBACK ON으로 두고 raw 출력을 메시지로 반환하는 분기 추가 가능.&lt;br /&gt;
	•	세미콜론 처리: 입력 SQL 끝의 ;은 정규식으로 제거 후 한 번만 붙임. PL/SQL 블록(BEGIN ... END;)은 별도 처리 필요 — 현재는 단일 SELECT 기준.&lt;br /&gt;
	•	결과 크기: 매우 큰 결과셋은 stdout 버퍼/메모리 부담. 운영용은 WHERE ROWNUM &amp;lt;= n 또는 FETCH FIRST n ROWS ONLY로 제한 권장.&lt;br /&gt;
	•	conn.ini 보안: 평문 비밀번호이므로 NTFS 권한으로 본인만 읽기 가능하게 설정하거나, AutoIt에서 간단 XOR 암호화 래퍼를 씌우는 것도 가능.&lt;br /&gt;
&lt;br /&gt;
PL/SQL 블록 지원이나 DML 결과 표시, 컬럼별 데이터 타입(NUMBER/DATE) 우측정렬·포맷 등이 필요하면 이어서 만들어 드릴게요.&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=Autoit_web_%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&amp;diff=2411</id>
		<title>Autoit web 하이브리드 프로그래밍</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=Autoit_web_%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&amp;diff=2411"/>
		<updated>2026-05-18T10:14:36Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;좋습니다. AutoIt이 로컬 HTTP 서버 + SQLite 백엔드 역할을 하고, 브라우저에서 jspreadsheet-ce(엑셀형 그리드)로 화면을 띄우는 구조로 만들어 드릴게요.&lt;br /&gt;
&lt;br /&gt;
구조&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
project/&lt;br /&gt;
├── main.au3          ← HTTP 서버 + SQLite&lt;br /&gt;
├── sqlite3.dll       ← SQLite UDF용 (AutoIt SQLite UDF에 포함)&lt;br /&gt;
└── web/&lt;br /&gt;
    └── index.html    ← 엑셀형 UI&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
동작: AutoIt이 127.0.0.1:8765 에서 listen → 자동으로 기본 브라우저 오픈 → 그리드에서 입력/수정 → /api/save, /api/load 로 SQLite와 통신.&lt;br /&gt;
&lt;br /&gt;
main.au3&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
#include &amp;lt;SQLite.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;SQLite.dll.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Opt(&amp;quot;TCPTimeout&amp;quot;, 100)&lt;br /&gt;
OnAutoItExitRegister(&amp;quot;OnExit&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
Global $g_sDBPath  = @ScriptDir &amp;amp; &amp;quot;\sheet.db&amp;quot;&lt;br /&gt;
Global $g_sWebRoot = @ScriptDir &amp;amp; &amp;quot;\web&amp;quot;&lt;br /&gt;
Global $g_iPort    = 8765&lt;br /&gt;
Global $g_hDB&lt;br /&gt;
&lt;br /&gt;
_SQLite_Startup()&lt;br /&gt;
If @error Then Exit MsgBox(16, &amp;quot;Err&amp;quot;, &amp;quot;SQLite 시작 실패&amp;quot;)&lt;br /&gt;
$g_hDB = _SQLite_Open($g_sDBPath)&lt;br /&gt;
_SQLite_Exec($g_hDB, _&lt;br /&gt;
    &amp;quot;CREATE TABLE IF NOT EXISTS sheet_data(&amp;quot; &amp;amp; _&lt;br /&gt;
    &amp;quot;  row_idx INTEGER, col_idx INTEGER, value TEXT,&amp;quot; &amp;amp; _&lt;br /&gt;
    &amp;quot;  PRIMARY KEY(row_idx, col_idx));&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
TCPStartup()&lt;br /&gt;
Local $iListen = TCPListen(&amp;quot;127.0.0.1&amp;quot;, $g_iPort)&lt;br /&gt;
If $iListen = -1 Then Exit MsgBox(16, &amp;quot;Err&amp;quot;, &amp;quot;포트 사용중: &amp;quot; &amp;amp; $g_iPort)&lt;br /&gt;
&lt;br /&gt;
ShellExecute(&amp;quot;http://127.0.0.1:&amp;quot; &amp;amp; $g_iPort &amp;amp; &amp;quot;/&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
While 1&lt;br /&gt;
    Local $iSock = TCPAccept($iListen)&lt;br /&gt;
    If $iSock &amp;gt;= 0 Then _Handle($iSock)&lt;br /&gt;
    Sleep(10)&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
Func _Handle($iSock)&lt;br /&gt;
    Local $sReq = &amp;quot;&amp;quot;, $iWait = 0&lt;br /&gt;
    While $iWait &amp;lt; 300&lt;br /&gt;
        Local $c = TCPRecv($iSock, 8192)&lt;br /&gt;
        If $c &amp;lt;&amp;gt; &amp;quot;&amp;quot; Then&lt;br /&gt;
            $sReq &amp;amp;= $c&lt;br /&gt;
            If StringInStr($sReq, @CRLF &amp;amp; @CRLF) Then ExitLoop&lt;br /&gt;
        EndIf&lt;br /&gt;
        Sleep(5)&lt;br /&gt;
        $iWait += 1&lt;br /&gt;
    WEnd&lt;br /&gt;
&lt;br /&gt;
    Local $iSplit = StringInStr($sReq, @CRLF &amp;amp; @CRLF)&lt;br /&gt;
    Local $sHead  = StringLeft($sReq, $iSplit - 1)&lt;br /&gt;
    Local $sBody  = StringMid($sReq, $iSplit + 3)&lt;br /&gt;
&lt;br /&gt;
    Local $aCL = StringRegExp($sHead, &amp;quot;(?i)Content-Length:\s*(\d+)&amp;quot;, 1)&lt;br /&gt;
    Local $iCL = (IsArray($aCL)) ? Int($aCL[0]) : 0&lt;br /&gt;
    While StringLen(BinaryToString(StringToBinary($sBody, 4), 4)) &amp;lt; $iCL&lt;br /&gt;
        Local $c = TCPRecv($iSock, 8192)&lt;br /&gt;
        If $c &amp;lt;&amp;gt; &amp;quot;&amp;quot; Then $sBody &amp;amp;= $c&lt;br /&gt;
        Sleep(5)&lt;br /&gt;
    WEnd&lt;br /&gt;
&lt;br /&gt;
    Local $aFirst = StringRegExp($sHead, &amp;quot;^(\S+)\s+(\S+)&amp;quot;, 1)&lt;br /&gt;
    If Not IsArray($aFirst) Then&lt;br /&gt;
        TCPCloseSocket($iSock)&lt;br /&gt;
        Return&lt;br /&gt;
    EndIf&lt;br /&gt;
    Local $sMethod = $aFirst[0], $sPath = $aFirst[1]&lt;br /&gt;
&lt;br /&gt;
    Switch $sPath&lt;br /&gt;
        Case &amp;quot;/&amp;quot;, &amp;quot;/index.html&amp;quot;&lt;br /&gt;
            _SendFile($iSock, $g_sWebRoot &amp;amp; &amp;quot;\index.html&amp;quot;, &amp;quot;text/html; charset=utf-8&amp;quot;)&lt;br /&gt;
        Case &amp;quot;/api/load&amp;quot;&lt;br /&gt;
            _ApiLoad($iSock)&lt;br /&gt;
        Case &amp;quot;/api/save&amp;quot;&lt;br /&gt;
            _ApiSave($iSock, $sBody)&lt;br /&gt;
        Case &amp;quot;/api/clear&amp;quot;&lt;br /&gt;
            _SQLite_Exec($g_hDB, &amp;quot;DELETE FROM sheet_data;&amp;quot;)&lt;br /&gt;
            _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:true}&#039;)&lt;br /&gt;
        Case Else&lt;br /&gt;
            _Send404($iSock)&lt;br /&gt;
    EndSwitch&lt;br /&gt;
&lt;br /&gt;
    TCPCloseSocket($iSock)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ApiLoad($iSock)&lt;br /&gt;
    Local $aRes, $iRows, $iCols&lt;br /&gt;
    _SQLite_GetTable2d($g_hDB, _&lt;br /&gt;
        &amp;quot;SELECT row_idx,col_idx,value FROM sheet_data ORDER BY row_idx,col_idx;&amp;quot;, _&lt;br /&gt;
        $aRes, $iRows, $iCols)&lt;br /&gt;
    Local $s = &#039;{&amp;quot;cells&amp;quot;:[&#039;&lt;br /&gt;
    For $i = 1 To $iRows&lt;br /&gt;
        If $i &amp;gt; 1 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
        $s &amp;amp;= &#039;{&amp;quot;r&amp;quot;:&#039; &amp;amp; $aRes[$i][0] &amp;amp; &#039;,&amp;quot;c&amp;quot;:&#039; &amp;amp; $aRes[$i][1] &amp;amp; _&lt;br /&gt;
              &#039;,&amp;quot;v&amp;quot;:&#039; &amp;amp; _JsonStr($aRes[$i][2]) &amp;amp; &#039;}&#039;&lt;br /&gt;
    Next&lt;br /&gt;
    $s &amp;amp;= &#039;]}&#039;&lt;br /&gt;
    _SendJson($iSock, $s)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ApiSave($iSock, $sBody)&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;BEGIN;&amp;quot;)&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;DELETE FROM sheet_data;&amp;quot;)&lt;br /&gt;
    Local $aM = StringRegExp($sBody, _&lt;br /&gt;
        &#039;\{&amp;quot;r&amp;quot;:(\d+),&amp;quot;c&amp;quot;:(\d+),&amp;quot;v&amp;quot;:&amp;quot;((?:\\.|[^&amp;quot;\\])*)&amp;quot;\}&#039;, 3)&lt;br /&gt;
    If Not @error Then&lt;br /&gt;
        Local $sStmt&lt;br /&gt;
        For $i = 0 To UBound($aM) - 1 Step 3&lt;br /&gt;
            Local $v = _JsonUnesc($aM[$i + 2])&lt;br /&gt;
            $v = StringReplace($v, &amp;quot;&#039;&amp;quot;, &amp;quot;&#039;&#039;&amp;quot;)&lt;br /&gt;
            _SQLite_Exec($g_hDB, _&lt;br /&gt;
                &amp;quot;INSERT INTO sheet_data VALUES(&amp;quot; &amp;amp; _&lt;br /&gt;
                $aM[$i] &amp;amp; &amp;quot;,&amp;quot; &amp;amp; $aM[$i+1] &amp;amp; &amp;quot;,&#039;&amp;quot; &amp;amp; $v &amp;amp; &amp;quot;&#039;);&amp;quot;)&lt;br /&gt;
        Next&lt;br /&gt;
    EndIf&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;COMMIT;&amp;quot;)&lt;br /&gt;
    _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:true}&#039;)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendFile($iSock, $sFile, $sCT)&lt;br /&gt;
    If Not FileExists($sFile) Then Return _Send404($iSock)&lt;br /&gt;
    Local $h = FileOpen($sFile, 16)&lt;br /&gt;
    Local $b = FileRead($h)&lt;br /&gt;
    FileClose($h)&lt;br /&gt;
    Local $sH = &amp;quot;HTTP/1.1 200 OK&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Type: &amp;quot; &amp;amp; $sCT &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Length: &amp;quot; &amp;amp; BinaryLen($b) &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Connection: close&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF&lt;br /&gt;
    TCPSend($iSock, $sH)&lt;br /&gt;
    TCPSend($iSock, $b)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendJson($iSock, $sJson)&lt;br /&gt;
    Local $b = StringToBinary($sJson, 4)&lt;br /&gt;
    Local $sH = &amp;quot;HTTP/1.1 200 OK&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Type: application/json; charset=utf-8&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Length: &amp;quot; &amp;amp; BinaryLen($b) &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Connection: close&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF&lt;br /&gt;
    TCPSend($iSock, $sH)&lt;br /&gt;
    TCPSend($iSock, $b)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _Send404($iSock)&lt;br /&gt;
    TCPSend($iSock, &amp;quot;HTTP/1.1 404 Not Found&amp;quot; &amp;amp; @CRLF &amp;amp; &amp;quot;Content-Length: 0&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _JsonStr($s)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\&amp;quot;, &amp;quot;\\&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, &#039;&amp;quot;&#039;, &#039;\&amp;quot;&#039;)&lt;br /&gt;
    $s = StringReplace($s, @CR, &amp;quot;\r&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, @LF, &amp;quot;\n&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, @TAB, &amp;quot;\t&amp;quot;)&lt;br /&gt;
    Return &#039;&amp;quot;&#039; &amp;amp; $s &amp;amp; &#039;&amp;quot;&#039;&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _JsonUnesc($s)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\n&amp;quot;, @LF)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\r&amp;quot;, @CR)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\t&amp;quot;, @TAB)&lt;br /&gt;
    $s = StringReplace($s, &#039;\&amp;quot;&#039;, &#039;&amp;quot;&#039;)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\\&amp;quot;, &amp;quot;\&amp;quot;)&lt;br /&gt;
    Return $s&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func OnExit()&lt;br /&gt;
    _SQLite_Close($g_hDB)&lt;br /&gt;
    _SQLite_Shutdown()&lt;br /&gt;
    TCPShutdown()&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
web/index.html&lt;br /&gt;
&amp;lt;source lang=html&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ko&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;웹 시트 (AutoIt + SQLite)&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/jspreadsheet.css&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdn.jsdelivr.net/npm/jsuites@4/dist/jsuites.css&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  body { font-family:&#039;맑은 고딕&#039;,sans-serif; padding:12px; background:#fafafa; }&lt;br /&gt;
  .bar { margin-bottom:10px; display:flex; align-items:center; gap:8px; }&lt;br /&gt;
  button { padding:7px 14px; border:1px solid #ccc; background:#fff;&lt;br /&gt;
           border-radius:4px; cursor:pointer; }&lt;br /&gt;
  button:hover { background:#f0f0f0; }&lt;br /&gt;
  #status { margin-left:10px; color:#555; font-size:13px; }&lt;br /&gt;
  h2 { margin:0 0 12px; font-size:18px; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&amp;lt;h2&amp;gt;📊 웹 스프레드시트&amp;lt;/h2&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;bar&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;saveAll()&amp;quot;&amp;gt;💾 저장&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;loadAll()&amp;quot;&amp;gt;🔄 불러오기&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;clearAll()&amp;quot;&amp;gt;🗑 전체삭제&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;exportCsv()&amp;quot;&amp;gt;📤 CSV&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;span id=&amp;quot;status&amp;quot;&amp;gt;준비&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;sheet&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/jsuites@4/dist/jsuites.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/jspreadsheet.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
const COLS = 10, ROWS = 50;&lt;br /&gt;
let sheet;&lt;br /&gt;
const setStatus = s =&amp;gt; document.getElementById(&#039;status&#039;).textContent = s;&lt;br /&gt;
const emptyData = () =&amp;gt; Array.from({length:ROWS}, () =&amp;gt; Array(COLS).fill(&#039;&#039;));&lt;br /&gt;
&lt;br /&gt;
function initSheet(data){&lt;br /&gt;
  const el = document.getElementById(&#039;sheet&#039;);&lt;br /&gt;
  el.innerHTML = &#039;&#039;;&lt;br /&gt;
  sheet = jspreadsheet(el, {&lt;br /&gt;
    data,&lt;br /&gt;
    columns: Array.from({length:COLS}, (_,i) =&amp;gt; ({&lt;br /&gt;
      title: String.fromCharCode(65+i), width: 110&lt;br /&gt;
    })),&lt;br /&gt;
    allowInsertRow: true,&lt;br /&gt;
    minDimensions: [COLS, ROWS],&lt;br /&gt;
    onchange: () =&amp;gt; setStatus(&#039;수정됨 (저장 필요)&#039;)&lt;br /&gt;
  });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function loadAll(){&lt;br /&gt;
  setStatus(&#039;불러오는 중...&#039;);&lt;br /&gt;
  const data = emptyData();&lt;br /&gt;
  const r = await fetch(&#039;/api/load&#039;);&lt;br /&gt;
  const j = await r.json();&lt;br /&gt;
  for (const c of j.cells){&lt;br /&gt;
    while (data.length &amp;lt;= c.r) data.push(Array(COLS).fill(&#039;&#039;));&lt;br /&gt;
    while (data[c.r].length &amp;lt;= c.c) data[c.r].push(&#039;&#039;);&lt;br /&gt;
    data[c.r][c.c] = c.v;&lt;br /&gt;
  }&lt;br /&gt;
  initSheet(data);&lt;br /&gt;
  setStatus(`불러옴 (${j.cells.length}셀)`);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function saveAll(){&lt;br /&gt;
  setStatus(&#039;저장 중...&#039;);&lt;br /&gt;
  const data = sheet.getData();&lt;br /&gt;
  const cells = [];&lt;br /&gt;
  for (let r=0; r&amp;lt;data.length; r++)&lt;br /&gt;
    for (let c=0; c&amp;lt;data[r].length; c++){&lt;br /&gt;
      const v = data[r][c];&lt;br /&gt;
      if (v !== &#039;&#039; &amp;amp;&amp;amp; v != null) cells.push({r, c, v: String(v)});&lt;br /&gt;
    }&lt;br /&gt;
  const res = await fetch(&#039;/api/save&#039;, {&lt;br /&gt;
    method:&#039;POST&#039;,&lt;br /&gt;
    headers:{&#039;Content-Type&#039;:&#039;application/json&#039;},&lt;br /&gt;
    body: JSON.stringify({cells})&lt;br /&gt;
  });&lt;br /&gt;
  const j = await res.json();&lt;br /&gt;
  setStatus(j.ok ? `저장 완료 (${cells.length}셀)` : &#039;저장 실패&#039;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function clearAll(){&lt;br /&gt;
  if (!confirm(&#039;전체 데이터를 삭제할까요?&#039;)) return;&lt;br /&gt;
  await fetch(&#039;/api/clear&#039;, {method:&#039;POST&#039;});&lt;br /&gt;
  initSheet(emptyData());&lt;br /&gt;
  setStatus(&#039;삭제됨&#039;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function exportCsv(){&lt;br /&gt;
  const data = sheet.getData();&lt;br /&gt;
  const csv = data.map(r =&amp;gt; r.map(v =&amp;gt;&lt;br /&gt;
    `&amp;quot;${String(v ?? &#039;&#039;).replace(/&amp;quot;/g,&#039;&amp;quot;&amp;quot;&#039;)}&amp;quot;`).join(&#039;,&#039;)).join(&#039;\n&#039;);&lt;br /&gt;
  const blob = new Blob([&#039;\uFEFF&#039;+csv], {type:&#039;text/csv;charset=utf-8&#039;});&lt;br /&gt;
  const a = document.createElement(&#039;a&#039;);&lt;br /&gt;
  a.href = URL.createObjectURL(blob);&lt;br /&gt;
  a.download = &#039;sheet.csv&#039;;&lt;br /&gt;
  a.click();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
loadAll();&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
실행 방법&lt;br /&gt;
&lt;br /&gt;
	1.	위 구조대로 폴더 생성, 두 파일 저장&lt;br /&gt;
	2.	sqlite3.dll을 main.au3 옆에 두기 (AutoIt SQLite UDF 폴더에 포함)&lt;br /&gt;
	3.	main.au3 더블클릭 → 브라우저 자동 오픈&lt;br /&gt;
	4.	셀에 입력 후 💾 저장 → SQLite에 즉시 저장됨&lt;br /&gt;
&lt;br /&gt;
확장 포인트&lt;br /&gt;
&lt;br /&gt;
	•	시트 여러 개: sheet_data에 sheet_name 컬럼 추가 + 상단 탭 UI&lt;br /&gt;
	•	셀 서식: value 외에 style_json 컬럼 추가, jspreadsheet getStyle()/setStyle() 활용&lt;br /&gt;
	•	수식: jspreadsheet-ce가 기본 SUM/AVG 등 지원 — 자동 동작함&lt;br /&gt;
	•	오라클 연동: /api/oracle/query 엔드포인트 추가해서 [[AutoIt에서 sqlplus.exe 호출 결과를 그리드]]에 표시&lt;br /&gt;
&lt;br /&gt;
CSV import나 다중 시트, 수식 자동계산 결과 저장 등 원하시는 방향 있으면 이어서 만들어 드릴게요.&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=Autoit_web_%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&amp;diff=2410</id>
		<title>Autoit web 하이브리드 프로그래밍</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=Autoit_web_%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&amp;diff=2410"/>
		<updated>2026-05-18T10:03:55Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: 좋습니다. AutoIt이 로컬 HTTP 서버 + SQLite 백엔드 역할을 하고, 브라우저에서 jspreadsheet-ce(엑셀형 그리드)로 화면을 띄우는 구조로 만들어 드릴게요.  구조 &amp;lt;source lang=bash&amp;gt; project/ ├── main.au3          ← HTTP 서버 + SQLite ├── sqlite3.dll       ← SQLite UDF용 (AutoIt SQLite UDF에 포함) └── web/     └── index.html    ← 엑셀형 UI &amp;lt;/source&amp;gt;   동작: AutoIt이 127.0.0.1:8765 에서 listen →...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;좋습니다. AutoIt이 로컬 HTTP 서버 + SQLite 백엔드 역할을 하고, 브라우저에서 jspreadsheet-ce(엑셀형 그리드)로 화면을 띄우는 구조로 만들어 드릴게요.&lt;br /&gt;
&lt;br /&gt;
구조&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
project/&lt;br /&gt;
├── main.au3          ← HTTP 서버 + SQLite&lt;br /&gt;
├── sqlite3.dll       ← SQLite UDF용 (AutoIt SQLite UDF에 포함)&lt;br /&gt;
└── web/&lt;br /&gt;
    └── index.html    ← 엑셀형 UI&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
동작: AutoIt이 127.0.0.1:8765 에서 listen → 자동으로 기본 브라우저 오픈 → 그리드에서 입력/수정 → /api/save, /api/load 로 SQLite와 통신.&lt;br /&gt;
&lt;br /&gt;
main.au3&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
#include &amp;lt;SQLite.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;SQLite.dll.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Opt(&amp;quot;TCPTimeout&amp;quot;, 100)&lt;br /&gt;
OnAutoItExitRegister(&amp;quot;OnExit&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
Global $g_sDBPath  = @ScriptDir &amp;amp; &amp;quot;\sheet.db&amp;quot;&lt;br /&gt;
Global $g_sWebRoot = @ScriptDir &amp;amp; &amp;quot;\web&amp;quot;&lt;br /&gt;
Global $g_iPort    = 8765&lt;br /&gt;
Global $g_hDB&lt;br /&gt;
&lt;br /&gt;
_SQLite_Startup()&lt;br /&gt;
If @error Then Exit MsgBox(16, &amp;quot;Err&amp;quot;, &amp;quot;SQLite 시작 실패&amp;quot;)&lt;br /&gt;
$g_hDB = _SQLite_Open($g_sDBPath)&lt;br /&gt;
_SQLite_Exec($g_hDB, _&lt;br /&gt;
    &amp;quot;CREATE TABLE IF NOT EXISTS sheet_data(&amp;quot; &amp;amp; _&lt;br /&gt;
    &amp;quot;  row_idx INTEGER, col_idx INTEGER, value TEXT,&amp;quot; &amp;amp; _&lt;br /&gt;
    &amp;quot;  PRIMARY KEY(row_idx, col_idx));&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
TCPStartup()&lt;br /&gt;
Local $iListen = TCPListen(&amp;quot;127.0.0.1&amp;quot;, $g_iPort)&lt;br /&gt;
If $iListen = -1 Then Exit MsgBox(16, &amp;quot;Err&amp;quot;, &amp;quot;포트 사용중: &amp;quot; &amp;amp; $g_iPort)&lt;br /&gt;
&lt;br /&gt;
ShellExecute(&amp;quot;http://127.0.0.1:&amp;quot; &amp;amp; $g_iPort &amp;amp; &amp;quot;/&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
While 1&lt;br /&gt;
    Local $iSock = TCPAccept($iListen)&lt;br /&gt;
    If $iSock &amp;gt;= 0 Then _Handle($iSock)&lt;br /&gt;
    Sleep(10)&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
Func _Handle($iSock)&lt;br /&gt;
    Local $sReq = &amp;quot;&amp;quot;, $iWait = 0&lt;br /&gt;
    While $iWait &amp;lt; 300&lt;br /&gt;
        Local $c = TCPRecv($iSock, 8192)&lt;br /&gt;
        If $c &amp;lt;&amp;gt; &amp;quot;&amp;quot; Then&lt;br /&gt;
            $sReq &amp;amp;= $c&lt;br /&gt;
            If StringInStr($sReq, @CRLF &amp;amp; @CRLF) Then ExitLoop&lt;br /&gt;
        EndIf&lt;br /&gt;
        Sleep(5)&lt;br /&gt;
        $iWait += 1&lt;br /&gt;
    WEnd&lt;br /&gt;
&lt;br /&gt;
    Local $iSplit = StringInStr($sReq, @CRLF &amp;amp; @CRLF)&lt;br /&gt;
    Local $sHead  = StringLeft($sReq, $iSplit - 1)&lt;br /&gt;
    Local $sBody  = StringMid($sReq, $iSplit + 3)&lt;br /&gt;
&lt;br /&gt;
    Local $aCL = StringRegExp($sHead, &amp;quot;(?i)Content-Length:\s*(\d+)&amp;quot;, 1)&lt;br /&gt;
    Local $iCL = (IsArray($aCL)) ? Int($aCL[0]) : 0&lt;br /&gt;
    While StringLen(BinaryToString(StringToBinary($sBody, 4), 4)) &amp;lt; $iCL&lt;br /&gt;
        Local $c = TCPRecv($iSock, 8192)&lt;br /&gt;
        If $c &amp;lt;&amp;gt; &amp;quot;&amp;quot; Then $sBody &amp;amp;= $c&lt;br /&gt;
        Sleep(5)&lt;br /&gt;
    WEnd&lt;br /&gt;
&lt;br /&gt;
    Local $aFirst = StringRegExp($sHead, &amp;quot;^(\S+)\s+(\S+)&amp;quot;, 1)&lt;br /&gt;
    If Not IsArray($aFirst) Then&lt;br /&gt;
        TCPCloseSocket($iSock)&lt;br /&gt;
        Return&lt;br /&gt;
    EndIf&lt;br /&gt;
    Local $sMethod = $aFirst[0], $sPath = $aFirst[1]&lt;br /&gt;
&lt;br /&gt;
    Switch $sPath&lt;br /&gt;
        Case &amp;quot;/&amp;quot;, &amp;quot;/index.html&amp;quot;&lt;br /&gt;
            _SendFile($iSock, $g_sWebRoot &amp;amp; &amp;quot;\index.html&amp;quot;, &amp;quot;text/html; charset=utf-8&amp;quot;)&lt;br /&gt;
        Case &amp;quot;/api/load&amp;quot;&lt;br /&gt;
            _ApiLoad($iSock)&lt;br /&gt;
        Case &amp;quot;/api/save&amp;quot;&lt;br /&gt;
            _ApiSave($iSock, $sBody)&lt;br /&gt;
        Case &amp;quot;/api/clear&amp;quot;&lt;br /&gt;
            _SQLite_Exec($g_hDB, &amp;quot;DELETE FROM sheet_data;&amp;quot;)&lt;br /&gt;
            _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:true}&#039;)&lt;br /&gt;
        Case Else&lt;br /&gt;
            _Send404($iSock)&lt;br /&gt;
    EndSwitch&lt;br /&gt;
&lt;br /&gt;
    TCPCloseSocket($iSock)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ApiLoad($iSock)&lt;br /&gt;
    Local $aRes, $iRows, $iCols&lt;br /&gt;
    _SQLite_GetTable2d($g_hDB, _&lt;br /&gt;
        &amp;quot;SELECT row_idx,col_idx,value FROM sheet_data ORDER BY row_idx,col_idx;&amp;quot;, _&lt;br /&gt;
        $aRes, $iRows, $iCols)&lt;br /&gt;
    Local $s = &#039;{&amp;quot;cells&amp;quot;:[&#039;&lt;br /&gt;
    For $i = 1 To $iRows&lt;br /&gt;
        If $i &amp;gt; 1 Then $s &amp;amp;= &amp;quot;,&amp;quot;&lt;br /&gt;
        $s &amp;amp;= &#039;{&amp;quot;r&amp;quot;:&#039; &amp;amp; $aRes[$i][0] &amp;amp; &#039;,&amp;quot;c&amp;quot;:&#039; &amp;amp; $aRes[$i][1] &amp;amp; _&lt;br /&gt;
              &#039;,&amp;quot;v&amp;quot;:&#039; &amp;amp; _JsonStr($aRes[$i][2]) &amp;amp; &#039;}&#039;&lt;br /&gt;
    Next&lt;br /&gt;
    $s &amp;amp;= &#039;]}&#039;&lt;br /&gt;
    _SendJson($iSock, $s)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _ApiSave($iSock, $sBody)&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;BEGIN;&amp;quot;)&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;DELETE FROM sheet_data;&amp;quot;)&lt;br /&gt;
    Local $aM = StringRegExp($sBody, _&lt;br /&gt;
        &#039;\{&amp;quot;r&amp;quot;:(\d+),&amp;quot;c&amp;quot;:(\d+),&amp;quot;v&amp;quot;:&amp;quot;((?:\\.|[^&amp;quot;\\])*)&amp;quot;\}&#039;, 3)&lt;br /&gt;
    If Not @error Then&lt;br /&gt;
        Local $sStmt&lt;br /&gt;
        For $i = 0 To UBound($aM) - 1 Step 3&lt;br /&gt;
            Local $v = _JsonUnesc($aM[$i + 2])&lt;br /&gt;
            $v = StringReplace($v, &amp;quot;&#039;&amp;quot;, &amp;quot;&#039;&#039;&amp;quot;)&lt;br /&gt;
            _SQLite_Exec($g_hDB, _&lt;br /&gt;
                &amp;quot;INSERT INTO sheet_data VALUES(&amp;quot; &amp;amp; _&lt;br /&gt;
                $aM[$i] &amp;amp; &amp;quot;,&amp;quot; &amp;amp; $aM[$i+1] &amp;amp; &amp;quot;,&#039;&amp;quot; &amp;amp; $v &amp;amp; &amp;quot;&#039;);&amp;quot;)&lt;br /&gt;
        Next&lt;br /&gt;
    EndIf&lt;br /&gt;
    _SQLite_Exec($g_hDB, &amp;quot;COMMIT;&amp;quot;)&lt;br /&gt;
    _SendJson($iSock, &#039;{&amp;quot;ok&amp;quot;:true}&#039;)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendFile($iSock, $sFile, $sCT)&lt;br /&gt;
    If Not FileExists($sFile) Then Return _Send404($iSock)&lt;br /&gt;
    Local $h = FileOpen($sFile, 16)&lt;br /&gt;
    Local $b = FileRead($h)&lt;br /&gt;
    FileClose($h)&lt;br /&gt;
    Local $sH = &amp;quot;HTTP/1.1 200 OK&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Type: &amp;quot; &amp;amp; $sCT &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Length: &amp;quot; &amp;amp; BinaryLen($b) &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Connection: close&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF&lt;br /&gt;
    TCPSend($iSock, $sH)&lt;br /&gt;
    TCPSend($iSock, $b)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendJson($iSock, $sJson)&lt;br /&gt;
    Local $b = StringToBinary($sJson, 4)&lt;br /&gt;
    Local $sH = &amp;quot;HTTP/1.1 200 OK&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Type: application/json; charset=utf-8&amp;quot; &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Content-Length: &amp;quot; &amp;amp; BinaryLen($b) &amp;amp; @CRLF &amp;amp; _&lt;br /&gt;
        &amp;quot;Connection: close&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF&lt;br /&gt;
    TCPSend($iSock, $sH)&lt;br /&gt;
    TCPSend($iSock, $b)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _Send404($iSock)&lt;br /&gt;
    TCPSend($iSock, &amp;quot;HTTP/1.1 404 Not Found&amp;quot; &amp;amp; @CRLF &amp;amp; &amp;quot;Content-Length: 0&amp;quot; &amp;amp; @CRLF &amp;amp; @CRLF)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _JsonStr($s)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\&amp;quot;, &amp;quot;\\&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, &#039;&amp;quot;&#039;, &#039;\&amp;quot;&#039;)&lt;br /&gt;
    $s = StringReplace($s, @CR, &amp;quot;\r&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, @LF, &amp;quot;\n&amp;quot;)&lt;br /&gt;
    $s = StringReplace($s, @TAB, &amp;quot;\t&amp;quot;)&lt;br /&gt;
    Return &#039;&amp;quot;&#039; &amp;amp; $s &amp;amp; &#039;&amp;quot;&#039;&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _JsonUnesc($s)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\n&amp;quot;, @LF)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\r&amp;quot;, @CR)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\t&amp;quot;, @TAB)&lt;br /&gt;
    $s = StringReplace($s, &#039;\&amp;quot;&#039;, &#039;&amp;quot;&#039;)&lt;br /&gt;
    $s = StringReplace($s, &amp;quot;\\&amp;quot;, &amp;quot;\&amp;quot;)&lt;br /&gt;
    Return $s&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func OnExit()&lt;br /&gt;
    _SQLite_Close($g_hDB)&lt;br /&gt;
    _SQLite_Shutdown()&lt;br /&gt;
    TCPShutdown()&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
web/index.html&lt;br /&gt;
&amp;lt;source lang=html&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ko&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
&amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;title&amp;gt;웹 시트 (AutoIt + SQLite)&amp;lt;/title&amp;gt;&lt;br /&gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/jspreadsheet.css&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://cdn.jsdelivr.net/npm/jsuites@4/dist/jsuites.css&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  body { font-family:&#039;맑은 고딕&#039;,sans-serif; padding:12px; background:#fafafa; }&lt;br /&gt;
  .bar { margin-bottom:10px; display:flex; align-items:center; gap:8px; }&lt;br /&gt;
  button { padding:7px 14px; border:1px solid #ccc; background:#fff;&lt;br /&gt;
           border-radius:4px; cursor:pointer; }&lt;br /&gt;
  button:hover { background:#f0f0f0; }&lt;br /&gt;
  #status { margin-left:10px; color:#555; font-size:13px; }&lt;br /&gt;
  h2 { margin:0 0 12px; font-size:18px; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&amp;lt;h2&amp;gt;📊 웹 스프레드시트&amp;lt;/h2&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;bar&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;saveAll()&amp;quot;&amp;gt;💾 저장&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;loadAll()&amp;quot;&amp;gt;🔄 불러오기&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;clearAll()&amp;quot;&amp;gt;🗑 전체삭제&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;button onclick=&amp;quot;exportCsv()&amp;quot;&amp;gt;📤 CSV&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;span id=&amp;quot;status&amp;quot;&amp;gt;준비&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;sheet&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/jsuites@4/dist/jsuites.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/jspreadsheet-ce@4/dist/jspreadsheet.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
const COLS = 10, ROWS = 50;&lt;br /&gt;
let sheet;&lt;br /&gt;
const setStatus = s =&amp;gt; document.getElementById(&#039;status&#039;).textContent = s;&lt;br /&gt;
const emptyData = () =&amp;gt; Array.from({length:ROWS}, () =&amp;gt; Array(COLS).fill(&#039;&#039;));&lt;br /&gt;
&lt;br /&gt;
function initSheet(data){&lt;br /&gt;
  const el = document.getElementById(&#039;sheet&#039;);&lt;br /&gt;
  el.innerHTML = &#039;&#039;;&lt;br /&gt;
  sheet = jspreadsheet(el, {&lt;br /&gt;
    data,&lt;br /&gt;
    columns: Array.from({length:COLS}, (_,i) =&amp;gt; ({&lt;br /&gt;
      title: String.fromCharCode(65+i), width: 110&lt;br /&gt;
    })),&lt;br /&gt;
    allowInsertRow: true,&lt;br /&gt;
    minDimensions: [COLS, ROWS],&lt;br /&gt;
    onchange: () =&amp;gt; setStatus(&#039;수정됨 (저장 필요)&#039;)&lt;br /&gt;
  });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function loadAll(){&lt;br /&gt;
  setStatus(&#039;불러오는 중...&#039;);&lt;br /&gt;
  const data = emptyData();&lt;br /&gt;
  const r = await fetch(&#039;/api/load&#039;);&lt;br /&gt;
  const j = await r.json();&lt;br /&gt;
  for (const c of j.cells){&lt;br /&gt;
    while (data.length &amp;lt;= c.r) data.push(Array(COLS).fill(&#039;&#039;));&lt;br /&gt;
    while (data[c.r].length &amp;lt;= c.c) data[c.r].push(&#039;&#039;);&lt;br /&gt;
    data[c.r][c.c] = c.v;&lt;br /&gt;
  }&lt;br /&gt;
  initSheet(data);&lt;br /&gt;
  setStatus(`불러옴 (${j.cells.length}셀)`);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function saveAll(){&lt;br /&gt;
  setStatus(&#039;저장 중...&#039;);&lt;br /&gt;
  const data = sheet.getData();&lt;br /&gt;
  const cells = [];&lt;br /&gt;
  for (let r=0; r&amp;lt;data.length; r++)&lt;br /&gt;
    for (let c=0; c&amp;lt;data[r].length; c++){&lt;br /&gt;
      const v = data[r][c];&lt;br /&gt;
      if (v !== &#039;&#039; &amp;amp;&amp;amp; v != null) cells.push({r, c, v: String(v)});&lt;br /&gt;
    }&lt;br /&gt;
  const res = await fetch(&#039;/api/save&#039;, {&lt;br /&gt;
    method:&#039;POST&#039;,&lt;br /&gt;
    headers:{&#039;Content-Type&#039;:&#039;application/json&#039;},&lt;br /&gt;
    body: JSON.stringify({cells})&lt;br /&gt;
  });&lt;br /&gt;
  const j = await res.json();&lt;br /&gt;
  setStatus(j.ok ? `저장 완료 (${cells.length}셀)` : &#039;저장 실패&#039;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function clearAll(){&lt;br /&gt;
  if (!confirm(&#039;전체 데이터를 삭제할까요?&#039;)) return;&lt;br /&gt;
  await fetch(&#039;/api/clear&#039;, {method:&#039;POST&#039;});&lt;br /&gt;
  initSheet(emptyData());&lt;br /&gt;
  setStatus(&#039;삭제됨&#039;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function exportCsv(){&lt;br /&gt;
  const data = sheet.getData();&lt;br /&gt;
  const csv = data.map(r =&amp;gt; r.map(v =&amp;gt;&lt;br /&gt;
    `&amp;quot;${String(v ?? &#039;&#039;).replace(/&amp;quot;/g,&#039;&amp;quot;&amp;quot;&#039;)}&amp;quot;`).join(&#039;,&#039;)).join(&#039;\n&#039;);&lt;br /&gt;
  const blob = new Blob([&#039;\uFEFF&#039;+csv], {type:&#039;text/csv;charset=utf-8&#039;});&lt;br /&gt;
  const a = document.createElement(&#039;a&#039;);&lt;br /&gt;
  a.href = URL.createObjectURL(blob);&lt;br /&gt;
  a.download = &#039;sheet.csv&#039;;&lt;br /&gt;
  a.click();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
loadAll();&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
실행 방법&lt;br /&gt;
&lt;br /&gt;
	1.	위 구조대로 폴더 생성, 두 파일 저장&lt;br /&gt;
	2.	sqlite3.dll을 main.au3 옆에 두기 (AutoIt SQLite UDF 폴더에 포함)&lt;br /&gt;
	3.	main.au3 더블클릭 → 브라우저 자동 오픈&lt;br /&gt;
	4.	셀에 입력 후 💾 저장 → SQLite에 즉시 저장됨&lt;br /&gt;
&lt;br /&gt;
확장 포인트&lt;br /&gt;
&lt;br /&gt;
	•	시트 여러 개: sheet_data에 sheet_name 컬럼 추가 + 상단 탭 UI&lt;br /&gt;
	•	셀 서식: value 외에 style_json 컬럼 추가, jspreadsheet getStyle()/setStyle() 활용&lt;br /&gt;
	•	수식: jspreadsheet-ce가 기본 SUM/AVG 등 지원 — 자동 동작함&lt;br /&gt;
	•	오라클 연동: /api/oracle/query 엔드포인트 추가해서 AutoIt에서 sqlplus.exe 호출 결과를 그리드에 표시&lt;br /&gt;
&lt;br /&gt;
CSV import나 다중 시트, 수식 자동계산 결과 저장 등 원하시는 방향 있으면 이어서 만들어 드릴게요.&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=3D_%ED%94%84%EB%A6%B0%ED%84%B0_%EC%B6%9C%EB%A0%A5_%EC%A0%9C%ED%92%88_%EB%A7%8C%EC%A1%B1%EB%8F%84_%ED%8F%89%EA%B0%80&amp;diff=2409</id>
		<title>3D 프린터 출력 제품 만족도 평가</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=3D_%ED%94%84%EB%A6%B0%ED%84%B0_%EC%B6%9C%EB%A0%A5_%EC%A0%9C%ED%92%88_%EB%A7%8C%EC%A1%B1%EB%8F%84_%ED%8F%89%EA%B0%80&amp;diff=2409"/>
		<updated>2026-05-18T01:24:23Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 펜꽃이 - 고양이 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 3D 프린터 출력 제품 만족도 평가 ==&lt;br /&gt;
&lt;br /&gt;
=== 헤드폰/헤드셋 홀더 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US68fc4dae065e8f/design/2025-03-28_1feae62045c1a.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/DSM00000001219205/design/2025-03-17_0fc3095691aa8.jpg&lt;br /&gt;
&lt;br /&gt;
=== 소니 XM3 및 XM4 충전 거치대 (WH-1000) ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US127c0899d75255/design/2025-01-03_1ccc9333d2ef4.png&lt;br /&gt;
&lt;br /&gt;
* WH-1000XM5 / WH-1000XM4 무게추 충전 거치대&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US89bdb97c2a11aa/design/2025-01-16_42a440f0c55ea8.jpeg&lt;br /&gt;
&lt;br /&gt;
=== 헤드폰 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USad02f14f272221/design/2025-07-04_861bd3495ed6d8.png&lt;br /&gt;
&lt;br /&gt;
=== 버니어 캘리퍼스 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US20b733ea7bdd6/design/2024-02-22_2ef61241b4db6.png&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US4943f807c219bf/design/2023-12-30_a8fedab5e5305.png&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US4943f807c219bf/design/2023-12-30_c632a791e240b.png&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 - 고양이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 미정 &lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd337a77cf632a7/design/2024-08-17_066550a2de59c.jpg&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 - 모니터 부착용 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd2cf925b88aa0a/design/2025-02-18_1e04ddfa81fe3.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd2cf925b88aa0a/design/2025-02-18_6b702547228c9.jpg&lt;br /&gt;
&lt;br /&gt;
=== USB 보관함 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/DSM00000000216210/design/2024-03-01_hmkduzq4wqx1.jpg&lt;br /&gt;
=== 약 보관함(휴대용) ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 3점&lt;br /&gt;
|별3=3&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USf676c682197ca9/design/2025-06-10_0f13b4ae848588.jpg&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 휴대폰 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 3점&lt;br /&gt;
|별3=3&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US3241ce462ee634/design/2024-07-25_e2ca3cb3c0ddf.jpg&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US3241ce462ee634/design/2024-07-25_31cd10b1eb459.jpg&lt;br /&gt;
&lt;br /&gt;
=== 안경 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점 (뿔테에 적합)&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US1c1d2849305c5b/design/2024-10-07_b5f15c6035ce18.jpg&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/687240/615928/ratings/23179640-9bbc-11ef-bc9c-edb68d07a32b.jpg&lt;br /&gt;
&lt;br /&gt;
=== 피규어 - 모니터위의 고양이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US41a761b447d9ab/design/2024-12-06_b5af18e3af28a.jpg&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 수건걸이 - 와이프 추천  ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USbe8cfffdbf83ac/design/2025-04-16_ccf14e2be08de8.png&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USbe8cfffdbf83ac/design/2024-10-26_10e88bc02912f8.gif&lt;br /&gt;
&lt;br /&gt;
=== 구글홈 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USc758a7e304d074/design/2024-02-18_bd74aae30a00f.png&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 마우스 - 로지텍 MX 마스터 3S 마우스 손목 받침대 CARPIO===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US6dd80ef1b184/design/2025-05-20_6ad01270c5a43.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US6dd80ef1b184/design/2025-05-20_80482e1c58adf8.gif&lt;br /&gt;
[[분류:3D프린터]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=3D_%ED%94%84%EB%A6%B0%ED%84%B0_%EC%B6%9C%EB%A0%A5_%EC%A0%9C%ED%92%88_%EB%A7%8C%EC%A1%B1%EB%8F%84_%ED%8F%89%EA%B0%80&amp;diff=2408</id>
		<title>3D 프린터 출력 제품 만족도 평가</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=3D_%ED%94%84%EB%A6%B0%ED%84%B0_%EC%B6%9C%EB%A0%A5_%EC%A0%9C%ED%92%88_%EB%A7%8C%EC%A1%B1%EB%8F%84_%ED%8F%89%EA%B0%80&amp;diff=2408"/>
		<updated>2026-05-18T01:24:13Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 펜꽃이 - 고양이 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 3D 프린터 출력 제품 만족도 평가 ==&lt;br /&gt;
&lt;br /&gt;
=== 헤드폰/헤드셋 홀더 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US68fc4dae065e8f/design/2025-03-28_1feae62045c1a.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/DSM00000001219205/design/2025-03-17_0fc3095691aa8.jpg&lt;br /&gt;
&lt;br /&gt;
=== 소니 XM3 및 XM4 충전 거치대 (WH-1000) ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US127c0899d75255/design/2025-01-03_1ccc9333d2ef4.png&lt;br /&gt;
&lt;br /&gt;
* WH-1000XM5 / WH-1000XM4 무게추 충전 거치대&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US89bdb97c2a11aa/design/2025-01-16_42a440f0c55ea8.jpeg&lt;br /&gt;
&lt;br /&gt;
=== 헤드폰 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USad02f14f272221/design/2025-07-04_861bd3495ed6d8.png&lt;br /&gt;
&lt;br /&gt;
=== 버니어 캘리퍼스 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US20b733ea7bdd6/design/2024-02-22_2ef61241b4db6.png&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US4943f807c219bf/design/2023-12-30_a8fedab5e5305.png&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US4943f807c219bf/design/2023-12-30_c632a791e240b.png&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 - 고양이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 미정 &lt;br /&gt;
|별3=5&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd337a77cf632a7/design/2024-08-17_066550a2de59c.jpg&lt;br /&gt;
&lt;br /&gt;
=== 펜꽃이 - 모니터 부착용 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd2cf925b88aa0a/design/2025-02-18_1e04ddfa81fe3.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USd2cf925b88aa0a/design/2025-02-18_6b702547228c9.jpg&lt;br /&gt;
&lt;br /&gt;
=== USB 보관함 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/DSM00000000216210/design/2024-03-01_hmkduzq4wqx1.jpg&lt;br /&gt;
=== 약 보관함(휴대용) ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 3점&lt;br /&gt;
|별3=3&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USf676c682197ca9/design/2025-06-10_0f13b4ae848588.jpg&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 휴대폰 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 3점&lt;br /&gt;
|별3=3&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US3241ce462ee634/design/2024-07-25_e2ca3cb3c0ddf.jpg&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US3241ce462ee634/design/2024-07-25_31cd10b1eb459.jpg&lt;br /&gt;
&lt;br /&gt;
=== 안경 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점 (뿔테에 적합)&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US1c1d2849305c5b/design/2024-10-07_b5f15c6035ce18.jpg&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/687240/615928/ratings/23179640-9bbc-11ef-bc9c-edb68d07a32b.jpg&lt;br /&gt;
&lt;br /&gt;
=== 피규어 - 모니터위의 고양이 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 4점&lt;br /&gt;
|별4=4&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US41a761b447d9ab/design/2024-12-06_b5af18e3af28a.jpg&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 수건걸이 - 와이프 추천  ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USbe8cfffdbf83ac/design/2025-04-16_ccf14e2be08de8.png&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USbe8cfffdbf83ac/design/2024-10-26_10e88bc02912f8.gif&lt;br /&gt;
&lt;br /&gt;
=== 구글홈 거치대 ===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/USc758a7e304d074/design/2024-02-18_bd74aae30a00f.png&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 마우스 - 로지텍 MX 마스터 3S 마우스 손목 받침대 CARPIO===&lt;br /&gt;
{{평점&lt;br /&gt;
|제목=제품만족도 : 5점&lt;br /&gt;
|별5=5&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US6dd80ef1b184/design/2025-05-20_6ad01270c5a43.jpg&lt;br /&gt;
&lt;br /&gt;
https://makerworld.bblmw.com/makerworld/model/US6dd80ef1b184/design/2025-05-20_80482e1c58adf8.gif&lt;br /&gt;
[[분류:3D프린터]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%B2%84%ED%8A%BC_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2407</id>
		<title>버튼 스타일</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%B2%84%ED%8A%BC_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2407"/>
		<updated>2026-05-15T08:59:31Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 플랫 버튼 스타일 ==&lt;br /&gt;
&lt;br /&gt;
## 1. BS_FLAT 스타일 (가장 간단)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;ButtonConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
$hGUI = GUICreate(&amp;quot;Flat Button&amp;quot;, 300, 200)&lt;br /&gt;
GUISetBkColor(0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
$hBtn = GUICtrlCreateButton(&amp;quot;클릭&amp;quot;, 50, 50, 200, 40, $BS_FLAT)&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
While GUIGetMsg() &amp;lt;&amp;gt; $GUI_EVENT_CLOSE&lt;br /&gt;
WEnd&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3D 테두리가 제거되지만, 마우스 오버 시 테두리가 살아나서 완전한 플랫은 아닙니다.&lt;br /&gt;
&lt;br /&gt;
## 2. Label + 색상 조합 (완전 플랫)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StaticConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
$hGUI = GUICreate(&amp;quot;Flat Button&amp;quot;, 400, 300)&lt;br /&gt;
GUISetBkColor(0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
; 버튼처럼 동작하는 Label&lt;br /&gt;
$hBtn1 = _CREATE_FLAT_BUTTON(&amp;quot;저장&amp;quot;,    30,  50, 150, 45, 0x2196F3, 0xFFFFFF)&lt;br /&gt;
$hBtn2 = _CREATE_FLAT_BUTTON(&amp;quot;취소&amp;quot;,   210,  50, 150, 45, 0xF44336, 0xFFFFFF)&lt;br /&gt;
$hBtn3 = _CREATE_FLAT_BUTTON(&amp;quot;설정&amp;quot;,    30, 120, 150, 45, 0x4CAF50, 0xFFFFFF)&lt;br /&gt;
$hBtn4 = _CREATE_FLAT_BUTTON(&amp;quot;닫기&amp;quot;,   210, 120, 150, 45, 0x757575, 0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
&lt;br /&gt;
While 1&lt;br /&gt;
    Switch GUIGetMsg()&lt;br /&gt;
        Case $GUI_EVENT_CLOSE&lt;br /&gt;
            Exit&lt;br /&gt;
        Case $hBtn1&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;저장 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn2&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;취소 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn3&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;설정 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn4&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;닫기 클릭&amp;quot;)&lt;br /&gt;
    EndSwitch&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
Func _CREATE_FLAT_BUTTON($sText, $iX, $iY, $iW, $iH, $iBgColor, $iFontColor)&lt;br /&gt;
    Local $hLabel = GUICtrlCreateLabel($sText, $iX, $iY, $iW, $iH, _&lt;br /&gt;
            BitOR($SS_CENTER, $SS_CENTERIMAGE))&lt;br /&gt;
    GUICtrlSetBkColor($hLabel, $iBgColor)&lt;br /&gt;
    GUICtrlSetColor($hLabel, $iFontColor)&lt;br /&gt;
    GUICtrlSetFont($hLabel, 11, 600, 0, &amp;quot;맑은 고딕&amp;quot;)&lt;br /&gt;
    GUICtrlSetCursor($hLabel, 0)  ; 손가락 커서&lt;br /&gt;
    Return $hLabel&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
## 3. GDI+ Owner-Draw (호버 효과 포함)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;GDIPlus.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StaticConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global Const $FLAT_COLOR_NORMAL  = 0xFF2196F3  ; 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_HOVER   = 0xFF1976D2  ; 진한 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_PRESS   = 0xFF0D47A1  ; 더 진한 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_TEXT    = 0xFFFFFFFF  ; 흰색&lt;br /&gt;
&lt;br /&gt;
Global $g_aBtns[0][6]  ; [n][CtrlID, X, Y, W, H, Text]&lt;br /&gt;
Global $g_iHoverIdx = -1&lt;br /&gt;
Global $g_iPressIdx = -1&lt;br /&gt;
Global $g_hGUI&lt;br /&gt;
&lt;br /&gt;
_GDIPlus_Startup()&lt;br /&gt;
&lt;br /&gt;
$g_hGUI = GUICreate(&amp;quot;GDI+ Flat Buttons&amp;quot;, 400, 300)&lt;br /&gt;
GUISetBkColor(0xF5F5F5)&lt;br /&gt;
&lt;br /&gt;
; GDI+ 그리기용 Pic 컨트롤 (전체 영역)&lt;br /&gt;
Global $g_hPic = GUICtrlCreatePic(&amp;quot;&amp;quot;, 0, 0, 400, 300)&lt;br /&gt;
GUICtrlSetState($g_hPic, $GUI_DISABLE)&lt;br /&gt;
&lt;br /&gt;
; 버튼 등록&lt;br /&gt;
_ADD_FLAT_BUTTON(30,   50, 150, 45, &amp;quot;저장&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(210,  50, 150, 45, &amp;quot;취소&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(30,  120, 150, 45, &amp;quot;설정&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(210, 120, 150, 45, &amp;quot;닫기&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
_DRAW_ALL_BUTTONS()&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
GUIRegisterMsg($WM_MOUSEMOVE, &amp;quot;_WM_MOUSEMOVE&amp;quot;)&lt;br /&gt;
GUIRegisterMsg($WM_LBUTTONDOWN, &amp;quot;_WM_LBUTTONDOWN&amp;quot;)&lt;br /&gt;
GUIRegisterMsg($WM_LBUTTONUP, &amp;quot;_WM_LBUTTONUP&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
While GUIGetMsg() &amp;lt;&amp;gt; $GUI_EVENT_CLOSE&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
_GDIPlus_Shutdown()&lt;br /&gt;
&lt;br /&gt;
; ── 버튼 추가 ──&lt;br /&gt;
Func _ADD_FLAT_BUTTON($iX, $iY, $iW, $iH, $sText)&lt;br /&gt;
    Local $iIdx = UBound($g_aBtns)&lt;br /&gt;
    ReDim $g_aBtns[$iIdx + 1][6]&lt;br /&gt;
    $g_aBtns[$iIdx][0] = $iIdx&lt;br /&gt;
    $g_aBtns[$iIdx][1] = $iX&lt;br /&gt;
    $g_aBtns[$iIdx][2] = $iY&lt;br /&gt;
    $g_aBtns[$iIdx][3] = $iW&lt;br /&gt;
    $g_aBtns[$iIdx][4] = $iH&lt;br /&gt;
    $g_aBtns[$iIdx][5] = $sText&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 전체 버튼 그리기 ──&lt;br /&gt;
Func _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Local $hBitmap = _GDIPlus_BitmapCreateFromScan0(400, 300)&lt;br /&gt;
    Local $hGfx = _GDIPlus_ImageGetGraphicsContext($hBitmap)&lt;br /&gt;
    _GDIPlus_GraphicsSetSmoothingMode($hGfx, 4)&lt;br /&gt;
    _GDIPlus_GraphicsSetTextRenderingHint($hGfx, 5)&lt;br /&gt;
    _GDIPlus_GraphicsClear($hGfx, 0xFFF5F5F5)&lt;br /&gt;
&lt;br /&gt;
    Local $hFontFamily = _GDIPlus_FontFamilyCreate(&amp;quot;맑은 고딕&amp;quot;)&lt;br /&gt;
    Local $hFont = _GDIPlus_FontCreate($hFontFamily, 12, 1)&lt;br /&gt;
    Local $hFormat = _GDIPlus_StringFormatCreate()&lt;br /&gt;
    _GDIPlus_StringFormatSetAlign($hFormat, 1)      ; 가로 중앙&lt;br /&gt;
    _GDIPlus_StringFormatSetLineAlign($hFormat, 1)   ; 세로 중앙&lt;br /&gt;
&lt;br /&gt;
    Local $hTextBrush = _GDIPlus_BrushCreateSolid($FLAT_COLOR_TEXT)&lt;br /&gt;
&lt;br /&gt;
    For $i = 0 To UBound($g_aBtns) - 1&lt;br /&gt;
        ; 상태별 색상 결정&lt;br /&gt;
        Local $iColor = $FLAT_COLOR_NORMAL&lt;br /&gt;
        If $i = $g_iPressIdx Then&lt;br /&gt;
            $iColor = $FLAT_COLOR_PRESS&lt;br /&gt;
        ElseIf $i = $g_iHoverIdx Then&lt;br /&gt;
            $iColor = $FLAT_COLOR_HOVER&lt;br /&gt;
        EndIf&lt;br /&gt;
&lt;br /&gt;
        ; 둥근 사각형 배경&lt;br /&gt;
        Local $hPath = _GDIPlus_PathCreate()&lt;br /&gt;
        Local $r = 8  ; 둥근 반지름&lt;br /&gt;
        Local $bX = $g_aBtns[$i][1], $bY = $g_aBtns[$i][2]&lt;br /&gt;
        Local $bW = $g_aBtns[$i][3], $bH = $g_aBtns[$i][4]&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX, $bY, $r * 2, $r * 2, 180, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX + $bW - $r * 2, $bY, $r * 2, $r * 2, 270, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX + $bW - $r * 2, $bY + $bH - $r * 2, $r * 2, $r * 2, 0, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX, $bY + $bH - $r * 2, $r * 2, $r * 2, 90, 90)&lt;br /&gt;
        _GDIPlus_PathCloseFigure($hPath)&lt;br /&gt;
&lt;br /&gt;
        Local $hBrush = _GDIPlus_BrushCreateSolid($iColor)&lt;br /&gt;
        _GDIPlus_GraphicsFillPath($hGfx, $hPath, $hBrush)&lt;br /&gt;
        _GDIPlus_BrushDispose($hBrush)&lt;br /&gt;
        _GDIPlus_PathDispose($hPath)&lt;br /&gt;
&lt;br /&gt;
        ; 텍스트&lt;br /&gt;
        Local $tRect = _GDIPlus_RectFCreate($bX, $bY, $bW, $bH)&lt;br /&gt;
        _GDIPlus_GraphicsDrawStringEx($hGfx, $g_aBtns[$i][5], $hFont, $tRect, $hFormat, $hTextBrush)&lt;br /&gt;
    Next&lt;br /&gt;
&lt;br /&gt;
    ; Pic에 적용&lt;br /&gt;
    Local $hHBmp = _GDIPlus_BitmapCreateHBITMAPFromBitmap($hBitmap)&lt;br /&gt;
    _WinAPI_DeleteObject(GUICtrlSendMsg($g_hPic, 0x0172, 0, $hHBmp))  ; STM_SETIMAGE&lt;br /&gt;
    _WinAPI_DeleteObject($hHBmp)&lt;br /&gt;
&lt;br /&gt;
    _GDIPlus_BrushDispose($hTextBrush)&lt;br /&gt;
    _GDIPlus_FontDispose($hFont)&lt;br /&gt;
    _GDIPlus_FontFamilyDispose($hFontFamily)&lt;br /&gt;
    _GDIPlus_StringFormatDispose($hFormat)&lt;br /&gt;
    _GDIPlus_GraphicsDispose($hGfx)&lt;br /&gt;
    _GDIPlus_BitmapDispose($hBitmap)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 위치로 버튼 인덱스 찾기 ──&lt;br /&gt;
Func _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    For $i = 0 To UBound($g_aBtns) - 1&lt;br /&gt;
        If $iMX &amp;gt;= $g_aBtns[$i][1] And $iMX &amp;lt;= $g_aBtns[$i][1] + $g_aBtns[$i][3] And _&lt;br /&gt;
           $iMY &amp;gt;= $g_aBtns[$i][2] And $iMY &amp;lt;= $g_aBtns[$i][2] + $g_aBtns[$i][4] Then&lt;br /&gt;
            Return $i&lt;br /&gt;
        EndIf&lt;br /&gt;
    Next&lt;br /&gt;
    Return -1&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 이동 ──&lt;br /&gt;
Func _WM_MOUSEMOVE($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    Local $iIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $iIdx &amp;lt;&amp;gt; $g_iHoverIdx Then&lt;br /&gt;
        $g_iHoverIdx = $iIdx&lt;br /&gt;
        _DRAW_ALL_BUTTONS()&lt;br /&gt;
    EndIf&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 누름 ──&lt;br /&gt;
Func _WM_LBUTTONDOWN($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    $g_iPressIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $g_iPressIdx &amp;gt;= 0 Then _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 뗌 ──&lt;br /&gt;
Func _WM_LBUTTONUP($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    Local $iIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $iIdx &amp;gt;= 0 And $iIdx = $g_iPressIdx Then&lt;br /&gt;
        ; 버튼 클릭 이벤트&lt;br /&gt;
        MsgBox(0, &amp;quot;클릭&amp;quot;, $g_aBtns[$iIdx][5] &amp;amp; &amp;quot; 버튼 클릭!&amp;quot;)&lt;br /&gt;
    EndIf&lt;br /&gt;
    $g_iPressIdx = -1&lt;br /&gt;
    _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
세 방식의 차이를 정리하면 다음과 같습니다.&lt;br /&gt;
&lt;br /&gt;
**방법 1 (BS_FLAT)** — 코드 한 줄이면 되지만 Windows 기본 버튼 렌더링에 의존하므로 완전한 플랫이 아닙니다. 마우스 오버 시 3D 테두리가 나타납니다.&lt;br /&gt;
&lt;br /&gt;
**방법 2 (Label 활용)** — Label에 배경색과 폰트색을 입히는 방식으로, 코드가 간결하면서도 완전 플랫합니다. 다만 호버/프레스 시 색상 변화를 주려면 타이머나 메시지 처리를 추가해야 합니다.&lt;br /&gt;
&lt;br /&gt;
**방법 3 (GDI+ Owner-Draw)** — 가장 자유도가 높습니다. 둥근 모서리, 호버 색상 변화, 프레스 효과까지 완전히 커스텀 가능합니다. 코드량이 많지만 SQL*KEY처럼 UI를 세밀하게 제어해야 하는 프로그램에 적합합니다.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 플랫한 버튼 스타일 예제 ===&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;GDIPlus.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; ── Tab Control 스타일 상수 ──&lt;br /&gt;
Global Const $TCS_MULTILINE      = 0x0200&lt;br /&gt;
Global Const $TCS_BUTTONS        = 0x0100&lt;br /&gt;
Global Const $TCS_FLATBUTTONS    = 0x0008&lt;br /&gt;
Global Const $TCS_OWNERDRAWFIXED = 0x2000&lt;br /&gt;
Global Const $ODT_TAB            = 101&lt;br /&gt;
Global Const $ODS_SELECTED       = 0x0001&lt;br /&gt;
&lt;br /&gt;
; ── 색상 설정 ──&lt;br /&gt;
Global Const $CLR_TAB_SELECTED_BG   = 0x2196F3  ; 선택된 탭 배경 (파란색)&lt;br /&gt;
Global Const $CLR_TAB_SELECTED_TEXT  = 0xFFFFFF  ; 선택된 탭 텍스트 (흰색)&lt;br /&gt;
Global Const $CLR_TAB_NORMAL_BG     = 0xE0E0E0  ; 일반 탭 배경 (밝은 회색)&lt;br /&gt;
Global Const $CLR_TAB_NORMAL_TEXT   = 0x333333  ; 일반 탭 텍스트 (진한 회색)&lt;br /&gt;
Global Const $CLR_TAB_HOVER_BG      = 0xBBDEFB  ; 호버 탭 배경 (연한 파란색)&lt;br /&gt;
Global Const $CLR_TAB_HOVER_TEXT    = 0x1565C0  ; 호버 탭 텍스트&lt;br /&gt;
Global Const $CLR_GUI_BG            = 0xFAFAFA  ; GUI 배경&lt;br /&gt;
Global Const $CLR_CONTENT_BG        = 0xFFFFFF  ; 탭 콘텐츠 영역 배경&lt;br /&gt;
&lt;br /&gt;
; ── 폰트 설정 ──&lt;br /&gt;
Global Const $TAB_FONT_NAME = &amp;quot;맑은 고딕&amp;quot;&lt;br /&gt;
Global Const $TAB_FONT_SIZE = 10&lt;br /&gt;
&lt;br /&gt;
; ── 탭 데이터 ──&lt;br /&gt;
Global $g_aTabNames[] = [ _&lt;br /&gt;
    &amp;quot;환경설정&amp;quot;, &amp;quot;단축키 관리&amp;quot;, &amp;quot;실행파일&amp;quot;, &amp;quot;후킹설정&amp;quot;, _&lt;br /&gt;
    &amp;quot;Direct Key&amp;quot;, &amp;quot;매크로&amp;quot;, &amp;quot;히스토리&amp;quot;, &amp;quot;도움말&amp;quot; _&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
Global $g_hGUI, $g_hTab&lt;br /&gt;
Global $g_aTabItems[UBound($g_aTabNames)]&lt;br /&gt;
Global $g_aTabLabels[UBound($g_aTabNames)]  ; 탭별 콘텐츠 라벨&lt;br /&gt;
Global $g_iHoverTab = -1&lt;br /&gt;
&lt;br /&gt;
; ── GUI 생성 ──&lt;br /&gt;
$g_hGUI = GUICreate(&amp;quot;SQL*KEY - Flat Tab Demo&amp;quot;, 700, 500)&lt;br /&gt;
GUISetBkColor($CLR_GUI_BG)&lt;br /&gt;
GUISetFont($TAB_FONT_SIZE, 400, 0, $TAB_FONT_NAME)&lt;br /&gt;
&lt;br /&gt;
; ── Tab 컨트롤 생성 ──&lt;br /&gt;
; TCS_OWNERDRAWFIXED: 직접 그리기&lt;br /&gt;
; TCS_BUTTONS: 버튼 스타일&lt;br /&gt;
; TCS_MULTILINE: 다중 행&lt;br /&gt;
; TCS_FLATBUTTONS: 플랫 버튼 (owner-draw와 함께 사용)&lt;br /&gt;
$g_hTab = GUICtrlCreateTab(10, 10, 680, 480, _&lt;br /&gt;
        BitOR($TCS_OWNERDRAWFIXED, $TCS_BUTTONS, $TCS_MULTILINE, $TCS_FLATBUTTONS))&lt;br /&gt;
GUICtrlSetFont($g_hTab, $TAB_FONT_SIZE, 400, 0, $TAB_FONT_NAME)&lt;br /&gt;
&lt;br /&gt;
; ── TabItem 생성 ──&lt;br /&gt;
For $i = 0 To UBound($g_aTabNames) - 1&lt;br /&gt;
    $g_aTabItems[$i] = GUICtrlCreateTabItem($g_aTabNames[$i])&lt;br /&gt;
&lt;br /&gt;
    ; 각 탭의 콘텐츠 예시&lt;br /&gt;
    $g_aTabLabels[$i] = GUICtrlCreateLabel( _&lt;br /&gt;
            &amp;quot;[ &amp;quot; &amp;amp; $g_aTabNames[$i] &amp;amp; &amp;quot; ] 탭 콘텐츠 영역&amp;quot;, _&lt;br /&gt;
            30, 80, 640, 380)&lt;br /&gt;
    GUICtrlSetFont($g_aTabLabels[$i], 14, 400, 0, $TAB_FONT_NAME)&lt;br /&gt;
    GUICtrlSetColor($g_aTabLabels[$i], 0x757575)&lt;br /&gt;
    GUICtrlSetBkColor($g_aTabLabels[$i], $CLR_CONTENT_BG)&lt;br /&gt;
Next&lt;br /&gt;
GUICtrlCreateTabItem(&amp;quot;&amp;quot;)  ; 탭 아이템 종료&lt;br /&gt;
&lt;br /&gt;
; ── WM_DRAWITEM 등록 (탭 커스텀 그리기) ──&lt;br /&gt;
GUIRegisterMsg($WM_DRAWITEM, &amp;quot;_WM_DRAWITEM&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
; ── WM_MOUSEMOVE 등록 (호버 효과) ──&lt;br /&gt;
GUIRegisterMsg($WM_NOTIFY, &amp;quot;_WM_NOTIFY&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
GUISetState(@SW_SHOW)&lt;br /&gt;
&lt;br /&gt;
; ── 메인 루프 ──&lt;br /&gt;
While 1&lt;br /&gt;
    Switch GUIGetMsg()&lt;br /&gt;
        Case $GUI_EVENT_CLOSE&lt;br /&gt;
            Exit&lt;br /&gt;
    EndSwitch&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
; WM_DRAWITEM 핸들러 - 탭을 플랫 스타일로 직접 그리기&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
Func _WM_DRAWITEM($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    ; DRAWITEMSTRUCT 구조체 파싱&lt;br /&gt;
    Local $tDRAWITEM = DllStructCreate( _&lt;br /&gt;
            &amp;quot;uint CtlType;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;uint CtlID;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;uint itemID;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;uint itemAction;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;uint itemState;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;hwnd hwndItem;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;handle hDC;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;long rcLeft;long rcTop;long rcRight;long rcBottom;&amp;quot; &amp;amp; _&lt;br /&gt;
            &amp;quot;ulong_ptr itemData&amp;quot;, _&lt;br /&gt;
            $lParam)&lt;br /&gt;
&lt;br /&gt;
    Local $iCtlType  = DllStructGetData($tDRAWITEM, &amp;quot;CtlType&amp;quot;)&lt;br /&gt;
    If $iCtlType &amp;lt;&amp;gt; $ODT_TAB Then Return $GUI_RUNDEFMSG&lt;br /&gt;
&lt;br /&gt;
    Local $iItemID   = DllStructGetData($tDRAWITEM, &amp;quot;itemID&amp;quot;)&lt;br /&gt;
    Local $iState    = DllStructGetData($tDRAWITEM, &amp;quot;itemState&amp;quot;)&lt;br /&gt;
    Local $hDC       = DllStructGetData($tDRAWITEM, &amp;quot;hDC&amp;quot;)&lt;br /&gt;
    Local $iLeft     = DllStructGetData($tDRAWITEM, &amp;quot;rcLeft&amp;quot;)&lt;br /&gt;
    Local $iTop      = DllStructGetData($tDRAWITEM, &amp;quot;rcTop&amp;quot;)&lt;br /&gt;
    Local $iRight    = DllStructGetData($tDRAWITEM, &amp;quot;rcRight&amp;quot;)&lt;br /&gt;
    Local $iBottom   = DllStructGetData($tDRAWITEM, &amp;quot;rcBottom&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    Local $bSelected = BitAND($iState, $ODS_SELECTED)&lt;br /&gt;
&lt;br /&gt;
    ; ── 색상 결정 ──&lt;br /&gt;
    Local $iBgColor, $iTextColor&lt;br /&gt;
    If $bSelected Then&lt;br /&gt;
        $iBgColor   = $CLR_TAB_SELECTED_BG&lt;br /&gt;
        $iTextColor = $CLR_TAB_SELECTED_TEXT&lt;br /&gt;
    ElseIf $iItemID = $g_iHoverTab Then&lt;br /&gt;
        $iBgColor   = $CLR_TAB_HOVER_BG&lt;br /&gt;
        $iTextColor = $CLR_TAB_HOVER_TEXT&lt;br /&gt;
    Else&lt;br /&gt;
        $iBgColor   = $CLR_TAB_NORMAL_BG&lt;br /&gt;
        $iTextColor = $CLR_TAB_NORMAL_TEXT&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; ── 배경 채우기 (1px 간격으로 버튼 분리) ──&lt;br /&gt;
    Local $tRect = DllStructCreate(&amp;quot;long Left;long Top;long Right;long Bottom&amp;quot;)&lt;br /&gt;
    DllStructSetData($tRect, &amp;quot;Left&amp;quot;,   $iLeft + 1)&lt;br /&gt;
    DllStructSetData($tRect, &amp;quot;Top&amp;quot;,    $iTop + 1)&lt;br /&gt;
    DllStructSetData($tRect, &amp;quot;Right&amp;quot;,  $iRight - 1)&lt;br /&gt;
    DllStructSetData($tRect, &amp;quot;Bottom&amp;quot;, $iBottom - 1)&lt;br /&gt;
&lt;br /&gt;
    Local $hBrush = _WinAPI_CreateSolidBrush($iBgColor)&lt;br /&gt;
    _WinAPI_FillRect($hDC, $tRect, $hBrush)&lt;br /&gt;
    _WinAPI_DeleteObject($hBrush)&lt;br /&gt;
&lt;br /&gt;
    ; ── 선택된 탭 하단 액센트 라인 ──&lt;br /&gt;
    If $bSelected Then&lt;br /&gt;
        Local $tAccent = DllStructCreate(&amp;quot;long Left;long Top;long Right;long Bottom&amp;quot;)&lt;br /&gt;
        DllStructSetData($tAccent, &amp;quot;Left&amp;quot;,   $iLeft + 1)&lt;br /&gt;
        DllStructSetData($tAccent, &amp;quot;Top&amp;quot;,    $iBottom - 4)&lt;br /&gt;
        DllStructSetData($tAccent, &amp;quot;Right&amp;quot;,  $iRight - 1)&lt;br /&gt;
        DllStructSetData($tAccent, &amp;quot;Bottom&amp;quot;, $iBottom - 1)&lt;br /&gt;
        Local $hAccentBrush = _WinAPI_CreateSolidBrush(0x0D47A1)  ; 진한 파란 액센트&lt;br /&gt;
        _WinAPI_FillRect($hDC, $tAccent, $hAccentBrush)&lt;br /&gt;
        _WinAPI_DeleteObject($hAccentBrush)&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; ── 텍스트 그리기 ──&lt;br /&gt;
    Local $sText = &amp;quot;&amp;quot;&lt;br /&gt;
    If $iItemID &amp;gt;= 0 And $iItemID &amp;lt; UBound($g_aTabNames) Then&lt;br /&gt;
        $sText = $g_aTabNames[$iItemID]&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; 폰트 생성&lt;br /&gt;
    Local $hFont = _WinAPI_CreateFont($TAB_FONT_SIZE + 3, 0, 0, 0, _&lt;br /&gt;
            ($bSelected ? 700 : 400), _  ; 선택 시 Bold&lt;br /&gt;
            False, False, False, _&lt;br /&gt;
            $DEFAULT_CHARSET, 0, 0, 5, 0, $TAB_FONT_NAME)&lt;br /&gt;
    Local $hOldFont = _WinAPI_SelectObject($hDC, $hFont)&lt;br /&gt;
&lt;br /&gt;
    ; 텍스트 색상 및 배경 모드&lt;br /&gt;
    DllCall(&amp;quot;gdi32.dll&amp;quot;, &amp;quot;int&amp;quot;, &amp;quot;SetBkMode&amp;quot;, &amp;quot;handle&amp;quot;, $hDC, &amp;quot;int&amp;quot;, 1)  ; TRANSPARENT&lt;br /&gt;
    DllCall(&amp;quot;gdi32.dll&amp;quot;, &amp;quot;int&amp;quot;, &amp;quot;SetTextColor&amp;quot;, &amp;quot;handle&amp;quot;, $hDC, _&lt;br /&gt;
            &amp;quot;int&amp;quot;, $iTextColor)&lt;br /&gt;
&lt;br /&gt;
    ; 텍스트 중앙 정렬 출력&lt;br /&gt;
    Local $tTextRect = DllStructCreate(&amp;quot;long Left;long Top;long Right;long Bottom&amp;quot;)&lt;br /&gt;
    DllStructSetData($tTextRect, &amp;quot;Left&amp;quot;,   $iLeft)&lt;br /&gt;
    DllStructSetData($tTextRect, &amp;quot;Top&amp;quot;,    $iTop)&lt;br /&gt;
    DllStructSetData($tTextRect, &amp;quot;Right&amp;quot;,  $iRight)&lt;br /&gt;
    DllStructSetData($tTextRect, &amp;quot;Bottom&amp;quot;, $iBottom)&lt;br /&gt;
&lt;br /&gt;
    ; DT_CENTER=1, DT_VCENTER=4, DT_SINGLELINE=32&lt;br /&gt;
    DllCall(&amp;quot;user32.dll&amp;quot;, &amp;quot;int&amp;quot;, &amp;quot;DrawTextW&amp;quot;, _&lt;br /&gt;
            &amp;quot;handle&amp;quot;, $hDC, _&lt;br /&gt;
            &amp;quot;wstr&amp;quot;, $sText, _&lt;br /&gt;
            &amp;quot;int&amp;quot;, -1, _&lt;br /&gt;
            &amp;quot;struct*&amp;quot;, $tTextRect, _&lt;br /&gt;
            &amp;quot;uint&amp;quot;, BitOR(1, 4, 32))&lt;br /&gt;
&lt;br /&gt;
    _WinAPI_SelectObject($hDC, $hOldFont)&lt;br /&gt;
    _WinAPI_DeleteObject($hFont)&lt;br /&gt;
&lt;br /&gt;
    Return True  ; 그리기 처리 완료&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
; WM_NOTIFY 핸들러 - 호버 효과를 위한 마우스 추적&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
Func _WM_NOTIFY($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $tNMHDR = DllStructCreate(&amp;quot;hwnd hWndFrom;uint_ptr IDFrom;int Code&amp;quot;, $lParam)&lt;br /&gt;
    Local $iCode = DllStructGetData($tNMHDR, &amp;quot;Code&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ; TCN_SELCHANGE = -551 (탭 변경 시 강제 다시 그리기)&lt;br /&gt;
    If $iCode = -551 Then&lt;br /&gt;
        _WinAPI_InvalidateRect(GUICtrlGetHandle($g_hTab))&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
; WinAPI 보조 함수&lt;br /&gt;
; ══════════════════════════════════════════════════════&lt;br /&gt;
Func _WinAPI_CreateSolidBrush($iColor)&lt;br /&gt;
    Local $aRet = DllCall(&amp;quot;gdi32.dll&amp;quot;, &amp;quot;handle&amp;quot;, &amp;quot;CreateSolidBrush&amp;quot;, &amp;quot;int&amp;quot;, $iColor)&lt;br /&gt;
    Return $aRet[0]&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _WinAPI_FillRect($hDC, $tRect, $hBrush)&lt;br /&gt;
    DllCall(&amp;quot;user32.dll&amp;quot;, &amp;quot;int&amp;quot;, &amp;quot;FillRect&amp;quot;, &amp;quot;handle&amp;quot;, $hDC, &amp;quot;struct*&amp;quot;, $tRect, &amp;quot;handle&amp;quot;, $hBrush)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[category:autoit]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%B2%84%ED%8A%BC_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2406</id>
		<title>버튼 스타일</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%B2%84%ED%8A%BC_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2406"/>
		<updated>2026-05-15T08:53:03Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: == 플랫 버튼 스타일 ==  ## 1. BS_FLAT 스타일 (가장 간단)  &amp;lt;source lang=autoit&amp;gt;  #include &amp;lt;GUIConstantsEx.au3&amp;gt; #include &amp;lt;ButtonConstants.au3&amp;gt;  $hGUI = GUICreate(&amp;quot;Flat Button&amp;quot;, 300, 200) GUISetBkColor(0xFFFFFF)  $hBtn = GUICtrlCreateButton(&amp;quot;클릭&amp;quot;, 50, 50, 200, 40, $BS_FLAT)  GUISetState() While GUIGetMsg() &amp;lt;&amp;gt; $GUI_EVENT_CLOSE WEnd &amp;lt;/source&amp;gt;   3D 테두리가 제거되지만, 마우스 오버 시 테두리가 살아나서 완전한 플랫은 아닙니다.  ## 2. Lab...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 플랫 버튼 스타일 ==&lt;br /&gt;
&lt;br /&gt;
## 1. BS_FLAT 스타일 (가장 간단)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;ButtonConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
$hGUI = GUICreate(&amp;quot;Flat Button&amp;quot;, 300, 200)&lt;br /&gt;
GUISetBkColor(0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
$hBtn = GUICtrlCreateButton(&amp;quot;클릭&amp;quot;, 50, 50, 200, 40, $BS_FLAT)&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
While GUIGetMsg() &amp;lt;&amp;gt; $GUI_EVENT_CLOSE&lt;br /&gt;
WEnd&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
3D 테두리가 제거되지만, 마우스 오버 시 테두리가 살아나서 완전한 플랫은 아닙니다.&lt;br /&gt;
&lt;br /&gt;
## 2. Label + 색상 조합 (완전 플랫)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StaticConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
$hGUI = GUICreate(&amp;quot;Flat Button&amp;quot;, 400, 300)&lt;br /&gt;
GUISetBkColor(0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
; 버튼처럼 동작하는 Label&lt;br /&gt;
$hBtn1 = _CREATE_FLAT_BUTTON(&amp;quot;저장&amp;quot;,    30,  50, 150, 45, 0x2196F3, 0xFFFFFF)&lt;br /&gt;
$hBtn2 = _CREATE_FLAT_BUTTON(&amp;quot;취소&amp;quot;,   210,  50, 150, 45, 0xF44336, 0xFFFFFF)&lt;br /&gt;
$hBtn3 = _CREATE_FLAT_BUTTON(&amp;quot;설정&amp;quot;,    30, 120, 150, 45, 0x4CAF50, 0xFFFFFF)&lt;br /&gt;
$hBtn4 = _CREATE_FLAT_BUTTON(&amp;quot;닫기&amp;quot;,   210, 120, 150, 45, 0x757575, 0xFFFFFF)&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
&lt;br /&gt;
While 1&lt;br /&gt;
    Switch GUIGetMsg()&lt;br /&gt;
        Case $GUI_EVENT_CLOSE&lt;br /&gt;
            Exit&lt;br /&gt;
        Case $hBtn1&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;저장 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn2&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;취소 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn3&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;설정 클릭&amp;quot;)&lt;br /&gt;
        Case $hBtn4&lt;br /&gt;
            MsgBox(0, &amp;quot;&amp;quot;, &amp;quot;닫기 클릭&amp;quot;)&lt;br /&gt;
    EndSwitch&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
Func _CREATE_FLAT_BUTTON($sText, $iX, $iY, $iW, $iH, $iBgColor, $iFontColor)&lt;br /&gt;
    Local $hLabel = GUICtrlCreateLabel($sText, $iX, $iY, $iW, $iH, _&lt;br /&gt;
            BitOR($SS_CENTER, $SS_CENTERIMAGE))&lt;br /&gt;
    GUICtrlSetBkColor($hLabel, $iBgColor)&lt;br /&gt;
    GUICtrlSetColor($hLabel, $iFontColor)&lt;br /&gt;
    GUICtrlSetFont($hLabel, 11, 600, 0, &amp;quot;맑은 고딕&amp;quot;)&lt;br /&gt;
    GUICtrlSetCursor($hLabel, 0)  ; 손가락 커서&lt;br /&gt;
    Return $hLabel&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
## 3. GDI+ Owner-Draw (호버 효과 포함)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;GDIPlus.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StaticConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global Const $FLAT_COLOR_NORMAL  = 0xFF2196F3  ; 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_HOVER   = 0xFF1976D2  ; 진한 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_PRESS   = 0xFF0D47A1  ; 더 진한 파란색&lt;br /&gt;
Global Const $FLAT_COLOR_TEXT    = 0xFFFFFFFF  ; 흰색&lt;br /&gt;
&lt;br /&gt;
Global $g_aBtns[0][6]  ; [n][CtrlID, X, Y, W, H, Text]&lt;br /&gt;
Global $g_iHoverIdx = -1&lt;br /&gt;
Global $g_iPressIdx = -1&lt;br /&gt;
Global $g_hGUI&lt;br /&gt;
&lt;br /&gt;
_GDIPlus_Startup()&lt;br /&gt;
&lt;br /&gt;
$g_hGUI = GUICreate(&amp;quot;GDI+ Flat Buttons&amp;quot;, 400, 300)&lt;br /&gt;
GUISetBkColor(0xF5F5F5)&lt;br /&gt;
&lt;br /&gt;
; GDI+ 그리기용 Pic 컨트롤 (전체 영역)&lt;br /&gt;
Global $g_hPic = GUICtrlCreatePic(&amp;quot;&amp;quot;, 0, 0, 400, 300)&lt;br /&gt;
GUICtrlSetState($g_hPic, $GUI_DISABLE)&lt;br /&gt;
&lt;br /&gt;
; 버튼 등록&lt;br /&gt;
_ADD_FLAT_BUTTON(30,   50, 150, 45, &amp;quot;저장&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(210,  50, 150, 45, &amp;quot;취소&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(30,  120, 150, 45, &amp;quot;설정&amp;quot;)&lt;br /&gt;
_ADD_FLAT_BUTTON(210, 120, 150, 45, &amp;quot;닫기&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
_DRAW_ALL_BUTTONS()&lt;br /&gt;
&lt;br /&gt;
GUISetState()&lt;br /&gt;
GUIRegisterMsg($WM_MOUSEMOVE, &amp;quot;_WM_MOUSEMOVE&amp;quot;)&lt;br /&gt;
GUIRegisterMsg($WM_LBUTTONDOWN, &amp;quot;_WM_LBUTTONDOWN&amp;quot;)&lt;br /&gt;
GUIRegisterMsg($WM_LBUTTONUP, &amp;quot;_WM_LBUTTONUP&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
While GUIGetMsg() &amp;lt;&amp;gt; $GUI_EVENT_CLOSE&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
_GDIPlus_Shutdown()&lt;br /&gt;
&lt;br /&gt;
; ── 버튼 추가 ──&lt;br /&gt;
Func _ADD_FLAT_BUTTON($iX, $iY, $iW, $iH, $sText)&lt;br /&gt;
    Local $iIdx = UBound($g_aBtns)&lt;br /&gt;
    ReDim $g_aBtns[$iIdx + 1][6]&lt;br /&gt;
    $g_aBtns[$iIdx][0] = $iIdx&lt;br /&gt;
    $g_aBtns[$iIdx][1] = $iX&lt;br /&gt;
    $g_aBtns[$iIdx][2] = $iY&lt;br /&gt;
    $g_aBtns[$iIdx][3] = $iW&lt;br /&gt;
    $g_aBtns[$iIdx][4] = $iH&lt;br /&gt;
    $g_aBtns[$iIdx][5] = $sText&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 전체 버튼 그리기 ──&lt;br /&gt;
Func _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Local $hBitmap = _GDIPlus_BitmapCreateFromScan0(400, 300)&lt;br /&gt;
    Local $hGfx = _GDIPlus_ImageGetGraphicsContext($hBitmap)&lt;br /&gt;
    _GDIPlus_GraphicsSetSmoothingMode($hGfx, 4)&lt;br /&gt;
    _GDIPlus_GraphicsSetTextRenderingHint($hGfx, 5)&lt;br /&gt;
    _GDIPlus_GraphicsClear($hGfx, 0xFFF5F5F5)&lt;br /&gt;
&lt;br /&gt;
    Local $hFontFamily = _GDIPlus_FontFamilyCreate(&amp;quot;맑은 고딕&amp;quot;)&lt;br /&gt;
    Local $hFont = _GDIPlus_FontCreate($hFontFamily, 12, 1)&lt;br /&gt;
    Local $hFormat = _GDIPlus_StringFormatCreate()&lt;br /&gt;
    _GDIPlus_StringFormatSetAlign($hFormat, 1)      ; 가로 중앙&lt;br /&gt;
    _GDIPlus_StringFormatSetLineAlign($hFormat, 1)   ; 세로 중앙&lt;br /&gt;
&lt;br /&gt;
    Local $hTextBrush = _GDIPlus_BrushCreateSolid($FLAT_COLOR_TEXT)&lt;br /&gt;
&lt;br /&gt;
    For $i = 0 To UBound($g_aBtns) - 1&lt;br /&gt;
        ; 상태별 색상 결정&lt;br /&gt;
        Local $iColor = $FLAT_COLOR_NORMAL&lt;br /&gt;
        If $i = $g_iPressIdx Then&lt;br /&gt;
            $iColor = $FLAT_COLOR_PRESS&lt;br /&gt;
        ElseIf $i = $g_iHoverIdx Then&lt;br /&gt;
            $iColor = $FLAT_COLOR_HOVER&lt;br /&gt;
        EndIf&lt;br /&gt;
&lt;br /&gt;
        ; 둥근 사각형 배경&lt;br /&gt;
        Local $hPath = _GDIPlus_PathCreate()&lt;br /&gt;
        Local $r = 8  ; 둥근 반지름&lt;br /&gt;
        Local $bX = $g_aBtns[$i][1], $bY = $g_aBtns[$i][2]&lt;br /&gt;
        Local $bW = $g_aBtns[$i][3], $bH = $g_aBtns[$i][4]&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX, $bY, $r * 2, $r * 2, 180, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX + $bW - $r * 2, $bY, $r * 2, $r * 2, 270, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX + $bW - $r * 2, $bY + $bH - $r * 2, $r * 2, $r * 2, 0, 90)&lt;br /&gt;
        _GDIPlus_PathAddArc($hPath, $bX, $bY + $bH - $r * 2, $r * 2, $r * 2, 90, 90)&lt;br /&gt;
        _GDIPlus_PathCloseFigure($hPath)&lt;br /&gt;
&lt;br /&gt;
        Local $hBrush = _GDIPlus_BrushCreateSolid($iColor)&lt;br /&gt;
        _GDIPlus_GraphicsFillPath($hGfx, $hPath, $hBrush)&lt;br /&gt;
        _GDIPlus_BrushDispose($hBrush)&lt;br /&gt;
        _GDIPlus_PathDispose($hPath)&lt;br /&gt;
&lt;br /&gt;
        ; 텍스트&lt;br /&gt;
        Local $tRect = _GDIPlus_RectFCreate($bX, $bY, $bW, $bH)&lt;br /&gt;
        _GDIPlus_GraphicsDrawStringEx($hGfx, $g_aBtns[$i][5], $hFont, $tRect, $hFormat, $hTextBrush)&lt;br /&gt;
    Next&lt;br /&gt;
&lt;br /&gt;
    ; Pic에 적용&lt;br /&gt;
    Local $hHBmp = _GDIPlus_BitmapCreateHBITMAPFromBitmap($hBitmap)&lt;br /&gt;
    _WinAPI_DeleteObject(GUICtrlSendMsg($g_hPic, 0x0172, 0, $hHBmp))  ; STM_SETIMAGE&lt;br /&gt;
    _WinAPI_DeleteObject($hHBmp)&lt;br /&gt;
&lt;br /&gt;
    _GDIPlus_BrushDispose($hTextBrush)&lt;br /&gt;
    _GDIPlus_FontDispose($hFont)&lt;br /&gt;
    _GDIPlus_FontFamilyDispose($hFontFamily)&lt;br /&gt;
    _GDIPlus_StringFormatDispose($hFormat)&lt;br /&gt;
    _GDIPlus_GraphicsDispose($hGfx)&lt;br /&gt;
    _GDIPlus_BitmapDispose($hBitmap)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 위치로 버튼 인덱스 찾기 ──&lt;br /&gt;
Func _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    For $i = 0 To UBound($g_aBtns) - 1&lt;br /&gt;
        If $iMX &amp;gt;= $g_aBtns[$i][1] And $iMX &amp;lt;= $g_aBtns[$i][1] + $g_aBtns[$i][3] And _&lt;br /&gt;
           $iMY &amp;gt;= $g_aBtns[$i][2] And $iMY &amp;lt;= $g_aBtns[$i][2] + $g_aBtns[$i][4] Then&lt;br /&gt;
            Return $i&lt;br /&gt;
        EndIf&lt;br /&gt;
    Next&lt;br /&gt;
    Return -1&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 이동 ──&lt;br /&gt;
Func _WM_MOUSEMOVE($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    Local $iIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $iIdx &amp;lt;&amp;gt; $g_iHoverIdx Then&lt;br /&gt;
        $g_iHoverIdx = $iIdx&lt;br /&gt;
        _DRAW_ALL_BUTTONS()&lt;br /&gt;
    EndIf&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 누름 ──&lt;br /&gt;
Func _WM_LBUTTONDOWN($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    $g_iPressIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $g_iPressIdx &amp;gt;= 0 Then _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 마우스 뗌 ──&lt;br /&gt;
Func _WM_LBUTTONUP($hWnd, $iMsg, $wParam, $lParam)&lt;br /&gt;
    Local $iMX = BitAND($lParam, 0xFFFF)&lt;br /&gt;
    Local $iMY = BitShift(BitAND($lParam, 0xFFFF0000), 16)&lt;br /&gt;
    Local $iIdx = _HIT_TEST($iMX, $iMY)&lt;br /&gt;
    If $iIdx &amp;gt;= 0 And $iIdx = $g_iPressIdx Then&lt;br /&gt;
        ; 버튼 클릭 이벤트&lt;br /&gt;
        MsgBox(0, &amp;quot;클릭&amp;quot;, $g_aBtns[$iIdx][5] &amp;amp; &amp;quot; 버튼 클릭!&amp;quot;)&lt;br /&gt;
    EndIf&lt;br /&gt;
    $g_iPressIdx = -1&lt;br /&gt;
    _DRAW_ALL_BUTTONS()&lt;br /&gt;
    Return $GUI_RUNDEFMSG&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
세 방식의 차이를 정리하면 다음과 같습니다.&lt;br /&gt;
&lt;br /&gt;
**방법 1 (BS_FLAT)** — 코드 한 줄이면 되지만 Windows 기본 버튼 렌더링에 의존하므로 완전한 플랫이 아닙니다. 마우스 오버 시 3D 테두리가 나타납니다.&lt;br /&gt;
&lt;br /&gt;
**방법 2 (Label 활용)** — Label에 배경색과 폰트색을 입히는 방식으로, 코드가 간결하면서도 완전 플랫합니다. 다만 호버/프레스 시 색상 변화를 주려면 타이머나 메시지 처리를 추가해야 합니다.&lt;br /&gt;
&lt;br /&gt;
**방법 3 (GDI+ Owner-Draw)** — 가장 자유도가 높습니다. 둥근 모서리, 호버 색상 변화, 프레스 효과까지 완전히 커스텀 가능합니다. 코드량이 많지만 SQL*KEY처럼 UI를 세밀하게 제어해야 하는 프로그램에 적합합니다.&lt;br /&gt;
&lt;br /&gt;
[[category:autoit]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=Autoit_GUI_%EC%BB%A8%ED%8A%B8%EB%A1%A4_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2405</id>
		<title>Autoit GUI 컨트롤 스타일</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=Autoit_GUI_%EC%BB%A8%ED%8A%B8%EB%A1%A4_%EC%8A%A4%ED%83%80%EC%9D%BC&amp;diff=2405"/>
		<updated>2026-05-15T08:51:49Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== GUI 컨트롤 스타일 ==&lt;br /&gt;
* 컨트롤 윈도우에 사용할 수 있는 일반 및 확장 스타일 목록과 컨트롤에 사용할 수 있는 특정 스타일 목록&lt;br /&gt;
* 이러한 스타일은 많은 GUI 기능의 &amp;quot;style&amp;quot; 및/또는 &amp;quot;exStyle&amp;quot; 매개변수에 사용됩니다. &lt;br /&gt;
* 이러한 값을 사용하려면 각 섹션의 상단에 지정된 파일을 #include해야 합니다.									&lt;br /&gt;
** 참고: &amp;quot;값&amp;quot;은 단지 참조용으로 제공됩니다. &lt;br /&gt;
**: 스크립트를 읽기 쉽게 유지하려면 항상 스타일의 &amp;quot;이름&amp;quot;을 사용해야 합니다.				&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 일반적인 스타일 ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;text-align:center; bordercolor=&#039;#000000&#039; border=1 &amp;quot;&lt;br /&gt;
|- style=&amp;quot;font-size:14px; font-family:&#039;Arial Black&#039;, Gadget, sans-serif !important; background-color:#c0c0c0; color:#000000;&amp;quot;&lt;br /&gt;
|+ 일반적인 스타일 &lt;br /&gt;
!	일반적인 스타일	!!	값	!!	설명&lt;br /&gt;
|-&lt;br /&gt;
|- style=&amp;quot;font-size:12px; font-family:&#039;맑은 고딕&#039;, Gadget, sans-serif !important; background-color:#efefef;&amp;quot;&lt;br /&gt;
| colspan=&amp;quot;3&amp;quot; | #include &amp;lt;WindowsConstants.au3&amp;gt;				&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	GUI 생성을 위한 기본값/강제	||		||	$GUI_SS_DEFAULT_GUI GUICreate 함수를 참조하세요.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_BORDER	||	0x00800000	||	얇은 선 테두리가 있는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_POPUP	||	0x80000000	||	팝업창을 생성합니다. 이 스타일은 WS_CHILD 스타일과 함께 사용할 수 없습니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_CAPTION	||	0x00C00000	||	제목 표시줄(WS_BORDER 스타일 포함)이 있는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_CLIPCHILDREN	||	0x02000000	||	상위 창 내에서 그리기가 발생할 때 하위 창이 차지하는 영역을 제외합니다. 이 스타일은 상위 창을 만들 때 사용됩니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_CLIPSIBLINGS	||	0x04000000	||	자식 창을 서로 상대적으로 자릅니다. 즉, 특정 자식 창이 WM_PAINT 메시지를 받으면 WS_CLIPSIBLINGS 스타일은 업데이트할 자식 창 영역에서 다른 모든 겹치는 자식 창을 잘라냅니다. $WS_CLIPSIBLINGS가 지정되지 않고 자식 창이 겹치는 경우 자식 창의 클라이언트 영역 내에 그릴 때 인접한 자식 창의 클라이언트 영역 내에 그리는 것이 가능합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_DISABLED	||	0x08000000	||	처음에 비활성화된 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_DLGFRAME	||	0x00400000	||	일반적으로 대화 상자에 사용되는 스타일의 테두리가 있는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_HSCROLL	||	0x00100000	||	가로 스크롤 막대가 있는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_MAXIMIZE	||	0x01000000	||	처음에 최대화되는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_MAXIMIZEBOX	||	0x00010000	||	최대화 버튼이 있는 창을 만듭니다. WS_EX_CONTEXTHELP 스타일과 결합할 수 없습니다. WS_SYSMENU 스타일도 지정해야 합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_MINIMIZE	||	0x20000000	||	처음에 최소화된 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_MINIMIZEBOX	||	0x00020000	||	최소화 버튼이 있는 창을 만듭니다. WS_EX_CONTEXTHELP 스타일과 결합할 수 없습니다. WS_SYSMENU 스타일도 지정해야 합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_OVERLAPPED	||	0x00000000	||	겹쳐진 창을 만듭니다. 겹쳐진 창에는 제목 표시줄과 테두리가 있습니다. WS_TILED 스타일과 동일합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_OVERLAPPEDWINDOW	||	0x00CF0000	||	WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX 및 WS_MAXIMIZEBOX 스타일을 사용하여 겹쳐진 창을 만듭니다. WS_TILEDWINDOW 스타일과 동일합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_POPUPWINDOW	||	0x80880000	||	WS_BORDER, WS_POPUP 및 WS_SYSMENU 스타일을 사용하여 팝업 창을 만듭니다. 창 메뉴를 표시하려면 WS_CAPTION 및 WS_POPUPWINDOW 스타일을 결합해야 합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_SIZEBOX	||	0x00040000	||	크기 조정 테두리가 있는 창을 만듭니다. WS_THICKFRAME 스타일과 동일합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_SYSMENU	||	0x00080000	||	제목 표시줄에 창 메뉴가 있는 창을 만듭니다. WS_CAPTION 스타일도 지정해야 합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_THICKFRAME	||	0x00040000	||	크기 조정 테두리가 있는 창을 만듭니다. WS_SIZEBOX 스타일과 동일&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_VSCROLL	||	0x00200000	||	수직 스크롤 막대가 있는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_VISIBLE	||	0x10000000	||	처음에 표시되는 창을 만듭니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_CHILD	||	0x40000000	||	자식 창을 만듭니다. 이 스타일의 창에는 메뉴 표시줄이 있을 수 없습니다. 이 스타일은 WS_POPUP 스타일과 함께 사용할 수 없습니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_GROUP	||	0x00020000	||	컨트롤 그룹의 첫 번째 컨트롤을 지정합니다. 그룹은 이 첫 번째 컨트롤과 그 뒤에 정의된 모든 컨트롤, WS_GROUP 스타일을 사용하는 다음 컨트롤로 구성됩니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$WS_TABSTOP	||	0x00010000	||	컨트롤을 탭 정지로 전환하면 사용자가 대화 상자의 컨트롤을 탭하여 컨트롤을 선택할 수 있습니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$DS_MODALFRAME	||	0x00000080	||	WS_CAPTION 및 WS_SYSMENU 스타일을 지정하여 제목 표시줄 및 창 메뉴 와 결합할 수 있는 모달 대화 상자 프레임이 있는 대화 상자를 만듭니다 .&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$DS_SETFOREGROUND	||	0x00000200	||	이 스타일은 소유자 창이 전경 창인지 여부에 관계없이 사용자의 즉각적인 주의가 필요한 모달 대화 상자에 유용합니다.&lt;br /&gt;
|- style=&amp;quot;text-align:left;&amp;quot;&lt;br /&gt;
|	$DS_CONTEXTHELP	||	0x00002000	||	대화 상자의 제목 표시줄에 물음표를 포함합니다. WS_MAXIMIZEBOX 또는 WS_MINIMIZEBOX 스타일과 함께 사용할 수 없습니다. WS_EX_CONTEXTHELP 확장 스타일과 동일합니다.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
{{:푸시 버튼 스타일}}&lt;br /&gt;
&lt;br /&gt;
{{:콤보(Combo ) 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:목록(List) 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Edit/Input 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Progress Bar 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Up-down 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Label/Static 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:탭 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Avi 클립 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:Date 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:월달력(MonthCal) 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:TreeView 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:슬라이더 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:ListView 스타일 }}&lt;br /&gt;
&lt;br /&gt;
{{:ListView 확장 스타일 }}&lt;br /&gt;
{{:버튼 스타일 }}&lt;br /&gt;
[[category:autoit]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%95%84%EC%9D%B4%ED%8F%B0_%EB%B2%84%ED%8A%BC_%EC%9E%AC%EB%B6%80%ED%8C%85&amp;diff=2404</id>
		<title>아이폰 버튼 재부팅</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%95%84%EC%9D%B4%ED%8F%B0_%EB%B2%84%ED%8A%BC_%EC%9E%AC%EB%B6%80%ED%8C%85&amp;diff=2404"/>
		<updated>2026-05-14T03:46:56Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: == 모델별 재시작 방법 == * (iOS 16 이상)iPhone X, 11, 12, 13, 14, 15, 16 및 SE(3세대): # 볼륨 높이기(+) 버튼을 빠르게 눌렀다 뗍니다. # 볼륨 낮추기(-) 버튼을 빠르게 눌렀다 뗍니다. # 오른쪽 측면(전원) 버튼을 화면에 애플 로고가 나타날 때까지 길게 누릅니다.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 모델별 재시작 방법 ==&lt;br /&gt;
* (iOS 16 이상)iPhone X, 11, 12, 13, 14, 15, 16 및 SE(3세대):&lt;br /&gt;
# 볼륨 높이기(+) 버튼을 빠르게 눌렀다 뗍니다.&lt;br /&gt;
# 볼륨 낮추기(-) 버튼을 빠르게 눌렀다 뗍니다.&lt;br /&gt;
# 오른쪽 측면(전원) 버튼을 화면에 애플 로고가 나타날 때까지 길게 누릅니다.&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2403</id>
		<title>라즈베리파이 음성인식 프로그램 만들기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2403"/>
		<updated>2026-05-12T09:36:24Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 마이크 모듈 추가 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=== 마이크 모듈 추가 ===&lt;br /&gt;
INMP441(MS3625 모듈)을 Raspberry Pi Zero에 연결하는 핀맵입니다.&lt;br /&gt;
배선 (INMP441 → RPi Zero)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|INMP441 핀 |RPi Zero 핀 (물리번호)                     |GPIO   |&lt;br /&gt;
|----------|--------------------------------------|-------|&lt;br /&gt;
|VDD       |Pin 1 (3.3V)                          |-      |&lt;br /&gt;
|GND       |Pin 6 (GND)                           |-      |&lt;br /&gt;
|L/R       |Pin 6 (GND) → 좌채널 / Pin 1 (3.3V) → 우채널|-      |&lt;br /&gt;
|WS (LRCLK)|Pin 35                                |GPIO 19|&lt;br /&gt;
|SCK (BCLK)|Pin 12                                |GPIO 18|&lt;br /&gt;
|SD (DOUT) |Pin 38                                |GPIO 20|&lt;br /&gt;
&lt;br /&gt;
마이크 하나만 쓸 경우 L/R은 보통 GND로 묶어 좌채널로 설정합니다. 두 개를 스테레오로 쓸 거면 한쪽은 GND(좌), 다른 쪽은 3.3V(우)로 묶고 SD 핀끼리 합쳐서 GPIO 20 한 줄로 보내면 됩니다.&lt;br /&gt;
납땜 팁&lt;br /&gt;
	•	RPi Zero가 W(헤더 없는) 버전이면 먼저 40핀 헤더를 납땜하거나, 위 6개 홀에 직접 와이어를 연결. WH 버전이면 헤더에 듀폰 점퍼선으로 먼저 테스트 후 영구 연결 권장.&lt;br /&gt;
	•	와이어는 가능한 한 짧게(15cm 이내) — I2S 클럭이 길어지면 노이즈 탑니다. 길게 빼야 하면 차폐선 또는 트위스트 페어로.&lt;br /&gt;
	•	0.5mm 무연 솔더 + 인두 온도 320~340℃. 패드와 와이어 양쪽 다 미리 주석도금(pre-tinning) 후 접합하면 깔끔합니다.&lt;br /&gt;
	•	INMP441 모듈 핀이 2.54mm 피치라 헤더핀 꽂아서 점퍼선으로 먼저 동작 확인하고, 안정되면 직결 납땜하는 게 안전합니다.&lt;br /&gt;
	•	납땜 후 멀티미터로 VDD-GND 단락 여부, 각 신호선 도통 먼저 확인하고 전원 인가.&lt;br /&gt;
연결 후 활성화 (/boot/config.txt)&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
dtparam=i2s=on&lt;br /&gt;
dtoverlay=googlevoicehat-soundcard&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
또는 dtoverlay=i2s-mmap 후 arecord -l로 카드 인식 확인. arecord -D plughw:1 -c 1 -r 48000 -f S32_LE test.wav 로 녹음 테스트 하시면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 프로그램 소스 ===&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
INMP441 I2S 마이크 → Whisper STT → Claude 응답&lt;br /&gt;
Raspberry Pi Zero WH + INMP441 환경 기준&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import wave&lt;br /&gt;
import audioop&lt;br /&gt;
import tempfile&lt;br /&gt;
import subprocess&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
&lt;br /&gt;
from openai import OpenAI&lt;br /&gt;
import anthropic&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 환경설정&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
OPENAI_API_KEY = os.getenv(“OPENAI_API_KEY”)&lt;br /&gt;
ANTHROPIC_API_KEY = os.getenv(“ANTHROPIC_API_KEY”)&lt;br /&gt;
&lt;br /&gt;
# arecord 디바이스 (arecord -l 로 확인 후 카드 번호 맞추기)&lt;br /&gt;
&lt;br /&gt;
ALSA_DEVICE = os.getenv(“ALSA_DEVICE”, “plughw:1,0”)&lt;br /&gt;
&lt;br /&gt;
# 오디오 포맷&lt;br /&gt;
&lt;br /&gt;
SAMPLE_RATE = 16000      # Whisper 권장값&lt;br /&gt;
CHANNELS = 1&lt;br /&gt;
SAMPLE_WIDTH = 2         # 16-bit PCM (plughw가 32→16 자동 변환)&lt;br /&gt;
&lt;br /&gt;
# VAD (Voice Activity Detection) 파라미터&lt;br /&gt;
&lt;br /&gt;
CHUNK_MS = 100                       # 100ms 단위로 검사&lt;br /&gt;
CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_MS / 1000) * SAMPLE_WIDTH&lt;br /&gt;
SILENCE_RMS_THRESHOLD = 600          # 환경에 맞춰 튜닝 (조용한 방 300, 시끄러우면 1000+)&lt;br /&gt;
START_RMS_THRESHOLD = 900            # 발화 시작 판단 임계치&lt;br /&gt;
SILENCE_END_SEC = 1.5                # 이만큼 무음 지속 시 발화 종료로 판단&lt;br /&gt;
MAX_RECORD_SEC = 30                  # 최대 녹음 길이&lt;br /&gt;
MIN_SPEECH_SEC = 0.4                 # 너무 짧은 입력은 무시&lt;br /&gt;
&lt;br /&gt;
# Claude 모델&lt;br /&gt;
&lt;br /&gt;
CLAUDE_MODEL = “claude-opus-4-5”&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 1) I2S 마이크에서 발화 단위로 녹음&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def record_utterance(out_wav: str) -&amp;gt; bool:&lt;br /&gt;
“””&lt;br /&gt;
arecord를 파이프로 띄워 실시간으로 RMS를 측정하면서&lt;br /&gt;
- 발화 시작이 감지되면 버퍼링 시작&lt;br /&gt;
- 무음이 SILENCE_END_SEC 이상 지속되면 종료&lt;br /&gt;
“””&lt;br /&gt;
cmd = [&lt;br /&gt;
“arecord”,&lt;br /&gt;
“-D”, ALSA_DEVICE,&lt;br /&gt;
“-c”, str(CHANNELS),&lt;br /&gt;
“-r”, str(SAMPLE_RATE),&lt;br /&gt;
“-f”, “S16_LE”,&lt;br /&gt;
“-t”, “raw”,&lt;br /&gt;
“-q”,&lt;br /&gt;
]&lt;br /&gt;
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;🎤 듣는 중... (말씀하세요)&amp;quot;, flush=True)&lt;br /&gt;
frames = []&lt;br /&gt;
started = False&lt;br /&gt;
silence_chunks = 0&lt;br /&gt;
silence_limit = int(SILENCE_END_SEC * 1000 / CHUNK_MS)&lt;br /&gt;
start_time = None&lt;br /&gt;
pre_buffer = []                  # 발화 직전 짧은 구간도 함께 저장 (앞 음 잘림 방지)&lt;br /&gt;
pre_buffer_max = 3               # 약 300ms&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while True:&lt;br /&gt;
        data = proc.stdout.read(CHUNK_BYTES)&lt;br /&gt;
        if not data or len(data) &amp;lt; CHUNK_BYTES:&lt;br /&gt;
            break&lt;br /&gt;
&lt;br /&gt;
        rms = audioop.rms(data, SAMPLE_WIDTH)&lt;br /&gt;
&lt;br /&gt;
        if not started:&lt;br /&gt;
            pre_buffer.append(data)&lt;br /&gt;
            if len(pre_buffer) &amp;gt; pre_buffer_max:&lt;br /&gt;
                pre_buffer.pop(0)&lt;br /&gt;
            if rms &amp;gt; START_RMS_THRESHOLD:&lt;br /&gt;
                started = True&lt;br /&gt;
                start_time = time.time()&lt;br /&gt;
                frames.extend(pre_buffer)&lt;br /&gt;
                frames.append(data)&lt;br /&gt;
                print(&amp;quot;🔴 녹음 시작&amp;quot;, flush=True)&lt;br /&gt;
        else:&lt;br /&gt;
            frames.append(data)&lt;br /&gt;
            if rms &amp;lt; SILENCE_RMS_THRESHOLD:&lt;br /&gt;
                silence_chunks += 1&lt;br /&gt;
            else:&lt;br /&gt;
                silence_chunks = 0&lt;br /&gt;
&lt;br /&gt;
            if silence_chunks &amp;gt;= silence_limit:&lt;br /&gt;
                print(&amp;quot;⏹️  녹음 종료&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
            if time.time() - start_time &amp;gt; MAX_RECORD_SEC:&lt;br /&gt;
                print(&amp;quot;⏹️  최대 길이 도달&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
finally:&lt;br /&gt;
    proc.terminate()&lt;br /&gt;
    try:&lt;br /&gt;
        proc.wait(timeout=1)&lt;br /&gt;
    except subprocess.TimeoutExpired:&lt;br /&gt;
        proc.kill()&lt;br /&gt;
&lt;br /&gt;
if not started:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
duration = (len(frames) * CHUNK_BYTES) / (SAMPLE_RATE * SAMPLE_WIDTH)&lt;br /&gt;
if duration &amp;lt; MIN_SPEECH_SEC:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
with wave.open(out_wav, &amp;quot;wb&amp;quot;) as wf:&lt;br /&gt;
    wf.setnchannels(CHANNELS)&lt;br /&gt;
    wf.setsampwidth(SAMPLE_WIDTH)&lt;br /&gt;
    wf.setframerate(SAMPLE_RATE)&lt;br /&gt;
    wf.writeframes(b&amp;quot;&amp;quot;.join(frames))&lt;br /&gt;
return True&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 2) Whisper API로 STT (한국어 + 영어 코드스위칭 자동)&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def transcribe(audio_path: str) -&amp;gt; str:&lt;br /&gt;
client = OpenAI(api_key=OPENAI_API_KEY)&lt;br /&gt;
with open(audio_path, “rb”) as f:&lt;br /&gt;
result = client.audio.transcriptions.create(&lt;br /&gt;
model=“whisper-1”,&lt;br /&gt;
file=f,&lt;br /&gt;
language=“ko”,   # 주 언어 힌트. 영어 단어 섞여 있으면 자동 인식&lt;br /&gt;
prompt=“한국어 위주이고 영어 기술 용어가 섞일 수 있습니다. “&lt;br /&gt;
“예: Oracle, RAC, Raspberry Pi, GPIO, INMP441.”,&lt;br /&gt;
temperature=0.0,&lt;br /&gt;
)&lt;br /&gt;
return result.text.strip()&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 3) Claude API에 전달해서 응답 받기&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def ask_claude(user_text: str, history: list) -&amp;gt; str:&lt;br /&gt;
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)&lt;br /&gt;
history.append({“role”: “user”, “content”: user_text})&lt;br /&gt;
msg = client.messages.create(&lt;br /&gt;
model=CLAUDE_MODEL,&lt;br /&gt;
max_tokens=1024,&lt;br /&gt;
system=(“당신은 한국어로 답하는 음성 비서입니다. “&lt;br /&gt;
“사용자의 발화는 음성인식 결과라 오타·동음이의어가 있을 수 있으니 “&lt;br /&gt;
“맥락으로 보정해서 이해해 주세요. 간결하게 답하세요.”),&lt;br /&gt;
messages=history,&lt;br /&gt;
)&lt;br /&gt;
answer = msg.content[0].text&lt;br /&gt;
history.append({“role”: “assistant”, “content”: answer})&lt;br /&gt;
# 컨텍스트 비대화 방지: 최근 10턴만 유지&lt;br /&gt;
if len(history) &amp;gt; 20:&lt;br /&gt;
del history[: len(history) - 20]&lt;br /&gt;
return answer&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
if not OPENAI_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 OPENAI_API_KEY 가 필요합니다.”)&lt;br /&gt;
if not ANTHROPIC_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 ANTHROPIC_API_KEY 가 필요합니다.”)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
print(&amp;quot; INMP441 → Whisper → Claude 음성 어시스턴트&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  device   : {ALSA_DEVICE}&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  model    : {CLAUDE_MODEL}&amp;quot;)&lt;br /&gt;
print(&amp;quot;  Ctrl+C 로 종료&amp;quot;)&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
history = []&lt;br /&gt;
while True:&lt;br /&gt;
    try:&lt;br /&gt;
        with tempfile.NamedTemporaryFile(suffix=&amp;quot;.wav&amp;quot;, delete=False) as tmp:&lt;br /&gt;
            wav_path = tmp.name&lt;br /&gt;
&lt;br /&gt;
        ok = record_utterance(wav_path)&lt;br /&gt;
        if not ok:&lt;br /&gt;
            Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🔄 STT 변환 중...&amp;quot;, flush=True)&lt;br /&gt;
        text = transcribe(wav_path)&lt;br /&gt;
        Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
&lt;br /&gt;
        if not text:&lt;br /&gt;
            print(&amp;quot;(인식 실패)\n&amp;quot;)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(f&amp;quot;📝 사용자: {text}&amp;quot;, flush=True)&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🤖 Claude 응답 생성 중...&amp;quot;, flush=True)&lt;br /&gt;
        answer = ask_claude(text, history)&lt;br /&gt;
        print(f&amp;quot;💬 Claude: {answer}&amp;quot;)&lt;br /&gt;
        print(&amp;quot;-&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
    except KeyboardInterrupt:&lt;br /&gt;
        print(&amp;quot;\n종료합니다.&amp;quot;)&lt;br /&gt;
        break&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;⚠️  오류: {type(e).__name__}: {e}&amp;quot;)&lt;br /&gt;
        time.sleep(1)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == “**main**”:&lt;br /&gt;
main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2402</id>
		<title>라즈베리파이 음성인식 프로그램 만들기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2402"/>
		<updated>2026-05-12T09:35:25Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=== 마이크 모듈 추가 ===&lt;br /&gt;
&lt;br /&gt;
=== 프로그램 소스 ===&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
INMP441 I2S 마이크 → Whisper STT → Claude 응답&lt;br /&gt;
Raspberry Pi Zero WH + INMP441 환경 기준&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import wave&lt;br /&gt;
import audioop&lt;br /&gt;
import tempfile&lt;br /&gt;
import subprocess&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
&lt;br /&gt;
from openai import OpenAI&lt;br /&gt;
import anthropic&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 환경설정&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
OPENAI_API_KEY = os.getenv(“OPENAI_API_KEY”)&lt;br /&gt;
ANTHROPIC_API_KEY = os.getenv(“ANTHROPIC_API_KEY”)&lt;br /&gt;
&lt;br /&gt;
# arecord 디바이스 (arecord -l 로 확인 후 카드 번호 맞추기)&lt;br /&gt;
&lt;br /&gt;
ALSA_DEVICE = os.getenv(“ALSA_DEVICE”, “plughw:1,0”)&lt;br /&gt;
&lt;br /&gt;
# 오디오 포맷&lt;br /&gt;
&lt;br /&gt;
SAMPLE_RATE = 16000      # Whisper 권장값&lt;br /&gt;
CHANNELS = 1&lt;br /&gt;
SAMPLE_WIDTH = 2         # 16-bit PCM (plughw가 32→16 자동 변환)&lt;br /&gt;
&lt;br /&gt;
# VAD (Voice Activity Detection) 파라미터&lt;br /&gt;
&lt;br /&gt;
CHUNK_MS = 100                       # 100ms 단위로 검사&lt;br /&gt;
CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_MS / 1000) * SAMPLE_WIDTH&lt;br /&gt;
SILENCE_RMS_THRESHOLD = 600          # 환경에 맞춰 튜닝 (조용한 방 300, 시끄러우면 1000+)&lt;br /&gt;
START_RMS_THRESHOLD = 900            # 발화 시작 판단 임계치&lt;br /&gt;
SILENCE_END_SEC = 1.5                # 이만큼 무음 지속 시 발화 종료로 판단&lt;br /&gt;
MAX_RECORD_SEC = 30                  # 최대 녹음 길이&lt;br /&gt;
MIN_SPEECH_SEC = 0.4                 # 너무 짧은 입력은 무시&lt;br /&gt;
&lt;br /&gt;
# Claude 모델&lt;br /&gt;
&lt;br /&gt;
CLAUDE_MODEL = “claude-opus-4-5”&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 1) I2S 마이크에서 발화 단위로 녹음&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def record_utterance(out_wav: str) -&amp;gt; bool:&lt;br /&gt;
“””&lt;br /&gt;
arecord를 파이프로 띄워 실시간으로 RMS를 측정하면서&lt;br /&gt;
- 발화 시작이 감지되면 버퍼링 시작&lt;br /&gt;
- 무음이 SILENCE_END_SEC 이상 지속되면 종료&lt;br /&gt;
“””&lt;br /&gt;
cmd = [&lt;br /&gt;
“arecord”,&lt;br /&gt;
“-D”, ALSA_DEVICE,&lt;br /&gt;
“-c”, str(CHANNELS),&lt;br /&gt;
“-r”, str(SAMPLE_RATE),&lt;br /&gt;
“-f”, “S16_LE”,&lt;br /&gt;
“-t”, “raw”,&lt;br /&gt;
“-q”,&lt;br /&gt;
]&lt;br /&gt;
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;🎤 듣는 중... (말씀하세요)&amp;quot;, flush=True)&lt;br /&gt;
frames = []&lt;br /&gt;
started = False&lt;br /&gt;
silence_chunks = 0&lt;br /&gt;
silence_limit = int(SILENCE_END_SEC * 1000 / CHUNK_MS)&lt;br /&gt;
start_time = None&lt;br /&gt;
pre_buffer = []                  # 발화 직전 짧은 구간도 함께 저장 (앞 음 잘림 방지)&lt;br /&gt;
pre_buffer_max = 3               # 약 300ms&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while True:&lt;br /&gt;
        data = proc.stdout.read(CHUNK_BYTES)&lt;br /&gt;
        if not data or len(data) &amp;lt; CHUNK_BYTES:&lt;br /&gt;
            break&lt;br /&gt;
&lt;br /&gt;
        rms = audioop.rms(data, SAMPLE_WIDTH)&lt;br /&gt;
&lt;br /&gt;
        if not started:&lt;br /&gt;
            pre_buffer.append(data)&lt;br /&gt;
            if len(pre_buffer) &amp;gt; pre_buffer_max:&lt;br /&gt;
                pre_buffer.pop(0)&lt;br /&gt;
            if rms &amp;gt; START_RMS_THRESHOLD:&lt;br /&gt;
                started = True&lt;br /&gt;
                start_time = time.time()&lt;br /&gt;
                frames.extend(pre_buffer)&lt;br /&gt;
                frames.append(data)&lt;br /&gt;
                print(&amp;quot;🔴 녹음 시작&amp;quot;, flush=True)&lt;br /&gt;
        else:&lt;br /&gt;
            frames.append(data)&lt;br /&gt;
            if rms &amp;lt; SILENCE_RMS_THRESHOLD:&lt;br /&gt;
                silence_chunks += 1&lt;br /&gt;
            else:&lt;br /&gt;
                silence_chunks = 0&lt;br /&gt;
&lt;br /&gt;
            if silence_chunks &amp;gt;= silence_limit:&lt;br /&gt;
                print(&amp;quot;⏹️  녹음 종료&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
            if time.time() - start_time &amp;gt; MAX_RECORD_SEC:&lt;br /&gt;
                print(&amp;quot;⏹️  최대 길이 도달&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
finally:&lt;br /&gt;
    proc.terminate()&lt;br /&gt;
    try:&lt;br /&gt;
        proc.wait(timeout=1)&lt;br /&gt;
    except subprocess.TimeoutExpired:&lt;br /&gt;
        proc.kill()&lt;br /&gt;
&lt;br /&gt;
if not started:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
duration = (len(frames) * CHUNK_BYTES) / (SAMPLE_RATE * SAMPLE_WIDTH)&lt;br /&gt;
if duration &amp;lt; MIN_SPEECH_SEC:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
with wave.open(out_wav, &amp;quot;wb&amp;quot;) as wf:&lt;br /&gt;
    wf.setnchannels(CHANNELS)&lt;br /&gt;
    wf.setsampwidth(SAMPLE_WIDTH)&lt;br /&gt;
    wf.setframerate(SAMPLE_RATE)&lt;br /&gt;
    wf.writeframes(b&amp;quot;&amp;quot;.join(frames))&lt;br /&gt;
return True&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 2) Whisper API로 STT (한국어 + 영어 코드스위칭 자동)&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def transcribe(audio_path: str) -&amp;gt; str:&lt;br /&gt;
client = OpenAI(api_key=OPENAI_API_KEY)&lt;br /&gt;
with open(audio_path, “rb”) as f:&lt;br /&gt;
result = client.audio.transcriptions.create(&lt;br /&gt;
model=“whisper-1”,&lt;br /&gt;
file=f,&lt;br /&gt;
language=“ko”,   # 주 언어 힌트. 영어 단어 섞여 있으면 자동 인식&lt;br /&gt;
prompt=“한국어 위주이고 영어 기술 용어가 섞일 수 있습니다. “&lt;br /&gt;
“예: Oracle, RAC, Raspberry Pi, GPIO, INMP441.”,&lt;br /&gt;
temperature=0.0,&lt;br /&gt;
)&lt;br /&gt;
return result.text.strip()&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 3) Claude API에 전달해서 응답 받기&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def ask_claude(user_text: str, history: list) -&amp;gt; str:&lt;br /&gt;
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)&lt;br /&gt;
history.append({“role”: “user”, “content”: user_text})&lt;br /&gt;
msg = client.messages.create(&lt;br /&gt;
model=CLAUDE_MODEL,&lt;br /&gt;
max_tokens=1024,&lt;br /&gt;
system=(“당신은 한국어로 답하는 음성 비서입니다. “&lt;br /&gt;
“사용자의 발화는 음성인식 결과라 오타·동음이의어가 있을 수 있으니 “&lt;br /&gt;
“맥락으로 보정해서 이해해 주세요. 간결하게 답하세요.”),&lt;br /&gt;
messages=history,&lt;br /&gt;
)&lt;br /&gt;
answer = msg.content[0].text&lt;br /&gt;
history.append({“role”: “assistant”, “content”: answer})&lt;br /&gt;
# 컨텍스트 비대화 방지: 최근 10턴만 유지&lt;br /&gt;
if len(history) &amp;gt; 20:&lt;br /&gt;
del history[: len(history) - 20]&lt;br /&gt;
return answer&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
if not OPENAI_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 OPENAI_API_KEY 가 필요합니다.”)&lt;br /&gt;
if not ANTHROPIC_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 ANTHROPIC_API_KEY 가 필요합니다.”)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
print(&amp;quot; INMP441 → Whisper → Claude 음성 어시스턴트&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  device   : {ALSA_DEVICE}&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  model    : {CLAUDE_MODEL}&amp;quot;)&lt;br /&gt;
print(&amp;quot;  Ctrl+C 로 종료&amp;quot;)&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
history = []&lt;br /&gt;
while True:&lt;br /&gt;
    try:&lt;br /&gt;
        with tempfile.NamedTemporaryFile(suffix=&amp;quot;.wav&amp;quot;, delete=False) as tmp:&lt;br /&gt;
            wav_path = tmp.name&lt;br /&gt;
&lt;br /&gt;
        ok = record_utterance(wav_path)&lt;br /&gt;
        if not ok:&lt;br /&gt;
            Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🔄 STT 변환 중...&amp;quot;, flush=True)&lt;br /&gt;
        text = transcribe(wav_path)&lt;br /&gt;
        Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
&lt;br /&gt;
        if not text:&lt;br /&gt;
            print(&amp;quot;(인식 실패)\n&amp;quot;)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(f&amp;quot;📝 사용자: {text}&amp;quot;, flush=True)&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🤖 Claude 응답 생성 중...&amp;quot;, flush=True)&lt;br /&gt;
        answer = ask_claude(text, history)&lt;br /&gt;
        print(f&amp;quot;💬 Claude: {answer}&amp;quot;)&lt;br /&gt;
        print(&amp;quot;-&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
    except KeyboardInterrupt:&lt;br /&gt;
        print(&amp;quot;\n종료합니다.&amp;quot;)&lt;br /&gt;
        break&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;⚠️  오류: {type(e).__name__}: {e}&amp;quot;)&lt;br /&gt;
        time.sleep(1)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == “**main**”:&lt;br /&gt;
main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2401</id>
		<title>라즈베리파이 음성인식 프로그램 만들기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%9D%8C%EC%84%B1%EC%9D%B8%EC%8B%9D_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8_%EB%A7%8C%EB%93%A4%EA%B8%B0&amp;diff=2401"/>
		<updated>2026-05-12T09:34:21Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: &amp;lt;source lang=python&amp;gt; #!/usr/bin/env python3  # -*- coding: utf-8 -*-  “”” INMP441 I2S 마이크 → Whisper STT → Claude 응답 Raspberry Pi Zero WH + INMP441 환경 기준 “””  import os import sys import time import wave import audioop import tempfile import subprocess from pathlib import Path  from openai import OpenAI import anthropic  # ============================================================  # 환경설정  # =============================================...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
INMP441 I2S 마이크 → Whisper STT → Claude 응답&lt;br /&gt;
Raspberry Pi Zero WH + INMP441 환경 기준&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import wave&lt;br /&gt;
import audioop&lt;br /&gt;
import tempfile&lt;br /&gt;
import subprocess&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
&lt;br /&gt;
from openai import OpenAI&lt;br /&gt;
import anthropic&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 환경설정&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
OPENAI_API_KEY = os.getenv(“OPENAI_API_KEY”)&lt;br /&gt;
ANTHROPIC_API_KEY = os.getenv(“ANTHROPIC_API_KEY”)&lt;br /&gt;
&lt;br /&gt;
# arecord 디바이스 (arecord -l 로 확인 후 카드 번호 맞추기)&lt;br /&gt;
&lt;br /&gt;
ALSA_DEVICE = os.getenv(“ALSA_DEVICE”, “plughw:1,0”)&lt;br /&gt;
&lt;br /&gt;
# 오디오 포맷&lt;br /&gt;
&lt;br /&gt;
SAMPLE_RATE = 16000      # Whisper 권장값&lt;br /&gt;
CHANNELS = 1&lt;br /&gt;
SAMPLE_WIDTH = 2         # 16-bit PCM (plughw가 32→16 자동 변환)&lt;br /&gt;
&lt;br /&gt;
# VAD (Voice Activity Detection) 파라미터&lt;br /&gt;
&lt;br /&gt;
CHUNK_MS = 100                       # 100ms 단위로 검사&lt;br /&gt;
CHUNK_BYTES = int(SAMPLE_RATE * CHUNK_MS / 1000) * SAMPLE_WIDTH&lt;br /&gt;
SILENCE_RMS_THRESHOLD = 600          # 환경에 맞춰 튜닝 (조용한 방 300, 시끄러우면 1000+)&lt;br /&gt;
START_RMS_THRESHOLD = 900            # 발화 시작 판단 임계치&lt;br /&gt;
SILENCE_END_SEC = 1.5                # 이만큼 무음 지속 시 발화 종료로 판단&lt;br /&gt;
MAX_RECORD_SEC = 30                  # 최대 녹음 길이&lt;br /&gt;
MIN_SPEECH_SEC = 0.4                 # 너무 짧은 입력은 무시&lt;br /&gt;
&lt;br /&gt;
# Claude 모델&lt;br /&gt;
&lt;br /&gt;
CLAUDE_MODEL = “claude-opus-4-5”&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 1) I2S 마이크에서 발화 단위로 녹음&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def record_utterance(out_wav: str) -&amp;gt; bool:&lt;br /&gt;
“””&lt;br /&gt;
arecord를 파이프로 띄워 실시간으로 RMS를 측정하면서&lt;br /&gt;
- 발화 시작이 감지되면 버퍼링 시작&lt;br /&gt;
- 무음이 SILENCE_END_SEC 이상 지속되면 종료&lt;br /&gt;
“””&lt;br /&gt;
cmd = [&lt;br /&gt;
“arecord”,&lt;br /&gt;
“-D”, ALSA_DEVICE,&lt;br /&gt;
“-c”, str(CHANNELS),&lt;br /&gt;
“-r”, str(SAMPLE_RATE),&lt;br /&gt;
“-f”, “S16_LE”,&lt;br /&gt;
“-t”, “raw”,&lt;br /&gt;
“-q”,&lt;br /&gt;
]&lt;br /&gt;
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;🎤 듣는 중... (말씀하세요)&amp;quot;, flush=True)&lt;br /&gt;
frames = []&lt;br /&gt;
started = False&lt;br /&gt;
silence_chunks = 0&lt;br /&gt;
silence_limit = int(SILENCE_END_SEC * 1000 / CHUNK_MS)&lt;br /&gt;
start_time = None&lt;br /&gt;
pre_buffer = []                  # 발화 직전 짧은 구간도 함께 저장 (앞 음 잘림 방지)&lt;br /&gt;
pre_buffer_max = 3               # 약 300ms&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while True:&lt;br /&gt;
        data = proc.stdout.read(CHUNK_BYTES)&lt;br /&gt;
        if not data or len(data) &amp;lt; CHUNK_BYTES:&lt;br /&gt;
            break&lt;br /&gt;
&lt;br /&gt;
        rms = audioop.rms(data, SAMPLE_WIDTH)&lt;br /&gt;
&lt;br /&gt;
        if not started:&lt;br /&gt;
            pre_buffer.append(data)&lt;br /&gt;
            if len(pre_buffer) &amp;gt; pre_buffer_max:&lt;br /&gt;
                pre_buffer.pop(0)&lt;br /&gt;
            if rms &amp;gt; START_RMS_THRESHOLD:&lt;br /&gt;
                started = True&lt;br /&gt;
                start_time = time.time()&lt;br /&gt;
                frames.extend(pre_buffer)&lt;br /&gt;
                frames.append(data)&lt;br /&gt;
                print(&amp;quot;🔴 녹음 시작&amp;quot;, flush=True)&lt;br /&gt;
        else:&lt;br /&gt;
            frames.append(data)&lt;br /&gt;
            if rms &amp;lt; SILENCE_RMS_THRESHOLD:&lt;br /&gt;
                silence_chunks += 1&lt;br /&gt;
            else:&lt;br /&gt;
                silence_chunks = 0&lt;br /&gt;
&lt;br /&gt;
            if silence_chunks &amp;gt;= silence_limit:&lt;br /&gt;
                print(&amp;quot;⏹️  녹음 종료&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
            if time.time() - start_time &amp;gt; MAX_RECORD_SEC:&lt;br /&gt;
                print(&amp;quot;⏹️  최대 길이 도달&amp;quot;, flush=True)&lt;br /&gt;
                break&lt;br /&gt;
finally:&lt;br /&gt;
    proc.terminate()&lt;br /&gt;
    try:&lt;br /&gt;
        proc.wait(timeout=1)&lt;br /&gt;
    except subprocess.TimeoutExpired:&lt;br /&gt;
        proc.kill()&lt;br /&gt;
&lt;br /&gt;
if not started:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
duration = (len(frames) * CHUNK_BYTES) / (SAMPLE_RATE * SAMPLE_WIDTH)&lt;br /&gt;
if duration &amp;lt; MIN_SPEECH_SEC:&lt;br /&gt;
    return False&lt;br /&gt;
&lt;br /&gt;
with wave.open(out_wav, &amp;quot;wb&amp;quot;) as wf:&lt;br /&gt;
    wf.setnchannels(CHANNELS)&lt;br /&gt;
    wf.setsampwidth(SAMPLE_WIDTH)&lt;br /&gt;
    wf.setframerate(SAMPLE_RATE)&lt;br /&gt;
    wf.writeframes(b&amp;quot;&amp;quot;.join(frames))&lt;br /&gt;
return True&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 2) Whisper API로 STT (한국어 + 영어 코드스위칭 자동)&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def transcribe(audio_path: str) -&amp;gt; str:&lt;br /&gt;
client = OpenAI(api_key=OPENAI_API_KEY)&lt;br /&gt;
with open(audio_path, “rb”) as f:&lt;br /&gt;
result = client.audio.transcriptions.create(&lt;br /&gt;
model=“whisper-1”,&lt;br /&gt;
file=f,&lt;br /&gt;
language=“ko”,   # 주 언어 힌트. 영어 단어 섞여 있으면 자동 인식&lt;br /&gt;
prompt=“한국어 위주이고 영어 기술 용어가 섞일 수 있습니다. “&lt;br /&gt;
“예: Oracle, RAC, Raspberry Pi, GPIO, INMP441.”,&lt;br /&gt;
temperature=0.0,&lt;br /&gt;
)&lt;br /&gt;
return result.text.strip()&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 3) Claude API에 전달해서 응답 받기&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def ask_claude(user_text: str, history: list) -&amp;gt; str:&lt;br /&gt;
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)&lt;br /&gt;
history.append({“role”: “user”, “content”: user_text})&lt;br /&gt;
msg = client.messages.create(&lt;br /&gt;
model=CLAUDE_MODEL,&lt;br /&gt;
max_tokens=1024,&lt;br /&gt;
system=(“당신은 한국어로 답하는 음성 비서입니다. “&lt;br /&gt;
“사용자의 발화는 음성인식 결과라 오타·동음이의어가 있을 수 있으니 “&lt;br /&gt;
“맥락으로 보정해서 이해해 주세요. 간결하게 답하세요.”),&lt;br /&gt;
messages=history,&lt;br /&gt;
)&lt;br /&gt;
answer = msg.content[0].text&lt;br /&gt;
history.append({“role”: “assistant”, “content”: answer})&lt;br /&gt;
# 컨텍스트 비대화 방지: 최근 10턴만 유지&lt;br /&gt;
if len(history) &amp;gt; 20:&lt;br /&gt;
del history[: len(history) - 20]&lt;br /&gt;
return answer&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ============================================================&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
if not OPENAI_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 OPENAI_API_KEY 가 필요합니다.”)&lt;br /&gt;
if not ANTHROPIC_API_KEY:&lt;br /&gt;
sys.exit(“환경변수 ANTHROPIC_API_KEY 가 필요합니다.”)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
print(&amp;quot; INMP441 → Whisper → Claude 음성 어시스턴트&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  device   : {ALSA_DEVICE}&amp;quot;)&lt;br /&gt;
print(f&amp;quot;  model    : {CLAUDE_MODEL}&amp;quot;)&lt;br /&gt;
print(&amp;quot;  Ctrl+C 로 종료&amp;quot;)&lt;br /&gt;
print(&amp;quot;=&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
history = []&lt;br /&gt;
while True:&lt;br /&gt;
    try:&lt;br /&gt;
        with tempfile.NamedTemporaryFile(suffix=&amp;quot;.wav&amp;quot;, delete=False) as tmp:&lt;br /&gt;
            wav_path = tmp.name&lt;br /&gt;
&lt;br /&gt;
        ok = record_utterance(wav_path)&lt;br /&gt;
        if not ok:&lt;br /&gt;
            Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🔄 STT 변환 중...&amp;quot;, flush=True)&lt;br /&gt;
        text = transcribe(wav_path)&lt;br /&gt;
        Path(wav_path).unlink(missing_ok=True)&lt;br /&gt;
&lt;br /&gt;
        if not text:&lt;br /&gt;
            print(&amp;quot;(인식 실패)\n&amp;quot;)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        print(f&amp;quot;📝 사용자: {text}&amp;quot;, flush=True)&lt;br /&gt;
&lt;br /&gt;
        print(&amp;quot;🤖 Claude 응답 생성 중...&amp;quot;, flush=True)&lt;br /&gt;
        answer = ask_claude(text, history)&lt;br /&gt;
        print(f&amp;quot;💬 Claude: {answer}&amp;quot;)&lt;br /&gt;
        print(&amp;quot;-&amp;quot; * 60)&lt;br /&gt;
&lt;br /&gt;
    except KeyboardInterrupt:&lt;br /&gt;
        print(&amp;quot;\n종료합니다.&amp;quot;)&lt;br /&gt;
        break&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;⚠️  오류: {type(e).__name__}: {e}&amp;quot;)&lt;br /&gt;
        time.sleep(1)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == “**main**”:&lt;br /&gt;
main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%ED%82%A4%EB%B3%B4%EB%93%9C_%EC%95%8C%ED%8A%B8,%EC%BB%A8%ED%8A%B8%EB%A1%A4,%EC%89%AC%ED%94%84%ED%8A%B8_%ED%82%A4_%EB%88%8C%EB%A6%BC_%ED%99%95%EC%9D%B8_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8&amp;diff=2400</id>
		<title>키보드 알트,컨트롤,쉬프트 키 눌림 확인 프로그램</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%ED%82%A4%EB%B3%B4%EB%93%9C_%EC%95%8C%ED%8A%B8,%EC%BB%A8%ED%8A%B8%EB%A1%A4,%EC%89%AC%ED%94%84%ED%8A%B8_%ED%82%A4_%EB%88%8C%EB%A6%BC_%ED%99%95%EC%9D%B8_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8&amp;diff=2400"/>
		<updated>2026-05-12T07:38:50Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: == 키보드 알트,컨트롤,쉬프트 키 눌림 확인 프로그램  == &amp;lt;source lang=autoti&amp;gt; #include &amp;lt;Misc.au3&amp;gt; #include &amp;lt;GUIConstantsEx.au3&amp;gt; #include &amp;lt;WindowsConstants.au3&amp;gt; #include &amp;lt;GDIPlus.au3&amp;gt; #include &amp;lt;WinAPI.au3&amp;gt;  ; ── 설정 ── Global Const $OVERLAY_FONT_SIZE = 36 Global Const $OVERLAY_FONT_NAME = &amp;quot;맑은 고딕&amp;quot; Global Const $OVERLAY_BG_COLOR = 0xCC000000    ; 반투명 검정 배경 (AARRGGBB) Global Const $OVERLAY_TEXT_COLOR = 0xFFFFFF00   ; 노란색 텍...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 키보드 알트,컨트롤,쉬프트 키 눌림 확인 프로그램  ==&lt;br /&gt;
&amp;lt;source lang=autoti&amp;gt;&lt;br /&gt;
#include &amp;lt;Misc.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;GUIConstantsEx.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;GDIPlus.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; ── 설정 ──&lt;br /&gt;
Global Const $OVERLAY_FONT_SIZE = 36&lt;br /&gt;
Global Const $OVERLAY_FONT_NAME = &amp;quot;맑은 고딕&amp;quot;&lt;br /&gt;
Global Const $OVERLAY_BG_COLOR = 0xCC000000    ; 반투명 검정 배경 (AARRGGBB)&lt;br /&gt;
Global Const $OVERLAY_TEXT_COLOR = 0xFFFFFF00   ; 노란색 텍스트 (AARRGGBB)&lt;br /&gt;
Global Const $OVERLAY_PADDING = 20&lt;br /&gt;
Global Const $OVERLAY_DISPLAY_MS = 3000        ; 표시 시간 (ms)&lt;br /&gt;
Global Const $OVERLAY_CHECK_INTERVAL = 50      ; 키 체크 간격 (ms)&lt;br /&gt;
&lt;br /&gt;
_MAIN()&lt;br /&gt;
&lt;br /&gt;
Func _MAIN()&lt;br /&gt;
    Local $aPressedKeys = _GET_PRESSED_MOD_KEYS()&lt;br /&gt;
&lt;br /&gt;
    If $aPressedKeys[0] = 0 Then Exit  ; 눌린 키 없으면 종료&lt;br /&gt;
&lt;br /&gt;
    Local $sText = _BUILD_DISPLAY_TEXT($aPressedKeys)&lt;br /&gt;
    _SHOW_OVERLAY($sText)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 현재 눌린 수정키 검사 ──&lt;br /&gt;
Func _GET_PRESSED_MOD_KEYS()&lt;br /&gt;
    ; [0] = 개수, [1]~[n] = 키 이름&lt;br /&gt;
    Local $aResult[9]  ; 최대 8개 + 카운트&lt;br /&gt;
    $aResult[0] = 0&lt;br /&gt;
&lt;br /&gt;
    ; Left/Right Shift (VK_LSHIFT=A0, VK_RSHIFT=A1)&lt;br /&gt;
    If _IsPressed(&amp;quot;A0&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;L-Shift&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
    If _IsPressed(&amp;quot;A1&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;R-Shift&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; Left/Right Ctrl (VK_LCONTROL=A2, VK_RCONTROL=A3)&lt;br /&gt;
    If _IsPressed(&amp;quot;A2&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;L-Ctrl&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
    If _IsPressed(&amp;quot;A3&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;R-Ctrl&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; Left/Right Alt (VK_LMENU=A4, VK_RMENU=A5)&lt;br /&gt;
    If _IsPressed(&amp;quot;A4&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;L-Alt&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
    If _IsPressed(&amp;quot;A5&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;R-Alt&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    ; Left/Right Win (VK_LWIN=5B, VK_RWIN=5C)&lt;br /&gt;
    If _IsPressed(&amp;quot;5B&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;L-Win&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
    If _IsPressed(&amp;quot;5C&amp;quot;) Then&lt;br /&gt;
        $aResult[0] += 1&lt;br /&gt;
        $aResult[$aResult[0]] = &amp;quot;R-Win&amp;quot;&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Return $aResult&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 표시할 텍스트 조합 ──&lt;br /&gt;
Func _BUILD_DISPLAY_TEXT($aPressedKeys)&lt;br /&gt;
    Local $sText = &amp;quot;&amp;quot;&lt;br /&gt;
    For $i = 1 To $aPressedKeys[0]&lt;br /&gt;
        If $sText &amp;lt;&amp;gt; &amp;quot;&amp;quot; Then $sText &amp;amp;= &amp;quot; + &amp;quot;&lt;br /&gt;
        $sText &amp;amp;= $aPressedKeys[$i]&lt;br /&gt;
    Next&lt;br /&gt;
    Return &amp;quot;눌린 키: &amp;quot; &amp;amp; $sText&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── GDI+ 레이어드 오버레이 표시 ──&lt;br /&gt;
Func _SHOW_OVERLAY($sText)&lt;br /&gt;
    _GDIPlus_Startup()&lt;br /&gt;
&lt;br /&gt;
    ; 텍스트 크기 측정&lt;br /&gt;
    Local $hMeasureBmp = _GDIPlus_BitmapCreateFromScan0(1, 1)&lt;br /&gt;
    Local $hMeasureGfx = _GDIPlus_ImageGetGraphicsContext($hMeasureBmp)&lt;br /&gt;
    Local $hFontFamily = _GDIPlus_FontFamilyCreate($OVERLAY_FONT_NAME)&lt;br /&gt;
    Local $hFont = _GDIPlus_FontCreate($hFontFamily, $OVERLAY_FONT_SIZE, 1)  ; 1=Bold&lt;br /&gt;
    Local $hFormat = _GDIPlus_StringFormatCreate()&lt;br /&gt;
    Local $tLayout = _GDIPlus_RectFCreate(0, 0, 0, 0)&lt;br /&gt;
    Local $aInfo = _GDIPlus_GraphicsMeasureString($hMeasureGfx, $sText, $hFont, $tLayout, $hFormat)&lt;br /&gt;
    Local $iTextW = Ceiling(DllStructGetData($aInfo[0], &amp;quot;Width&amp;quot;))&lt;br /&gt;
    Local $iTextH = Ceiling(DllStructGetData($aInfo[0], &amp;quot;Height&amp;quot;))&lt;br /&gt;
    _GDIPlus_GraphicsDispose($hMeasureGfx)&lt;br /&gt;
    _GDIPlus_BitmapDispose($hMeasureBmp)&lt;br /&gt;
&lt;br /&gt;
    ; 오버레이 크기&lt;br /&gt;
    Local $iW = $iTextW + ($OVERLAY_PADDING * 2)&lt;br /&gt;
    Local $iH = $iTextH + ($OVERLAY_PADDING * 2)&lt;br /&gt;
    Local $iCorner = 16  ; 둥근 모서리 반지름&lt;br /&gt;
&lt;br /&gt;
    ; 화면 중앙 좌표&lt;br /&gt;
    Local $iScreenW = @DesktopWidth&lt;br /&gt;
    Local $iScreenH = @DesktopHeight&lt;br /&gt;
    Local $iX = Int(($iScreenW - $iW) / 2)&lt;br /&gt;
    Local $iY = Int(($iScreenH - $iH) / 2)&lt;br /&gt;
&lt;br /&gt;
    ; 레이어드 윈도우 생성 (투명, 항상위, 클릭통과)&lt;br /&gt;
    Local $hGUI = GUICreate(&amp;quot;&amp;quot;, $iW, $iH, $iX, $iY, _&lt;br /&gt;
            $WS_POPUP, _&lt;br /&gt;
            BitOR($WS_EX_LAYERED, $WS_EX_TOPMOST, $WS_EX_TRANSPARENT, $WS_EX_TOOLWINDOW))&lt;br /&gt;
&lt;br /&gt;
    ; GDI+ 비트맵에 그리기&lt;br /&gt;
    Local $hBitmap = _GDIPlus_BitmapCreateFromScan0($iW, $iH)&lt;br /&gt;
    Local $hGraphics = _GDIPlus_ImageGetGraphicsContext($hBitmap)&lt;br /&gt;
    _GDIPlus_GraphicsSetSmoothingMode($hGraphics, 4)  ; AntiAlias&lt;br /&gt;
    _GDIPlus_GraphicsSetTextRenderingHint($hGraphics, 5)  ; ClearType&lt;br /&gt;
&lt;br /&gt;
    ; 둥근 사각형 배경&lt;br /&gt;
    Local $hPath = _GDIPlus_PathCreate()&lt;br /&gt;
    _GDIPlus_PathAddArc($hPath, 0, 0, $iCorner * 2, $iCorner * 2, 180, 90)&lt;br /&gt;
    _GDIPlus_PathAddArc($hPath, $iW - $iCorner * 2, 0, $iCorner * 2, $iCorner * 2, 270, 90)&lt;br /&gt;
    _GDIPlus_PathAddArc($hPath, $iW - $iCorner * 2, $iH - $iCorner * 2, $iCorner * 2, $iCorner * 2, 0, 90)&lt;br /&gt;
    _GDIPlus_PathAddArc($hPath, 0, $iH - $iCorner * 2, $iCorner * 2, $iCorner * 2, 90, 90)&lt;br /&gt;
    _GDIPlus_PathCloseFigure($hPath)&lt;br /&gt;
&lt;br /&gt;
    Local $hBgBrush = _GDIPlus_BrushCreateSolid($OVERLAY_BG_COLOR)&lt;br /&gt;
    _GDIPlus_GraphicsFillPath($hGraphics, $hPath, $hBgBrush)&lt;br /&gt;
&lt;br /&gt;
    ; 텍스트 그리기&lt;br /&gt;
    Local $hTextBrush = _GDIPlus_BrushCreateSolid($OVERLAY_TEXT_COLOR)&lt;br /&gt;
    Local $tTextRect = _GDIPlus_RectFCreate($OVERLAY_PADDING, $OVERLAY_PADDING, $iTextW, $iTextH)&lt;br /&gt;
    _GDIPlus_GraphicsDrawStringEx($hGraphics, $sText, $hFont, $tTextRect, $hFormat, $hTextBrush)&lt;br /&gt;
&lt;br /&gt;
    ; 레이어드 윈도우에 적용&lt;br /&gt;
    Local $hHBitmap = _GDIPlus_BitmapCreateHBITMAPFromBitmap($hBitmap)&lt;br /&gt;
    Local $hDC_Screen = _WinAPI_GetDC(0)&lt;br /&gt;
    Local $hDC_Mem = _WinAPI_CreateCompatibleDC($hDC_Screen)&lt;br /&gt;
    Local $hOldBmp = _WinAPI_SelectObject($hDC_Mem, $hHBitmap)&lt;br /&gt;
&lt;br /&gt;
    Local $tSize = DllStructCreate(&amp;quot;long X;long Y&amp;quot;)&lt;br /&gt;
    DllStructSetData($tSize, &amp;quot;X&amp;quot;, $iW)&lt;br /&gt;
    DllStructSetData($tSize, &amp;quot;Y&amp;quot;, $iH)&lt;br /&gt;
    Local $tPos = DllStructCreate(&amp;quot;long X;long Y&amp;quot;)&lt;br /&gt;
    DllStructSetData($tPos, &amp;quot;X&amp;quot;, $iX)&lt;br /&gt;
    DllStructSetData($tPos, &amp;quot;Y&amp;quot;, $iY)&lt;br /&gt;
    Local $tSrcPos = DllStructCreate(&amp;quot;long X;long Y&amp;quot;)&lt;br /&gt;
    DllStructSetData($tSrcPos, &amp;quot;X&amp;quot;, 0)&lt;br /&gt;
    DllStructSetData($tSrcPos, &amp;quot;Y&amp;quot;, 0)&lt;br /&gt;
    Local $tBlend = DllStructCreate(&amp;quot;byte Op;byte Flags;byte Alpha;byte Format&amp;quot;)&lt;br /&gt;
    DllStructSetData($tBlend, &amp;quot;Op&amp;quot;, 0)       ; AC_SRC_OVER&lt;br /&gt;
    DllStructSetData($tBlend, &amp;quot;Flags&amp;quot;, 0)&lt;br /&gt;
    DllStructSetData($tBlend, &amp;quot;Alpha&amp;quot;, 255)  ; 전체 알파 (비트맵 자체 알파 사용)&lt;br /&gt;
    DllStructSetData($tBlend, &amp;quot;Format&amp;quot;, 1)   ; AC_SRC_ALPHA&lt;br /&gt;
&lt;br /&gt;
    _WinAPI_UpdateLayeredWindow($hGUI, $hDC_Screen, $tPos, $tSize, $hDC_Mem, $tSrcPos, 0, $tBlend, 2)&lt;br /&gt;
&lt;br /&gt;
    GUISetState(@SW_SHOWNOACTIVATE, $hGUI)&lt;br /&gt;
&lt;br /&gt;
    ; ── 표시 유지: 키가 떼지거나 시간 초과 시 종료 ──&lt;br /&gt;
    Local $iStart = TimerInit()&lt;br /&gt;
    While TimerDiff($iStart) &amp;lt; $OVERLAY_DISPLAY_MS&lt;br /&gt;
        ; 모든 수정키가 떼졌는지 체크&lt;br /&gt;
        If Not _IsPressed(&amp;quot;A0&amp;quot;) And Not _IsPressed(&amp;quot;A1&amp;quot;) And _&lt;br /&gt;
           Not _IsPressed(&amp;quot;A2&amp;quot;) And Not _IsPressed(&amp;quot;A3&amp;quot;) And _&lt;br /&gt;
           Not _IsPressed(&amp;quot;A4&amp;quot;) And Not _IsPressed(&amp;quot;A5&amp;quot;) And _&lt;br /&gt;
           Not _IsPressed(&amp;quot;5B&amp;quot;) And Not _IsPressed(&amp;quot;5C&amp;quot;) Then&lt;br /&gt;
            ExitLoop&lt;br /&gt;
        EndIf&lt;br /&gt;
        Sleep($OVERLAY_CHECK_INTERVAL)&lt;br /&gt;
    WEnd&lt;br /&gt;
&lt;br /&gt;
    ; ── 리소스 정리 ──&lt;br /&gt;
    _WinAPI_SelectObject($hDC_Mem, $hOldBmp)&lt;br /&gt;
    _WinAPI_DeleteDC($hDC_Mem)&lt;br /&gt;
    _WinAPI_ReleaseDC(0, $hDC_Screen)&lt;br /&gt;
    _WinAPI_DeleteObject($hHBitmap)&lt;br /&gt;
    _GDIPlus_BrushDispose($hTextBrush)&lt;br /&gt;
    _GDIPlus_BrushDispose($hBgBrush)&lt;br /&gt;
    _GDIPlus_PathDispose($hPath)&lt;br /&gt;
    _GDIPlus_FontDispose($hFont)&lt;br /&gt;
    _GDIPlus_FontFamilyDispose($hFontFamily)&lt;br /&gt;
    _GDIPlus_StringFormatDispose($hFormat)&lt;br /&gt;
    _GDIPlus_GraphicsDispose($hGraphics)&lt;br /&gt;
    _GDIPlus_BitmapDispose($hBitmap)&lt;br /&gt;
    _GDIPlus_Shutdown()&lt;br /&gt;
&lt;br /&gt;
    GUIDelete($hGUI)&lt;br /&gt;
EndFunc&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%BD%94%EC%8A%A4%ED%94%BC_%EB%8C%80%ED%98%95%EC%A3%BC_%EA%B3%BC%ED%8F%AD%EB%9D%BD_%EC%A3%BC%EC%8B%9D_%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&amp;diff=2399</id>
		<title>코스피 대형주 과폭락 주식 모니터링</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%BD%94%EC%8A%A4%ED%94%BC_%EB%8C%80%ED%98%95%EC%A3%BC_%EA%B3%BC%ED%8F%AD%EB%9D%BD_%EC%A3%BC%EC%8B%9D_%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&amp;diff=2399"/>
		<updated>2026-05-12T01:22:03Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: **✅ Telegram 알림 기능이 추가된 최종 버전**입니다.  ### 1. 먼저 Telegram Bot 설정 방법 (필수)  1. Telegram에서 **@BotFather** 검색 → `/newbot` 명령어로 봇 생성 2. Bot Token 복사 (예: `123456789:AAH...`) 3. 봇을 본인 채팅방에 추가 후, 아래 링크로 Chat ID 확인:        ```    https://api.telegram.org/bot[YOUR_BOT_TOKEN]/getUpdates    ``` 4. Chat ID 복사 (보통 `-`로 시작하는 숫자)  -----  ### 2. 완전한 코드 (...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;**✅ Telegram 알림 기능이 추가된 최종 버전**입니다.&lt;br /&gt;
&lt;br /&gt;
### 1. 먼저 Telegram Bot 설정 방법 (필수)&lt;br /&gt;
&lt;br /&gt;
1. Telegram에서 **@BotFather** 검색 → `/newbot` 명령어로 봇 생성&lt;br /&gt;
2. Bot Token 복사 (예: `123456789:AAH...`)&lt;br /&gt;
3. 봇을 본인 채팅방에 추가 후, 아래 링크로 Chat ID 확인:&lt;br /&gt;
   &lt;br /&gt;
   ```&lt;br /&gt;
   https://api.telegram.org/bot[YOUR_BOT_TOKEN]/getUpdates&lt;br /&gt;
   ```&lt;br /&gt;
4. Chat ID 복사 (보통 `-`로 시작하는 숫자)&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### 2. 완전한 코드 (pandas_ta + Telegram 알림)&lt;br /&gt;
&lt;br /&gt;
```python&lt;br /&gt;
import FinanceDataReader as fdr&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import pandas_ta as ta&lt;br /&gt;
from datetime import datetime, timedelta&lt;br /&gt;
import requests&lt;br /&gt;
import warnings&lt;br /&gt;
warnings.filterwarnings(&#039;ignore&#039;)&lt;br /&gt;
&lt;br /&gt;
# ==================== 설정 ====================&lt;br /&gt;
# ==================== Telegram 설정 ====================&lt;br /&gt;
TELEGRAM_BOT_TOKEN = &amp;quot;YOUR_BOT_TOKEN_HERE&amp;quot;      # ← 여기에 Bot Token 입력&lt;br /&gt;
TELEGRAM_CHAT_ID = &amp;quot;YOUR_CHAT_ID_HERE&amp;quot;           # ← 여기에 Chat ID 입력&lt;br /&gt;
&lt;br /&gt;
RSI_THRESHOLD = 30&lt;br /&gt;
BB_PERIOD = 20&lt;br /&gt;
BB_STD = 2.0&lt;br /&gt;
MA_SHORT = 20&lt;br /&gt;
MA_LONG = 60&lt;br /&gt;
&lt;br /&gt;
MONTH1_THRESHOLD = -15   # %&lt;br /&gt;
MONTH3_THRESHOLD = -20   # %&lt;br /&gt;
&lt;br /&gt;
# ==================== Telegram 메시지 전송 함수 ====================&lt;br /&gt;
def send_telegram_message(message):&lt;br /&gt;
    if not TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKEN == &amp;quot;YOUR_BOT_TOKEN_HERE&amp;quot;:&lt;br /&gt;
        print(&amp;quot;⚠️ Telegram 설정이 완료되지 않았습니다.&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
    &lt;br /&gt;
    url = f&amp;quot;https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage&amp;quot;&lt;br /&gt;
    payload = {&lt;br /&gt;
        &amp;quot;chat_id&amp;quot;: TELEGRAM_CHAT_ID,&lt;br /&gt;
        &amp;quot;text&amp;quot;: message,&lt;br /&gt;
        &amp;quot;parse_mode&amp;quot;: &amp;quot;Markdown&amp;quot;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.post(url, json=payload, timeout=10)&lt;br /&gt;
        if response.status_code == 200:&lt;br /&gt;
            print(&amp;quot;📤 Telegram 알림 전송 완료&amp;quot;)&lt;br /&gt;
            return True&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;❌ Telegram 전송 실패: {response.text}&amp;quot;)&lt;br /&gt;
            return False&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;❌ Telegram 전송 오류: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
# ==================== Top 100 추출 ====================&lt;br /&gt;
def get_kospi_top100():&lt;br /&gt;
    kospi = fdr.StockListing(&#039;KOSPI&#039;)&lt;br /&gt;
    if &#039;Marcap&#039; in kospi.columns:&lt;br /&gt;
        top100 = kospi.nlargest(100, &#039;Marcap&#039;).reset_index(drop=True)&lt;br /&gt;
    else:&lt;br /&gt;
        top100 = kospi.head(100).reset_index(drop=True)&lt;br /&gt;
    &lt;br /&gt;
    print(f&amp;quot;✅ KOSPI 시가총액 Top 100 로드 완료: {len(top100)}개&amp;quot;)&lt;br /&gt;
    return top100[[&#039;Symbol&#039;, &#039;Name&#039;]]&lt;br /&gt;
&lt;br /&gt;
# ==================== 지표 계산 ====================&lt;br /&gt;
def calculate_indicators(df):&lt;br /&gt;
    if len(df) &amp;lt; 100:&lt;br /&gt;
        return None&lt;br /&gt;
    &lt;br /&gt;
    close = df[&#039;Close&#039;]&lt;br /&gt;
    high = df[&#039;High&#039;]&lt;br /&gt;
    low = df[&#039;Low&#039;]&lt;br /&gt;
    &lt;br /&gt;
    df = df.copy()&lt;br /&gt;
    &lt;br /&gt;
    df[&#039;RSI&#039;] = ta.rsi(close, length=14)&lt;br /&gt;
    bb = ta.bbands(close, length=BB_PERIOD, std=BB_STD)&lt;br /&gt;
    df = pd.concat([df, bb], axis=1)&lt;br /&gt;
    &lt;br /&gt;
    df[&#039;MA20&#039;] = ta.sma(close, length=MA_SHORT)&lt;br /&gt;
    df[&#039;MA60&#039;] = ta.sma(close, length=MA_LONG)&lt;br /&gt;
    &lt;br /&gt;
    latest = df.iloc[-1]&lt;br /&gt;
    &lt;br /&gt;
    return_1m = (latest[&#039;Close&#039;] / df[&#039;Close&#039;].iloc[-22] - 1) * 100 if len(df) &amp;gt;= 22 else None&lt;br /&gt;
    return_3m = (latest[&#039;Close&#039;] / df[&#039;Close&#039;].iloc[-66] - 1) * 100 if len(df) &amp;gt;= 66 else None&lt;br /&gt;
    &lt;br /&gt;
    return {&lt;br /&gt;
        &#039;RSI&#039;: latest[&#039;RSI&#039;],&lt;br /&gt;
        &#039;BB_Lower&#039;: latest[f&#039;BBL_{BB_PERIOD}_{BB_STD:.1f}&#039;],&lt;br /&gt;
        &#039;Close&#039;: latest[&#039;Close&#039;],&lt;br /&gt;
        &#039;MA20&#039;: latest[&#039;MA20&#039;],&lt;br /&gt;
        &#039;MA60&#039;: latest[&#039;MA60&#039;],&lt;br /&gt;
        &#039;Return_1M&#039;: return_1m,&lt;br /&gt;
        &#039;Return_3M&#039;: return_3m&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
# ==================== 메인 함수 ====================&lt;br /&gt;
def monitor_stocks():&lt;br /&gt;
    top100 = get_kospi_top100()&lt;br /&gt;
    results = []&lt;br /&gt;
    &lt;br /&gt;
    end_date = datetime.today().strftime(&#039;%Y-%m-%d&#039;)&lt;br /&gt;
    start_date = (datetime.today() - timedelta(days=500)).strftime(&#039;%Y-%m-%d&#039;)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;🔄 기술적 지표 계산 중...&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    for idx, row in top100.iterrows():&lt;br /&gt;
        try:&lt;br /&gt;
            symbol = row[&#039;Symbol&#039;]&lt;br /&gt;
            name = row[&#039;Name&#039;]&lt;br /&gt;
            &lt;br /&gt;
            df = fdr.DataReader(symbol, start_date, end_date)&lt;br /&gt;
            if df.empty or len(df) &amp;lt; 80:&lt;br /&gt;
                continue&lt;br /&gt;
                &lt;br /&gt;
            ind = calculate_indicators(df)&lt;br /&gt;
            if ind is None or pd.isna(ind[&#039;RSI&#039;]):&lt;br /&gt;
                continue&lt;br /&gt;
            &lt;br /&gt;
            conditions_met = []&lt;br /&gt;
            &lt;br /&gt;
            if ind[&#039;RSI&#039;] &amp;lt;= RSI_THRESHOLD:&lt;br /&gt;
                conditions_met.append(f&amp;quot;RSI({ind[&#039;RSI&#039;]:.1f})&amp;quot;)&lt;br /&gt;
            &lt;br /&gt;
            if ind[&#039;Close&#039;] &amp;lt;= ind[&#039;BB_Lower&#039;] * 1.01:&lt;br /&gt;
                conditions_met.append(&amp;quot;BB Lower&amp;quot;)&lt;br /&gt;
            &lt;br /&gt;
            ma20_dev = (ind[&#039;Close&#039;] / ind[&#039;MA20&#039;] - 1) * 100&lt;br /&gt;
            ma60_dev = (ind[&#039;Close&#039;] / ind[&#039;MA60&#039;] - 1) * 100&lt;br /&gt;
            &lt;br /&gt;
            if ma20_dev &amp;lt;= -15 or ma60_dev &amp;lt;= -25:&lt;br /&gt;
                conditions_met.append(f&amp;quot;MA괴리({ma20_dev:.1f}%)&amp;quot;)&lt;br /&gt;
            &lt;br /&gt;
            if (ind[&#039;Return_1M&#039;] and ind[&#039;Return_1M&#039;] &amp;lt;= MONTH1_THRESHOLD) or \&lt;br /&gt;
               (ind[&#039;Return_3M&#039;] and ind[&#039;Return_3M&#039;] &amp;lt;= MONTH3_THRESHOLD):&lt;br /&gt;
                conditions_met.append(f&amp;quot;하락({ind[&#039;Return_1M&#039;]:.1f}%/{ind[&#039;Return_3M&#039;]:.1f}%)&amp;quot;)&lt;br /&gt;
            &lt;br /&gt;
            if conditions_met:&lt;br /&gt;
                results.append({&lt;br /&gt;
                    &#039;순위&#039;: idx + 1,&lt;br /&gt;
                    &#039;종목코드&#039;: symbol,&lt;br /&gt;
                    &#039;종목명&#039;: name,&lt;br /&gt;
                    &#039;현재가&#039;: int(ind[&#039;Close&#039;]),&lt;br /&gt;
                    &#039;RSI&#039;: round(ind[&#039;RSI&#039;], 1),&lt;br /&gt;
                    &#039;조건&#039;: &amp;quot; | &amp;quot;.join(conditions_met),&lt;br /&gt;
                    &#039;1개월&#039;: f&amp;quot;{ind[&#039;Return_1M&#039;]:.1f}%&amp;quot; if ind[&#039;Return_1M&#039;] else &amp;quot;-&amp;quot;,&lt;br /&gt;
                    &#039;3개월&#039;: f&amp;quot;{ind[&#039;Return_3M&#039;]:.1f}%&amp;quot; if ind[&#039;Return_3M&#039;] else &amp;quot;-&amp;quot;&lt;br /&gt;
                })&lt;br /&gt;
        except:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
    # 결과 처리&lt;br /&gt;
    if results:&lt;br /&gt;
        df_result = pd.DataFrame(results)&lt;br /&gt;
        &lt;br /&gt;
        print(&amp;quot;\n&amp;quot; + &amp;quot;=&amp;quot;*140)&lt;br /&gt;
        print(f&amp;quot;🔥 KOSPI Top 100 조건 충족 종목 ({len(df_result)}개) - {datetime.today().strftime(&#039;%Y-%m-%d %H:%M&#039;)}&amp;quot;)&lt;br /&gt;
        print(&amp;quot;=&amp;quot;*140)&lt;br /&gt;
        print(df_result.to_string(index=False))&lt;br /&gt;
        &lt;br /&gt;
        # Telegram 메시지 만들기&lt;br /&gt;
        msg = f&amp;quot;🔥 *KOSPI Top 100 고급지표 알림*\n&amp;quot;&lt;br /&gt;
        msg += f&amp;quot;📅 {datetime.today().strftime(&#039;%Y-%m-%d %H:%M&#039;)}\n&amp;quot;&lt;br /&gt;
        msg += f&amp;quot;📊 조건 충족: {len(df_result)}개\n\n&amp;quot;&lt;br /&gt;
        &lt;br /&gt;
        for _, row in df_result.iterrows():&lt;br /&gt;
            msg += f&amp;quot;• *{row[&#039;종목명&#039;]}* ({row[&#039;종목코드&#039;]})\n&amp;quot;&lt;br /&gt;
            msg += f&amp;quot;   현재가: {row[&#039;현재가&#039;]:,}원 | RSI: {row[&#039;RSI&#039;]}\n&amp;quot;&lt;br /&gt;
            msg += f&amp;quot;   조건: {row[&#039;조건&#039;]}\n\n&amp;quot;&lt;br /&gt;
        &lt;br /&gt;
        send_telegram_message(msg)&lt;br /&gt;
        &lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;✅ 오늘은 조건을 충족하는 대형주가 없습니다.&amp;quot;)&lt;br /&gt;
        # send_telegram_message(&amp;quot;✅ 오늘 KOSPI Top 100 중 조건 충족 종목이 없습니다.&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    monitor_stocks()&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### 사용법&lt;br /&gt;
&lt;br /&gt;
1. `TELEGRAM_BOT_TOKEN`과 `TELEGRAM_CHAT_ID`를 실제 값으로 바꾸기&lt;br /&gt;
2. 파일 저장 후 `python kospi_monitor.py` 실행&lt;br /&gt;
&lt;br /&gt;
원하시면 **매일 자동 실행**을 위한 스케줄러(crond / Windows Task Scheduler) 설정 방법도 알려드릴 수 있어요!&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=Autoit_%ED%82%A4%EB%B3%B4%EB%93%9C_%ED%9B%84%ED%82%B9&amp;diff=2398</id>
		<title>Autoit 키보드 후킹</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=Autoit_%ED%82%A4%EB%B3%B4%EB%93%9C_%ED%9B%84%ED%82%B9&amp;diff=2398"/>
		<updated>2026-05-08T06:09:36Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 키보드 후킹 예제 ==&lt;br /&gt;
* AutoIt에서 SetWindowsHookEx를 이용해 키보드 입력을 가로채고, 특정 키를 F13~F20으로 매핑하는 예제&lt;br /&gt;
* 예: CapsLock 키를 누르면 F13(가상 키 코드 0x7C)을 대신 보내도록 시스템 레벨에서 처리합니다.&lt;br /&gt;
* 이 코드는 모든 창에서 작동하며, 일반적인 HotKeySet()보다 안정적입니다.&lt;br /&gt;
&lt;br /&gt;
=== 준비 사항 ===&lt;br /&gt;
# 반드시 AutoIt을 관리자 권한으로 실행하세요.&lt;br /&gt;
# WinAPIEx.au3 또는 유사한 API 확장 라이브러리가 필요합니다.&lt;br /&gt;
&lt;br /&gt;
⸻&lt;br /&gt;
&lt;br /&gt;
=== 소스 코드 예제 (SetWindowsHookEx 기반 키보드 후킹) ===&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StructureConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global $hHook, $hMod&lt;br /&gt;
&lt;br /&gt;
; DLL 콜백 등록&lt;br /&gt;
Global $pFunc = DllCallbackRegister(&amp;quot;_LowLevelKeyboardProc&amp;quot;, &amp;quot;long&amp;quot;, &amp;quot;int;wparam;lparam&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
; 현재 모듈 핸들&lt;br /&gt;
$hMod = _WinAPI_GetModuleHandle(0)&lt;br /&gt;
&lt;br /&gt;
; 후킹 시작 (WH_KEYBOARD_LL = 13)&lt;br /&gt;
$hHook = _WinAPI_SetWindowsHookEx($WH_KEYBOARD_LL, DllCallbackGetPtr($pFunc), $hMod, 0)&lt;br /&gt;
&lt;br /&gt;
If @error Or $hHook = 0 Then&lt;br /&gt;
    MsgBox(16, &amp;quot;Error&amp;quot;, &amp;quot;Hook 설치 실패!&amp;quot;)&lt;br /&gt;
    Exit&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
; 메시지 루프 유지&lt;br /&gt;
While True&lt;br /&gt;
    Sleep(100)&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
; 프로그램 종료 시 후킹 해제&lt;br /&gt;
DllCallbackFree($pFunc)&lt;br /&gt;
_WinAPI_UnhookWindowsHookEx($hHook)&lt;br /&gt;
&lt;br /&gt;
; 후킹 함수 정의&lt;br /&gt;
Func _LowLevelKeyboardProc($nCode, $wParam, $lParam)&lt;br /&gt;
    If $nCode &amp;lt; 0 Then&lt;br /&gt;
        Return _WinAPI_CallNextHookEx($hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Local $tKBDLLHOOKSTRUCT = DllStructCreate($tagKBDLLHOOKSTRUCT, $lParam)&lt;br /&gt;
    Local $vkCode = DllStructGetData($tKBDLLHOOKSTRUCT, &amp;quot;vkCode&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ; 눌림 이벤트만 처리&lt;br /&gt;
    If $wParam = $WM_KEYDOWN Or $wParam = $WM_SYSKEYDOWN Then&lt;br /&gt;
        ; CapsLock(0x14) 키를 F13(0x7C)로 대체&lt;br /&gt;
        If $vkCode = 0x14 Then&lt;br /&gt;
            _SendVirtualKey(0x7C) ; F13&lt;br /&gt;
            Return 1 ; 원래 키는 무시&lt;br /&gt;
        EndIf&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Return _WinAPI_CallNextHookEx($hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendVirtualKey($vk)&lt;br /&gt;
    _WinAPI_Keybd_Event($vk, 0, 0, 0) ; key down&lt;br /&gt;
    _WinAPI_Keybd_Event($vk, 0, $KEYEVENTF_KEYUP, 0) ; key up&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 프로그램 확장 예시 ===&lt;br /&gt;
* 다른 키도 다음처럼 바꿀 수 있습니다:&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If $vkCode = 0x2D Then ; Insert&lt;br /&gt;
    _SendVirtualKey(0x7D) ; F14&lt;br /&gt;
    Return 1&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
If $vkCode = 0x91 Then ; ScrollLock&lt;br /&gt;
    _SendVirtualKey(0x7E) ; F15&lt;br /&gt;
    Return 1&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 참고자료 ===&lt;br /&gt;
&lt;br /&gt;
키 이름	가상 키 코드 (Hex)	설명&lt;br /&gt;
F13 ~ F20	0x7C ~ 0x83	확장 펑션키&lt;br /&gt;
CapsLock	0x14	일반 키&lt;br /&gt;
Insert	0x2D	일반 키&lt;br /&gt;
Scroll Lock	0x91	일반 키&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== ⚠️ 주의사항 ===&lt;br /&gt;
# 후킹은 시스템 자원을 사용하는 민감한 작업입니다.&lt;br /&gt;
# 잘못 구현 시 전체 시스템이 느려질 수 있습니다.&lt;br /&gt;
# 키를 너무 많이 리매핑하면 충돌이 생길 수 있으므로 필요한 키만 설정하세요.&lt;br /&gt;
#&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 트레이에 등록 하여 사용 하는 방법 ==&lt;br /&gt;
# 키보드 후킹 프로그램을 트레이 아이콘으로 실행하고, 우클릭 시 종료 메뉴가 뜨도록 만들기&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 기능 요약 ===&lt;br /&gt;
# CapsLock 키를 F13으로 변환&lt;br /&gt;
# 트레이 아이콘 표시&lt;br /&gt;
# 트레이 아이콘 우클릭 → 종료 메뉴 제공&lt;br /&gt;
&lt;br /&gt;
=== 전체 코드 (트레이 포함) ===&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
&lt;br /&gt;
#include &amp;lt;WinAPI.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;StructureConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;TrayConstants.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;MsgBoxConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global $hHook, $hMod&lt;br /&gt;
Global $pFunc = DllCallbackRegister(&amp;quot;_LowLevelKeyboardProc&amp;quot;, &amp;quot;long&amp;quot;, &amp;quot;int;wparam;lparam&amp;quot;)&lt;br /&gt;
Global $hMod = _WinAPI_GetModuleHandle(0)&lt;br /&gt;
&lt;br /&gt;
; 트레이 아이콘 설정&lt;br /&gt;
#NoTrayIcon&lt;br /&gt;
TraySetToolTip(&amp;quot;F13 키 후킹 프로그램&amp;quot;)&lt;br /&gt;
TraySetClick(8) ; 우클릭 감지&lt;br /&gt;
TrayCreateItem(&amp;quot;종료&amp;quot;)&lt;br /&gt;
TraySetState($TRAY_ICONSTATE_SHOW)&lt;br /&gt;
&lt;br /&gt;
; 후킹 설치&lt;br /&gt;
$hHook = _WinAPI_SetWindowsHookEx($WH_KEYBOARD_LL, DllCallbackGetPtr($pFunc), $hMod, 0)&lt;br /&gt;
If @error Or $hHook = 0 Then&lt;br /&gt;
    MsgBox($MB_ICONERROR, &amp;quot;오류&amp;quot;, &amp;quot;후킹 설치 실패!&amp;quot;)&lt;br /&gt;
    Exit&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
; 메시지 루프&lt;br /&gt;
While True&lt;br /&gt;
    Switch TrayGetMsg()&lt;br /&gt;
        Case 0 ; 아무 동작 없음&lt;br /&gt;
            ; pass&lt;br /&gt;
        Case 1 ; &amp;quot;종료&amp;quot; 클릭 시&lt;br /&gt;
            ExitLoop&lt;br /&gt;
    EndSwitch&lt;br /&gt;
    Sleep(50)&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
; 종료 처리&lt;br /&gt;
_WinAPI_UnhookWindowsHookEx($hHook)&lt;br /&gt;
DllCallbackFree($pFunc)&lt;br /&gt;
TraySetState($TRAY_ICONSTATE_HIDE)&lt;br /&gt;
Exit&lt;br /&gt;
&lt;br /&gt;
; 후킹 함수&lt;br /&gt;
Func _LowLevelKeyboardProc($nCode, $wParam, $lParam)&lt;br /&gt;
    If $nCode &amp;lt; 0 Then&lt;br /&gt;
        Return _WinAPI_CallNextHookEx($hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Local $tKBDLLHOOKSTRUCT = DllStructCreate($tagKBDLLHOOKSTRUCT, $lParam)&lt;br /&gt;
    Local $vkCode = DllStructGetData($tKBDLLHOOKSTRUCT, &amp;quot;vkCode&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ; 눌림만 처리&lt;br /&gt;
    If $wParam = $WM_KEYDOWN Or $wParam = $WM_SYSKEYDOWN Then&lt;br /&gt;
        ; CapsLock → F13&lt;br /&gt;
        If $vkCode = 0x14 Then&lt;br /&gt;
            _SendVirtualKey(0x7C) ; F13&lt;br /&gt;
            Return 1 ; 원래 키 무시&lt;br /&gt;
        EndIf&lt;br /&gt;
    EndIf&lt;br /&gt;
&lt;br /&gt;
    Return _WinAPI_CallNextHookEx($hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
Func _SendVirtualKey($vk)&lt;br /&gt;
    _WinAPI_Keybd_Event($vk, 0, 0, 0) ; down&lt;br /&gt;
    _WinAPI_Keybd_Event($vk, 0, $KEYEVENTF_KEYUP, 0) ; up&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 실행 방법 ===&lt;br /&gt;
# .au3 파일로 저장 (예: F13_Tray.au3)&lt;br /&gt;
# AutoIt 스크립트로 실행하거나, SciTE 또는 Aut2Exe로 .exe로 컴파일&lt;br /&gt;
# 실행 후 트레이 아이콘 확인&lt;br /&gt;
# CapsLock 키를 누르면 F13처럼 동작&lt;br /&gt;
# 트레이 아이콘 우클릭 → 종료&lt;br /&gt;
&lt;br /&gt;
=== 추가 팁 ===&lt;br /&gt;
* 아이콘 변경: TraySetIcon(&amp;quot;icon.ico&amp;quot;)&lt;br /&gt;
* 더 많은 단축키 추가 가능 (Insert → F14, ScrollLock → F15 등)&lt;br /&gt;
* TrayCreateItem()에 여러 항목 추가해 기능 확장 가능&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=autoit&amp;gt;&lt;br /&gt;
#include &amp;lt;WinAPIProc.au3&amp;gt;&lt;br /&gt;
#include &amp;lt;WindowsConstants.au3&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Global Const $WH_KEYBOARD_LL = 13&lt;br /&gt;
Global Const $WM_KEYDOWN    = 0x0100&lt;br /&gt;
Global Const $WM_KEYUP      = 0x0101&lt;br /&gt;
Global Const $WM_SYSKEYDOWN = 0x0104&lt;br /&gt;
Global Const $WM_SYSKEYUP   = 0x0105&lt;br /&gt;
&lt;br /&gt;
Global $g_hHook = 0&lt;br /&gt;
Global $g_hStub_KeyProc = 0&lt;br /&gt;
&lt;br /&gt;
; ── 콜백 함수 등록 ──&lt;br /&gt;
$g_hStub_KeyProc = DllCallbackRegister(&amp;quot;_KEYBOARD_HOOK_PROC&amp;quot;, &amp;quot;long&amp;quot;, &amp;quot;int;wparam;lparam&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
; ── 훅 설치 ──&lt;br /&gt;
$g_hHook = _WinAPI_SetWindowsHookEx($WH_KEYBOARD_LL, _&lt;br /&gt;
DllCallbackGetPtr($g_hStub_KeyProc), _&lt;br /&gt;
_WinAPI_GetModuleHandle(0))&lt;br /&gt;
&lt;br /&gt;
If $g_hHook = 0 Then&lt;br /&gt;
debugprint(&amp;quot;키보드 훅 설치 실패&amp;quot;)&lt;br /&gt;
Exit&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
debugprint(&amp;quot;키보드 훅 설치 완료 - ESC로 종료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
; ── 메인 루프 (메시지 펌프 필수) ──&lt;br /&gt;
While 1&lt;br /&gt;
Sleep(10)&lt;br /&gt;
WEnd&lt;br /&gt;
&lt;br /&gt;
; ── 종료 처리 ──&lt;br /&gt;
Func _EXIT_HOOK()&lt;br /&gt;
_WinAPI_UnhookWindowsHookEx($g_hHook)&lt;br /&gt;
DllCallbackFree($g_hStub_KeyProc)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── 키보드 훅 콜백 ──&lt;br /&gt;
Func _KEYBOARD_HOOK_PROC($nCode, $wParam, $lParam)&lt;br /&gt;
If $nCode &amp;lt; 0 Then Return _WinAPI_CallNextHookEx($g_hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
&lt;br /&gt;
; KBDLLHOOKSTRUCT 구조체에서 정보 추출&lt;br /&gt;
Local $tKBDLL = DllStructCreate( _&lt;br /&gt;
&amp;quot;dword vkCode;&amp;quot; &amp;amp; _    ; 가상키 코드&lt;br /&gt;
&amp;quot;dword scanCode;&amp;quot; &amp;amp; _  ; 스캔코드&lt;br /&gt;
&amp;quot;dword flags;&amp;quot; &amp;amp; _     ; 확장키, injected 등 플래그&lt;br /&gt;
&amp;quot;dword time;&amp;quot; &amp;amp; _      ; 타임스탬프&lt;br /&gt;
&amp;quot;ulong_ptr dwExtraInfo&amp;quot;, _ ; 추가 정보&lt;br /&gt;
$lParam)&lt;br /&gt;
&lt;br /&gt;
Local $iVKCode   = DllStructGetData($tKBDLL, &amp;quot;vkCode&amp;quot;)&lt;br /&gt;
Local $iScanCode = DllStructGetData($tKBDLL, &amp;quot;scanCode&amp;quot;)&lt;br /&gt;
Local $iFlags    = DllStructGetData($tKBDLL, &amp;quot;flags&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
; 키 상태 판별&lt;br /&gt;
Local $sState = &amp;quot;&amp;quot;&lt;br /&gt;
Switch $wParam&lt;br /&gt;
Case $WM_KEYDOWN, $WM_SYSKEYDOWN&lt;br /&gt;
$sState = &amp;quot;DOWN&amp;quot;&lt;br /&gt;
Case $WM_KEYUP, $WM_SYSKEYUP&lt;br /&gt;
$sState = &amp;quot;UP&amp;quot;&lt;br /&gt;
EndSwitch&lt;br /&gt;
&lt;br /&gt;
; 확장키 여부 (Right Ctrl, Right Alt, Insert, Delete, 방향키 등)&lt;br /&gt;
Local $bExtended = BitAND($iFlags, 1)&lt;br /&gt;
&lt;br /&gt;
debugprint(&amp;quot;[&amp;quot; &amp;amp; $sState &amp;amp; &amp;quot;] VK=0x&amp;quot; &amp;amp; Hex($iVKCode, 2) &amp;amp; _&lt;br /&gt;
&amp;quot; Scan=0x&amp;quot; &amp;amp; Hex($iScanCode, 2) &amp;amp; _&lt;br /&gt;
&amp;quot; Ext=&amp;quot; &amp;amp; $bExtended &amp;amp; _&lt;br /&gt;
&amp;quot; Key=&amp;quot; &amp;amp; _VK_TO_NAME($iVKCode))&lt;br /&gt;
&lt;br /&gt;
; ESC 키로 종료&lt;br /&gt;
If $iVKCode = 0x1B And $sState = &amp;quot;DOWN&amp;quot; Then&lt;br /&gt;
_EXIT_HOOK()&lt;br /&gt;
Exit&lt;br /&gt;
EndIf&lt;br /&gt;
&lt;br /&gt;
; ── 키 가로채기 예시 ──&lt;br /&gt;
; 특정 키를 먹고 싶으면 여기서 Return 1 (다음 훅으로 전달 안 함)&lt;br /&gt;
; If $iVKCode = 0x41 Then Return 1  ; A키 차단&lt;br /&gt;
&lt;br /&gt;
; 정상 전달&lt;br /&gt;
Return _WinAPI_CallNextHookEx($g_hHook, $nCode, $wParam, $lParam)&lt;br /&gt;
EndFunc&lt;br /&gt;
&lt;br /&gt;
; ── VK코드 → 키 이름 변환 (주요 키) ──&lt;br /&gt;
Func _VK_TO_NAME($iVK)&lt;br /&gt;
Switch $iVK&lt;br /&gt;
Case 0x08&lt;br /&gt;
Return &amp;quot;Backspace&amp;quot;&lt;br /&gt;
Case 0x09&lt;br /&gt;
Return &amp;quot;Tab&amp;quot;&lt;br /&gt;
Case 0x0D&lt;br /&gt;
Return &amp;quot;Enter&amp;quot;&lt;br /&gt;
Case 0x10&lt;br /&gt;
Return &amp;quot;Shift&amp;quot;&lt;br /&gt;
Case 0x11&lt;br /&gt;
Return &amp;quot;Ctrl&amp;quot;&lt;br /&gt;
Case 0x12&lt;br /&gt;
Return &amp;quot;Alt&amp;quot;&lt;br /&gt;
Case 0x14&lt;br /&gt;
Return &amp;quot;CapsLock&amp;quot;&lt;br /&gt;
Case 0x1B&lt;br /&gt;
Return &amp;quot;ESC&amp;quot;&lt;br /&gt;
Case 0x20&lt;br /&gt;
Return &amp;quot;Space&amp;quot;&lt;br /&gt;
Case 0x25&lt;br /&gt;
Return &amp;quot;Left&amp;quot;&lt;br /&gt;
Case 0x26&lt;br /&gt;
Return &amp;quot;Up&amp;quot;&lt;br /&gt;
Case 0x27&lt;br /&gt;
Return &amp;quot;Right&amp;quot;&lt;br /&gt;
Case 0x28&lt;br /&gt;
Return &amp;quot;Down&amp;quot;&lt;br /&gt;
Case 0x2E&lt;br /&gt;
Return &amp;quot;Delete&amp;quot;&lt;br /&gt;
Case 0x5B&lt;br /&gt;
Return &amp;quot;LWin&amp;quot;&lt;br /&gt;
Case 0x5C&lt;br /&gt;
Return &amp;quot;RWin&amp;quot;&lt;br /&gt;
Case 0xA0&lt;br /&gt;
Return &amp;quot;LShift&amp;quot;&lt;br /&gt;
Case 0xA1&lt;br /&gt;
Return &amp;quot;RShift&amp;quot;&lt;br /&gt;
Case 0xA2&lt;br /&gt;
Return &amp;quot;LCtrl&amp;quot;&lt;br /&gt;
Case 0xA3&lt;br /&gt;
Return &amp;quot;RCtrl&amp;quot;&lt;br /&gt;
Case 0xA4&lt;br /&gt;
Return &amp;quot;LAlt&amp;quot;&lt;br /&gt;
Case 0xA5&lt;br /&gt;
Return &amp;quot;RAlt&amp;quot;&lt;br /&gt;
Case 0x30 To 0x39&lt;br /&gt;
Return Chr($iVK)  ; 0~9&lt;br /&gt;
Case 0x41 To 0x5A&lt;br /&gt;
Return Chr($iVK)  ; A~Z&lt;br /&gt;
Case 0x70 To 0x87&lt;br /&gt;
Return &amp;quot;F&amp;quot; &amp;amp; ($iVK - 0x6F)  ; F1~F24&lt;br /&gt;
Case Else&lt;br /&gt;
Return &amp;quot;0x&amp;quot; &amp;amp; Hex($iVK, 2)&lt;br /&gt;
EndSwitch&lt;br /&gt;
EndFunc&lt;br /&gt;
주요 포인트를 정리하면 다음과 같습니다.&lt;br /&gt;
&lt;br /&gt;
동작 원리 — $WH_KEYBOARD_LL(13)은 저수준 키보드 훅으로, 시스템 전체의 키 입력을 가로챕니다. 포커스가 어느 프로그램에 있든 상관없이 모든 키 입력이 콜백으로 들어옵니다.&lt;br /&gt;
&lt;br /&gt;
KBDLLHOOKSTRUCT — $lParam이 가리키는 구조체에서 vkCode(가상키), scanCode(스캔코드), flags(확장키·주입 여부)를 추출합니다. flags의 bit 0이 1이면 확장키(Right Ctrl, Right Alt 등)입니다.&lt;br /&gt;
&lt;br /&gt;
키 차단 — 콜백에서 Return 1을 하면 해당 키 입력이 다른 프로그램에 전달되지 않습니다. 정상 전달하려면 반드시 _WinAPI_CallNextHookEx를 호출해야 합니다.&lt;br /&gt;
&lt;br /&gt;
메시지 펌프 필수 — 메인 루프에서 Sleep(10) 등으로 메시지 처리가 돌아야 훅 콜백이 정상 호출됩니다. 메인 루프가 멈추면 시스템 전체 키 입력이 지연될 수 있으므로, 콜백 안에서 무거운 처리는 피해야 합니다.&lt;br /&gt;
&lt;br /&gt;
종료 시 — _WinAPI_UnhookWindowsHookEx로 훅을 해제하고 DllCallbackFree로 콜백을 반드시 해제해야 메모리 누수가 없습니다.&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
[[category:autoit]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2397</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2397"/>
		<updated>2026-05-06T15:21:42Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 소스 코드 stock_monitor.py&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13&amp;quot; e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
  Page 0 - 요약 (양 종목)&lt;br /&gt;
  Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
  Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(&#039;~/e-Paper/RaspberryPi_JetsonNano/python/lib&#039;)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
    sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 설정&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
STOCKS = [&lt;br /&gt;
    {&#039;code&#039;: &#039;005930&#039;, &#039;name&#039;: &#039;삼성전자&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005380&#039;, &#039;name&#039;: &#039;현대차&#039;},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = &#039;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#039;&lt;br /&gt;
FONT_BOLD    = &#039;/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf&#039;&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
    level=logging.INFO,&lt;br /&gt;
    format=&#039;%(asctime)s [%(levelname)s] %(message)s&#039;&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(__name__)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;현재가 (네이버 모바일 API)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    url = f&#039;https://m.stock.naver.com/api/stock/{code}/basic&#039;&lt;br /&gt;
    headers = {&lt;br /&gt;
        &#039;User-Agent&#039;: &#039;Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36&#039;,&lt;br /&gt;
        &#039;Referer&#039;: &#039;https://m.stock.naver.com/&#039;,&lt;br /&gt;
    }&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        d = r.json()&lt;br /&gt;
&lt;br /&gt;
        def to_int(s):&lt;br /&gt;
            return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
        change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
        sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
        if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
            change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
            &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;change&#039;      : change,&lt;br /&gt;
            &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;sign&#039;        : sign,&lt;br /&gt;
            &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        }&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    주봉 종가 리스트 [(YYYYMMDD, close), ...]&lt;br /&gt;
    네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    end = datetime.now()&lt;br /&gt;
    # 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
    start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
    url = (f&#039;https://api.finance.naver.com/siseJson.naver&#039;&lt;br /&gt;
           f&#039;?symbol={code}&amp;amp;requestType=1&#039;&lt;br /&gt;
           f&#039;&amp;amp;startTime={start.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;endTime={end.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;timeframe=week&#039;)&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers={&#039;User-Agent&#039;: &#039;Mozilla/5.0&#039;}, timeout=8)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
        # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
        rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
        data = []&lt;br /&gt;
        for row in rows:&lt;br /&gt;
            parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
            if len(parts) &amp;lt; 5:&lt;br /&gt;
                continue&lt;br /&gt;
            date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
            if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
                continue&lt;br /&gt;
            try:&lt;br /&gt;
                close = float(parts[4])&lt;br /&gt;
                data.append((date_str, close))&lt;br /&gt;
            except (ValueError, IndexError):&lt;br /&gt;
                continue&lt;br /&gt;
&lt;br /&gt;
        return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 유틸&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
    return f&amp;quot;{p:,}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 y축 라벨용 포맷&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if v &amp;gt;= 10000:&lt;br /&gt;
        return f&#039;{v/10000:.1f}만&#039;&lt;br /&gt;
    if v &amp;gt;= 1000:&lt;br /&gt;
        return f&#039;{v/1000:.1f}k&#039;&lt;br /&gt;
    return f&#039;{int(v)}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
    if sign in (&#039;1&#039;, &#039;2&#039;):&lt;br /&gt;
        return &#039;▲&#039;&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        return &#039;▼&#039;&lt;br /&gt;
    return &#039;-&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
    now = datetime.now()&lt;br /&gt;
    if now.weekday() &amp;gt;= 5:&lt;br /&gt;
        return False&lt;br /&gt;
    return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;가로 모드 빈 캔버스 (250x122)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    width, height = epd.height, epd.width&lt;br /&gt;
    image = Image.new(&#039;1&#039;, (width, height), 255)&lt;br /&gt;
    return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;요약 페이지: 양 종목을 위/아래 분할 표시&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
    draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
    now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
    draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
    draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
    y = 15&lt;br /&gt;
&lt;br /&gt;
    for i, d in enumerate(stocks_data):&lt;br /&gt;
        if d is None:&lt;br /&gt;
            draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
            y += section_h&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
        price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
        ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
        draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
        y += section_h&lt;br /&gt;
        if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
            draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 페이지: 단일 종목 + 주봉 라인 차트&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    if stock is None:&lt;br /&gt;
        draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    # 헤더: 종목명 + 현재가&lt;br /&gt;
    draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 등락 + 페이지 라벨&lt;br /&gt;
    sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # ── 차트 영역 ──&lt;br /&gt;
    cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
    cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
    if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
        draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    closes = [c[1] for c in chart_data]&lt;br /&gt;
    pmin, pmax = min(closes), max(closes)&lt;br /&gt;
    if pmax == pmin:&lt;br /&gt;
        pmax = pmin + 1&lt;br /&gt;
    span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
    # 축&lt;br /&gt;
    draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
    draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 가로 그리드 (중간선)&lt;br /&gt;
    mid_y = cy + ch // 2&lt;br /&gt;
    for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
        draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 라인 그리기&lt;br /&gt;
    n = len(closes)&lt;br /&gt;
    pts = []&lt;br /&gt;
    for i, c in enumerate(closes):&lt;br /&gt;
        px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
        py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
        pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
    for i in range(len(pts) - 1):&lt;br /&gt;
        draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 현재가(마지막 봉) 강조 표시&lt;br /&gt;
    last_x, last_y = pts[-1]&lt;br /&gt;
    draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
    # Y축 라벨 (오른쪽)&lt;br /&gt;
    label_x = cx + cw + 3&lt;br /&gt;
    draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
    if chart_data:&lt;br /&gt;
        d_start = chart_data[0][0]&lt;br /&gt;
        d_end   = chart_data[-1][0]&lt;br /&gt;
        # YYYYMMDD → MM/DD&lt;br /&gt;
        s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
        e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
        draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
        bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
        draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 메인 루프&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
    global running&lt;br /&gt;
    log.info(f&amp;quot;신호 수신({signum}) - 종료 처리&amp;quot;)&lt;br /&gt;
    running = False&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;전체/부분 갱신을 자동 선택해 화면 출력&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if state[&#039;first&#039;] or state[&#039;partial_count&#039;] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.display(epd.getbuffer(image))&lt;br /&gt;
        state[&#039;partial_count&#039;] = 0&lt;br /&gt;
        state[&#039;first&#039;] = False&lt;br /&gt;
        log.info(&amp;quot;전체 갱신&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
            state[&#039;partial_count&#039;] += 1&lt;br /&gt;
            log.info(f&amp;quot;부분 갱신 ({state[&#039;partial_count&#039;]}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
        except AttributeError:&lt;br /&gt;
            epd.display(epd.getbuffer(image))&lt;br /&gt;
            log.info(&amp;quot;전체 갱신 (부분 갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
    signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
    log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
    epd = epd_module.EPD()&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
    stocks_data = [None] * len(STOCKS)&lt;br /&gt;
    chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
    state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
    total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
    current_page = 0&lt;br /&gt;
&lt;br /&gt;
    last_data_fetch = 0&lt;br /&gt;
    last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
    try:&lt;br /&gt;
        while running:&lt;br /&gt;
            now = time.time()&lt;br /&gt;
            need_redraw = False&lt;br /&gt;
&lt;br /&gt;
            # ── 시세 갱신 ──&lt;br /&gt;
            interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
            if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
                for i, s in enumerate(STOCKS):&lt;br /&gt;
                    d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                    stocks_data[i] = d&lt;br /&gt;
                    if d:&lt;br /&gt;
                        log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                last_data_fetch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 주봉 캐시 갱신 ──&lt;br /&gt;
            for s in STOCKS:&lt;br /&gt;
                code = s[&#039;code&#039;]&lt;br /&gt;
                cached = chart_cache.get(code)&lt;br /&gt;
                if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                    log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                    chart = fetch_weekly_chart(code)&lt;br /&gt;
                    chart_cache[code] = (chart, now)&lt;br /&gt;
                    need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 페이지 전환 ──&lt;br /&gt;
            if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
                current_page = (current_page + 1) % total_pages&lt;br /&gt;
                last_page_switch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
                log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            # ── 그리기 ──&lt;br /&gt;
            if need_redraw:&lt;br /&gt;
                if current_page == 0:&lt;br /&gt;
                    image = build_summary_image(epd, stocks_data)&lt;br /&gt;
                else:&lt;br /&gt;
                    idx = current_page - 1&lt;br /&gt;
                    code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                    chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                    image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
                render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
            # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
            for _ in range(1):&lt;br /&gt;
                if not running:&lt;br /&gt;
                    break&lt;br /&gt;
                time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
    finally:&lt;br /&gt;
        log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
        try:&lt;br /&gt;
            epd.init()&lt;br /&gt;
            epd.Clear(0xFF)&lt;br /&gt;
            epd.sleep()&lt;br /&gt;
        except Exception:&lt;br /&gt;
            pass&lt;br /&gt;
        log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
3-2. 요약페이지 3행1열 - 2개 페이지 , 주봉차트 6개&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
6종목 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13&amp;quot; e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 구성 (총 8페이지):&lt;br /&gt;
  Page 0   - 요약 1/2 (종목 1~3)&lt;br /&gt;
  Page 1   - 요약 2/2 (종목 4~6)&lt;br /&gt;
  Page 2~7 - 각 종목 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(&#039;~/e-Paper/RaspberryPi_JetsonNano/python/lib&#039;)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
    sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 종목 설정 (6개)&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
STOCKS = [&lt;br /&gt;
    {&#039;code&#039;: &#039;005930&#039;, &#039;name&#039;: &#039;삼성전자&#039;,     &#039;short&#039;: &#039;삼성전자&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;000660&#039;, &#039;name&#039;: &#039;SK하이닉스&#039;,   &#039;short&#039;: &#039;SK하이닉스&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005380&#039;, &#039;name&#039;: &#039;현대차&#039;,       &#039;short&#039;: &#039;현대차&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;035420&#039;, &#039;name&#039;: &#039;NAVER&#039;,        &#039;short&#039;: &#039;NAVER&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005490&#039;, &#039;name&#039;: &#039;POSCO홀딩스&#039;,  &#039;short&#039;: &#039;POSCO홀딩스&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;373220&#039;, &#039;name&#039;: &#039;LG에너지솔루션&#039;,&#039;short&#039;: &#039;LG엔솔&#039;},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26&lt;br /&gt;
DATA_REFRESH_OPEN  = 60&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60&lt;br /&gt;
PAGE_DURATION      = 12           # 페이지당 머무는 시간(초)&lt;br /&gt;
FULL_REFRESH_EVERY = 10           # 부분갱신 N회 후 전체갱신 (잔상 방지)&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = &#039;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#039;&lt;br /&gt;
FONT_BOLD    = &#039;/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf&#039;&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
# 요약 페이지 1장당 표시할 종목 수&lt;br /&gt;
STOCKS_PER_SUMMARY = 3&lt;br /&gt;
SUMMARY_PAGES = (len(STOCKS) + STOCKS_PER_SUMMARY - 1) // STOCKS_PER_SUMMARY&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
    level=logging.INFO,&lt;br /&gt;
    format=&#039;%(asctime)s [%(levelname)s] %(message)s&#039;&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(__name__)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
    url = f&#039;https://m.stock.naver.com/api/stock/{code}/basic&#039;&lt;br /&gt;
    headers = {&lt;br /&gt;
        &#039;User-Agent&#039;: &#039;Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36&#039;,&lt;br /&gt;
        &#039;Referer&#039;: &#039;https://m.stock.naver.com/&#039;,&lt;br /&gt;
    }&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        d = r.json()&lt;br /&gt;
&lt;br /&gt;
        def to_int(s):&lt;br /&gt;
            return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
        change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
        sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
        if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
            change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
            &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;change&#039;      : change,&lt;br /&gt;
            &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;sign&#039;        : sign,&lt;br /&gt;
            &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        }&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
    end = datetime.now()&lt;br /&gt;
    start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
    url = (f&#039;https://api.finance.naver.com/siseJson.naver&#039;&lt;br /&gt;
           f&#039;?symbol={code}&amp;amp;requestType=1&#039;&lt;br /&gt;
           f&#039;&amp;amp;startTime={start.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;endTime={end.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;timeframe=week&#039;)&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers={&#039;User-Agent&#039;: &#039;Mozilla/5.0&#039;}, timeout=8)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
        rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
        data = []&lt;br /&gt;
        for row in rows:&lt;br /&gt;
            parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
            if len(parts) &amp;lt; 5:&lt;br /&gt;
                continue&lt;br /&gt;
            date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
            if not date_str.isdigit():&lt;br /&gt;
                continue&lt;br /&gt;
            try:&lt;br /&gt;
                close = float(parts[4])&lt;br /&gt;
                data.append((date_str, close))&lt;br /&gt;
            except (ValueError, IndexError):&lt;br /&gt;
                continue&lt;br /&gt;
&lt;br /&gt;
        return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 유틸&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
    return f&amp;quot;{p:,}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
    if v &amp;gt;= 10000:&lt;br /&gt;
        return f&#039;{v/10000:.1f}만&#039;&lt;br /&gt;
    if v &amp;gt;= 1000:&lt;br /&gt;
        return f&#039;{v/1000:.1f}k&#039;&lt;br /&gt;
    return f&#039;{int(v)}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
    if sign in (&#039;1&#039;, &#039;2&#039;):&lt;br /&gt;
        return &#039;▲&#039;&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        return &#039;▼&#039;&lt;br /&gt;
    return &#039;-&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
    now = datetime.now()&lt;br /&gt;
    if now.weekday() &amp;gt;= 5:&lt;br /&gt;
        return False&lt;br /&gt;
    return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
    width, height = epd.height, epd.width   # 250 x 122&lt;br /&gt;
    image = Image.new(&#039;1&#039;, (width, height), 255)&lt;br /&gt;
    return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data, page_idx):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    요약 페이지: 3행 1열&lt;br /&gt;
    page_idx: 0 = 종목 1~3, 1 = 종목 4~6&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_time  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    18)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    10)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    # ── 헤더 (12px) ──&lt;br /&gt;
    market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
    hdr_text = f&#039;KR주식[{market}] {page_idx+1}/{SUMMARY_PAGES}&#039;&lt;br /&gt;
    draw.text((2, 0), hdr_text, font=f_hdr, fill=0)&lt;br /&gt;
&lt;br /&gt;
    now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
    draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
    draw.line([(0, 12), (width, 12)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # ── 종목 영역: 110px / 3행 ≒ 36px/행 ──&lt;br /&gt;
    grid_top = 13&lt;br /&gt;
    row_h = (height - grid_top) // STOCKS_PER_SUMMARY    # 36&lt;br /&gt;
    start = page_idx * STOCKS_PER_SUMMARY&lt;br /&gt;
    end = min(start + STOCKS_PER_SUMMARY, len(STOCKS))&lt;br /&gt;
&lt;br /&gt;
    for i in range(STOCKS_PER_SUMMARY):&lt;br /&gt;
        y = grid_top + i * row_h&lt;br /&gt;
        idx = start + i&lt;br /&gt;
&lt;br /&gt;
        # 행 구분선&lt;br /&gt;
        if i &amp;gt; 0:&lt;br /&gt;
            draw.line([(0, y), (width, y)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
        if idx &amp;gt;= end:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        meta = STOCKS[idx]&lt;br /&gt;
        d = stocks_data[idx]&lt;br /&gt;
&lt;br /&gt;
        if d is None:&lt;br /&gt;
            draw.text((3, y + 2), meta[&#039;short&#039;], font=f_name, fill=0)&lt;br /&gt;
            draw.text((3, y + 18), &#039;데이터 조회 실패&#039;, font=f_chg, fill=0)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        # 1행: 종목명(좌) + 현재가(우, 크게)&lt;br /&gt;
        draw.text((3, y + 1), meta[&#039;short&#039;], font=f_name, fill=0)&lt;br /&gt;
&lt;br /&gt;
        price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1),&lt;br /&gt;
                  price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        # 2행: ▲/▼ 등락 + 등락률 (좌) + 시/고/저 (우, 작게)&lt;br /&gt;
        sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, y + 21), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
        ohl = f&amp;quot;고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
        bbox = draw.textbbox((0, 0), ohl, font=f_tiny)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 23),&lt;br /&gt;
                  ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock_meta, stock_data, chart_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 페이지: 단일 종목 + 주봉 라인 차트&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    # 헤더&lt;br /&gt;
    draw.text((3, 0), stock_meta[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
&lt;br /&gt;
    if stock_data is None:&lt;br /&gt;
        draw.text((3, 22), &#039;시세 조회 실패&#039;, font=f_chg, fill=0)&lt;br /&gt;
    else:&lt;br /&gt;
        price_str = fmt_price(stock_data[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, 0),&lt;br /&gt;
                  price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        sym = change_symbol(stock_data[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(stock_data[&#039;change&#039;]):,} ({stock_data[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
    draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 차트 영역&lt;br /&gt;
    cx, cy = 3, 36&lt;br /&gt;
    cw, ch = 200, 82&lt;br /&gt;
&lt;br /&gt;
    if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
        draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    closes = [c[1] for c in chart_data]&lt;br /&gt;
    pmin, pmax = min(closes), max(closes)&lt;br /&gt;
    if pmax == pmin:&lt;br /&gt;
        pmax = pmin + 1&lt;br /&gt;
    span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
    draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
    draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    mid_y = cy + ch // 2&lt;br /&gt;
    for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
        draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
    n = len(closes)&lt;br /&gt;
    pts = []&lt;br /&gt;
    for i, c in enumerate(closes):&lt;br /&gt;
        px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
        py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
        pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
    for i in range(len(pts) - 1):&lt;br /&gt;
        draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    last_x, last_y = pts[-1]&lt;br /&gt;
    draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
    label_x = cx + cw + 3&lt;br /&gt;
    draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    if chart_data:&lt;br /&gt;
        d_start = chart_data[0][0]&lt;br /&gt;
        d_end   = chart_data[-1][0]&lt;br /&gt;
        s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
        e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
        draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
        bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
        draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1),&lt;br /&gt;
                  e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 페이지 라우팅&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def total_pages():&lt;br /&gt;
    return SUMMARY_PAGES + len(STOCKS)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def page_label(page):&lt;br /&gt;
    if page &amp;lt; SUMMARY_PAGES:&lt;br /&gt;
        return f&#039;요약 {page+1}/{SUMMARY_PAGES}&#039;&lt;br /&gt;
    return STOCKS[page - SUMMARY_PAGES][&#039;short&#039;]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def render_page(epd, page, stocks_data, chart_cache):&lt;br /&gt;
    if page &amp;lt; SUMMARY_PAGES:&lt;br /&gt;
        return build_summary_image(epd, stocks_data, page)&lt;br /&gt;
    idx = page - SUMMARY_PAGES&lt;br /&gt;
    code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
    chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
    return build_chart_image(epd, STOCKS[idx], stocks_data[idx], chart)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# e-Paper 갱신 (잔상 방지 개선판)&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def display_full_clear(epd, image):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;페이지 전환용: 화면을 흰색으로 완전히 지운 뒤 새로 그림&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)              # 화면 전체를 흰색으로 강제 클리어 (잔상 제거)&lt;br /&gt;
    time.sleep(0.3)              # 클리어 안정화&lt;br /&gt;
    epd.display(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def display_full(epd, image):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;전체 갱신 (Clear 없이)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.display(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def display_partial(epd, image):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;부분 갱신 (깜빡임 없음, 잔상 누적됨)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 메인 루프&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
    global running&lt;br /&gt;
    log.info(f&amp;quot;신호 수신({signum}) - 종료 처리&amp;quot;)&lt;br /&gt;
    running = False&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
    signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
    log.info(f&amp;quot;주가 모니터 시작 - 종목 {len(STOCKS)}개, 총 {total_pages()}페이지&amp;quot;)&lt;br /&gt;
    epd = epd_module.EPD()&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
    stocks_data = [None] * len(STOCKS)&lt;br /&gt;
    chart_cache = {}&lt;br /&gt;
&lt;br /&gt;
    current_page = 0&lt;br /&gt;
    prev_page    = -1               # 페이지 전환 감지용&lt;br /&gt;
    partial_count = 0&lt;br /&gt;
&lt;br /&gt;
    last_data_fetch  = 0&lt;br /&gt;
    last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
    try:&lt;br /&gt;
        while running:&lt;br /&gt;
            now = time.time()&lt;br /&gt;
            need_redraw = False&lt;br /&gt;
&lt;br /&gt;
            # ── 시세 갱신 ──&lt;br /&gt;
            interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
            if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
                for i, s in enumerate(STOCKS):&lt;br /&gt;
                    d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                    stocks_data[i] = d&lt;br /&gt;
                    if d:&lt;br /&gt;
                        log.info(f&amp;quot;{s[&#039;short&#039;]:10s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                    time.sleep(0.2)&lt;br /&gt;
                last_data_fetch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 주봉 캐시 갱신 ──&lt;br /&gt;
            for s in STOCKS:&lt;br /&gt;
                code = s[&#039;code&#039;]&lt;br /&gt;
                cached = chart_cache.get(code)&lt;br /&gt;
                if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                    log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                    chart = fetch_weekly_chart(code)&lt;br /&gt;
                    chart_cache[code] = (chart, now)&lt;br /&gt;
                    time.sleep(0.2)&lt;br /&gt;
&lt;br /&gt;
            # ── 페이지 자동 전환 ──&lt;br /&gt;
            if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
                current_page = (current_page + 1) % total_pages()&lt;br /&gt;
                last_page_switch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
                log.info(f&amp;quot;페이지 전환 → {current_page} ({page_label(current_page)})&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            # ── 그리기 ──&lt;br /&gt;
            if need_redraw:&lt;br /&gt;
                image = render_page(epd, current_page, stocks_data, chart_cache)&lt;br /&gt;
                page_changed = (current_page != prev_page)&lt;br /&gt;
&lt;br /&gt;
                if page_changed:&lt;br /&gt;
                    # 페이지 전환: Clear로 화면 완전히 지우고 새로 그림 (잔상 제거)&lt;br /&gt;
                    display_full_clear(epd, image)&lt;br /&gt;
                    partial_count = 0&lt;br /&gt;
                    log.info(f&amp;quot;갱신: FULL+CLEAR ({page_label(current_page)})&amp;quot;)&lt;br /&gt;
                elif partial_count &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
                    # 같은 페이지에서 부분갱신 누적 → 전체갱신&lt;br /&gt;
                    display_full(epd, image)&lt;br /&gt;
                    partial_count = 0&lt;br /&gt;
                    log.info(&amp;quot;갱신: FULL&amp;quot;)&lt;br /&gt;
                else:&lt;br /&gt;
                    # 같은 페이지 데이터만 변경 → 부분갱신&lt;br /&gt;
                    try:&lt;br /&gt;
                        display_partial(epd, image)&lt;br /&gt;
                        partial_count += 1&lt;br /&gt;
                        log.info(f&amp;quot;갱신: PARTIAL ({partial_count}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
                    except AttributeError:&lt;br /&gt;
                        display_full(epd, image)&lt;br /&gt;
                        log.info(&amp;quot;갱신: FULL (부분갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
                prev_page = current_page&lt;br /&gt;
&lt;br /&gt;
            time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
    finally:&lt;br /&gt;
        log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
        try:&lt;br /&gt;
            epd.init()&lt;br /&gt;
            epd.Clear(0xFF)&lt;br /&gt;
            epd.sleep()&lt;br /&gt;
        except Exception:&lt;br /&gt;
            pass&lt;br /&gt;
        log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2396</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2396"/>
		<updated>2026-05-06T15:13:11Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 소스 코드 stock_monitor.py&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13&amp;quot; e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
  Page 0 - 요약 (양 종목)&lt;br /&gt;
  Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
  Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(&#039;~/e-Paper/RaspberryPi_JetsonNano/python/lib&#039;)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
    sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 설정&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
STOCKS = [&lt;br /&gt;
    {&#039;code&#039;: &#039;005930&#039;, &#039;name&#039;: &#039;삼성전자&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005380&#039;, &#039;name&#039;: &#039;현대차&#039;},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = &#039;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#039;&lt;br /&gt;
FONT_BOLD    = &#039;/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf&#039;&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
    level=logging.INFO,&lt;br /&gt;
    format=&#039;%(asctime)s [%(levelname)s] %(message)s&#039;&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(__name__)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;현재가 (네이버 모바일 API)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    url = f&#039;https://m.stock.naver.com/api/stock/{code}/basic&#039;&lt;br /&gt;
    headers = {&lt;br /&gt;
        &#039;User-Agent&#039;: &#039;Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36&#039;,&lt;br /&gt;
        &#039;Referer&#039;: &#039;https://m.stock.naver.com/&#039;,&lt;br /&gt;
    }&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        d = r.json()&lt;br /&gt;
&lt;br /&gt;
        def to_int(s):&lt;br /&gt;
            return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
        change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
        sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
        if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
            change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
            &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;change&#039;      : change,&lt;br /&gt;
            &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;sign&#039;        : sign,&lt;br /&gt;
            &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        }&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    주봉 종가 리스트 [(YYYYMMDD, close), ...]&lt;br /&gt;
    네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    end = datetime.now()&lt;br /&gt;
    # 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
    start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
    url = (f&#039;https://api.finance.naver.com/siseJson.naver&#039;&lt;br /&gt;
           f&#039;?symbol={code}&amp;amp;requestType=1&#039;&lt;br /&gt;
           f&#039;&amp;amp;startTime={start.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;endTime={end.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;timeframe=week&#039;)&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers={&#039;User-Agent&#039;: &#039;Mozilla/5.0&#039;}, timeout=8)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
        # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
        rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
        data = []&lt;br /&gt;
        for row in rows:&lt;br /&gt;
            parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
            if len(parts) &amp;lt; 5:&lt;br /&gt;
                continue&lt;br /&gt;
            date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
            if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
                continue&lt;br /&gt;
            try:&lt;br /&gt;
                close = float(parts[4])&lt;br /&gt;
                data.append((date_str, close))&lt;br /&gt;
            except (ValueError, IndexError):&lt;br /&gt;
                continue&lt;br /&gt;
&lt;br /&gt;
        return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 유틸&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
    return f&amp;quot;{p:,}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 y축 라벨용 포맷&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if v &amp;gt;= 10000:&lt;br /&gt;
        return f&#039;{v/10000:.1f}만&#039;&lt;br /&gt;
    if v &amp;gt;= 1000:&lt;br /&gt;
        return f&#039;{v/1000:.1f}k&#039;&lt;br /&gt;
    return f&#039;{int(v)}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
    if sign in (&#039;1&#039;, &#039;2&#039;):&lt;br /&gt;
        return &#039;▲&#039;&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        return &#039;▼&#039;&lt;br /&gt;
    return &#039;-&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
    now = datetime.now()&lt;br /&gt;
    if now.weekday() &amp;gt;= 5:&lt;br /&gt;
        return False&lt;br /&gt;
    return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;가로 모드 빈 캔버스 (250x122)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    width, height = epd.height, epd.width&lt;br /&gt;
    image = Image.new(&#039;1&#039;, (width, height), 255)&lt;br /&gt;
    return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;요약 페이지: 양 종목을 위/아래 분할 표시&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
    draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
    now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
    draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
    draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
    y = 15&lt;br /&gt;
&lt;br /&gt;
    for i, d in enumerate(stocks_data):&lt;br /&gt;
        if d is None:&lt;br /&gt;
            draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
            y += section_h&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
        price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
        ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
        draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
        y += section_h&lt;br /&gt;
        if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
            draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 페이지: 단일 종목 + 주봉 라인 차트&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    if stock is None:&lt;br /&gt;
        draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    # 헤더: 종목명 + 현재가&lt;br /&gt;
    draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 등락 + 페이지 라벨&lt;br /&gt;
    sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # ── 차트 영역 ──&lt;br /&gt;
    cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
    cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
    if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
        draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    closes = [c[1] for c in chart_data]&lt;br /&gt;
    pmin, pmax = min(closes), max(closes)&lt;br /&gt;
    if pmax == pmin:&lt;br /&gt;
        pmax = pmin + 1&lt;br /&gt;
    span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
    # 축&lt;br /&gt;
    draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
    draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 가로 그리드 (중간선)&lt;br /&gt;
    mid_y = cy + ch // 2&lt;br /&gt;
    for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
        draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 라인 그리기&lt;br /&gt;
    n = len(closes)&lt;br /&gt;
    pts = []&lt;br /&gt;
    for i, c in enumerate(closes):&lt;br /&gt;
        px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
        py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
        pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
    for i in range(len(pts) - 1):&lt;br /&gt;
        draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 현재가(마지막 봉) 강조 표시&lt;br /&gt;
    last_x, last_y = pts[-1]&lt;br /&gt;
    draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
    # Y축 라벨 (오른쪽)&lt;br /&gt;
    label_x = cx + cw + 3&lt;br /&gt;
    draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
    if chart_data:&lt;br /&gt;
        d_start = chart_data[0][0]&lt;br /&gt;
        d_end   = chart_data[-1][0]&lt;br /&gt;
        # YYYYMMDD → MM/DD&lt;br /&gt;
        s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
        e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
        draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
        bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
        draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 메인 루프&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
    global running&lt;br /&gt;
    log.info(f&amp;quot;신호 수신({signum}) - 종료 처리&amp;quot;)&lt;br /&gt;
    running = False&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;전체/부분 갱신을 자동 선택해 화면 출력&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if state[&#039;first&#039;] or state[&#039;partial_count&#039;] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.display(epd.getbuffer(image))&lt;br /&gt;
        state[&#039;partial_count&#039;] = 0&lt;br /&gt;
        state[&#039;first&#039;] = False&lt;br /&gt;
        log.info(&amp;quot;전체 갱신&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
            state[&#039;partial_count&#039;] += 1&lt;br /&gt;
            log.info(f&amp;quot;부분 갱신 ({state[&#039;partial_count&#039;]}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
        except AttributeError:&lt;br /&gt;
            epd.display(epd.getbuffer(image))&lt;br /&gt;
            log.info(&amp;quot;전체 갱신 (부분 갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
    signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
    log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
    epd = epd_module.EPD()&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
    stocks_data = [None] * len(STOCKS)&lt;br /&gt;
    chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
    state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
    total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
    current_page = 0&lt;br /&gt;
&lt;br /&gt;
    last_data_fetch = 0&lt;br /&gt;
    last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
    try:&lt;br /&gt;
        while running:&lt;br /&gt;
            now = time.time()&lt;br /&gt;
            need_redraw = False&lt;br /&gt;
&lt;br /&gt;
            # ── 시세 갱신 ──&lt;br /&gt;
            interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
            if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
                for i, s in enumerate(STOCKS):&lt;br /&gt;
                    d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                    stocks_data[i] = d&lt;br /&gt;
                    if d:&lt;br /&gt;
                        log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                last_data_fetch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 주봉 캐시 갱신 ──&lt;br /&gt;
            for s in STOCKS:&lt;br /&gt;
                code = s[&#039;code&#039;]&lt;br /&gt;
                cached = chart_cache.get(code)&lt;br /&gt;
                if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                    log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                    chart = fetch_weekly_chart(code)&lt;br /&gt;
                    chart_cache[code] = (chart, now)&lt;br /&gt;
                    need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 페이지 전환 ──&lt;br /&gt;
            if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
                current_page = (current_page + 1) % total_pages&lt;br /&gt;
                last_page_switch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
                log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            # ── 그리기 ──&lt;br /&gt;
            if need_redraw:&lt;br /&gt;
                if current_page == 0:&lt;br /&gt;
                    image = build_summary_image(epd, stocks_data)&lt;br /&gt;
                else:&lt;br /&gt;
                    idx = current_page - 1&lt;br /&gt;
                    code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                    chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                    image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
                render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
            # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
            for _ in range(1):&lt;br /&gt;
                if not running:&lt;br /&gt;
                    break&lt;br /&gt;
                time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
    finally:&lt;br /&gt;
        log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
        try:&lt;br /&gt;
            epd.init()&lt;br /&gt;
            epd.Clear(0xFF)&lt;br /&gt;
            epd.sleep()&lt;br /&gt;
        except Exception:&lt;br /&gt;
            pass&lt;br /&gt;
        log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
3-2. 요약페이지 3행1열 - 2개 페이지 , 주봉차트 6개&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
6종목 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13” e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 구성 (총 8페이지):&lt;br /&gt;
Page 0   - 요약 1/2 (종목 1~3)&lt;br /&gt;
Page 1   - 요약 2/2 (종목 4~6)&lt;br /&gt;
Page 2~7 - 각 종목 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(’~/e-Paper/RaspberryPi_JetsonNano/python/lib’)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 종목 설정 (6개)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
STOCKS = [&lt;br /&gt;
{‘code’: ‘005930’, ‘name’: ‘삼성전자’,     ‘short’: ‘삼성전자’},&lt;br /&gt;
{‘code’: ‘000660’, ‘name’: ‘SK하이닉스’,   ‘short’: ‘SK하이닉스’},&lt;br /&gt;
{‘code’: ‘005380’, ‘name’: ‘현대차’,       ‘short’: ‘현대차’},&lt;br /&gt;
{‘code’: ‘035420’, ‘name’: ‘NAVER’,        ‘short’: ‘NAVER’},&lt;br /&gt;
{‘code’: ‘005490’, ‘name’: ‘POSCO홀딩스’,  ‘short’: ‘POSCO홀딩스’},&lt;br /&gt;
{‘code’: ‘373220’, ‘name’: ‘LG에너지솔루션’,‘short’: ‘LG엔솔’},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26&lt;br /&gt;
DATA_REFRESH_OPEN  = 60&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60&lt;br /&gt;
PAGE_DURATION      = 12           # 페이지당 머무는 시간(초)&lt;br /&gt;
FULL_REFRESH_EVERY = 10           # 부분갱신 N회 후 전체갱신 (잔상 방지)&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = ‘/usr/share/fonts/truetype/nanum/NanumGothic.ttf’&lt;br /&gt;
FONT_BOLD    = ‘/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf’&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
# 요약 페이지 1장당 표시할 종목 수&lt;br /&gt;
&lt;br /&gt;
STOCKS_PER_SUMMARY = 3&lt;br /&gt;
SUMMARY_PAGES = (len(STOCKS) + STOCKS_PER_SUMMARY - 1) // STOCKS_PER_SUMMARY&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
level=logging.INFO,&lt;br /&gt;
format=’%(asctime)s [%(levelname)s] %(message)s’&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(**name**)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
url = f’https://m.stock.naver.com/api/stock/{code}/basic’&lt;br /&gt;
headers = {&lt;br /&gt;
‘User-Agent’: ‘Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36’,&lt;br /&gt;
‘Referer’: ‘https://m.stock.naver.com/’,&lt;br /&gt;
}&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
d = r.json()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    def to_int(s):&lt;br /&gt;
        return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
    change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
    sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
    return {&lt;br /&gt;
        &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
        &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;change&#039;      : change,&lt;br /&gt;
        &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;sign&#039;        : sign,&lt;br /&gt;
        &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
    }&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return None&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
end = datetime.now()&lt;br /&gt;
start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
url = (f’https://api.finance.naver.com/siseJson.naver’&lt;br /&gt;
f’?symbol={code}&amp;amp;requestType=1’&lt;br /&gt;
f’&amp;amp;startTime={start.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;endTime={end.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;timeframe=week’)&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers={‘User-Agent’: ‘Mozilla/5.0’}, timeout=8)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
    data = []&lt;br /&gt;
    for row in rows:&lt;br /&gt;
        parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
        if len(parts) &amp;lt; 5:&lt;br /&gt;
            continue&lt;br /&gt;
        date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
        if not date_str.isdigit():&lt;br /&gt;
            continue&lt;br /&gt;
        try:&lt;br /&gt;
            close = float(parts[4])&lt;br /&gt;
            data.append((date_str, close))&lt;br /&gt;
        except (ValueError, IndexError):&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
    return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return []&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 유틸&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
return f”{p:,}”&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
if v &amp;gt;= 10000:&lt;br /&gt;
return f’{v/10000:.1f}만’&lt;br /&gt;
if v &amp;gt;= 1000:&lt;br /&gt;
return f’{v/1000:.1f}k’&lt;br /&gt;
return f’{int(v)}’&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
if sign in (‘1’, ‘2’):&lt;br /&gt;
return ‘▲’&lt;br /&gt;
if sign in (‘4’, ‘5’):&lt;br /&gt;
return ‘▼’&lt;br /&gt;
return ‘-’&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
now = datetime.now()&lt;br /&gt;
if now.weekday() &amp;gt;= 5:&lt;br /&gt;
return False&lt;br /&gt;
return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
width, height = epd.height, epd.width   # 250 x 122&lt;br /&gt;
image = Image.new(‘1’, (width, height), 255)&lt;br /&gt;
return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data, page_idx):&lt;br /&gt;
“””&lt;br /&gt;
요약 페이지: 3행 1열&lt;br /&gt;
page_idx: 0 = 종목 1~3, 1 = 종목 4~6&lt;br /&gt;
“””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_time  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    18)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    10)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
# ── 헤더 (12px) ──&lt;br /&gt;
market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
hdr_text = f&#039;KR주식[{market}] {page_idx+1}/{SUMMARY_PAGES}&#039;&lt;br /&gt;
draw.text((2, 0), hdr_text, font=f_hdr, fill=0)&lt;br /&gt;
&lt;br /&gt;
now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
draw.line([(0, 12), (width, 12)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# ── 종목 영역: 110px / 3행 ≒ 36px/행 ──&lt;br /&gt;
grid_top = 13&lt;br /&gt;
row_h = (height - grid_top) // STOCKS_PER_SUMMARY    # 36&lt;br /&gt;
start = page_idx * STOCKS_PER_SUMMARY&lt;br /&gt;
end = min(start + STOCKS_PER_SUMMARY, len(STOCKS))&lt;br /&gt;
&lt;br /&gt;
for i in range(STOCKS_PER_SUMMARY):&lt;br /&gt;
    y = grid_top + i * row_h&lt;br /&gt;
    idx = start + i&lt;br /&gt;
&lt;br /&gt;
    # 행 구분선&lt;br /&gt;
    if i &amp;gt; 0:&lt;br /&gt;
        draw.line([(0, y), (width, y)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    if idx &amp;gt;= end:&lt;br /&gt;
        continue&lt;br /&gt;
&lt;br /&gt;
    meta = STOCKS[idx]&lt;br /&gt;
    d = stocks_data[idx]&lt;br /&gt;
&lt;br /&gt;
    if d is None:&lt;br /&gt;
        draw.text((3, y + 2), meta[&#039;short&#039;], font=f_name, fill=0)&lt;br /&gt;
        draw.text((3, y + 18), &#039;데이터 조회 실패&#039;, font=f_chg, fill=0)&lt;br /&gt;
        continue&lt;br /&gt;
&lt;br /&gt;
    # 1행: 종목명(좌) + 현재가(우, 크게)&lt;br /&gt;
    draw.text((3, y + 1), meta[&#039;short&#039;], font=f_name, fill=0)&lt;br /&gt;
&lt;br /&gt;
    price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1),&lt;br /&gt;
              price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 2행: ▲/▼ 등락 + 등락률 (좌) + 시/고/저 (우, 작게)&lt;br /&gt;
    sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, y + 21), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    ohl = f&amp;quot;고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), ohl, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, y + 23),&lt;br /&gt;
              ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock_meta, stock_data, chart_data):&lt;br /&gt;
“”“차트 페이지: 단일 종목 + 주봉 라인 차트”””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
# 헤더&lt;br /&gt;
draw.text((3, 0), stock_meta[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
&lt;br /&gt;
if stock_data is None:&lt;br /&gt;
    draw.text((3, 22), &#039;시세 조회 실패&#039;, font=f_chg, fill=0)&lt;br /&gt;
else:&lt;br /&gt;
    price_str = fmt_price(stock_data[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 0),&lt;br /&gt;
              price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    sym = change_symbol(stock_data[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(stock_data[&#039;change&#039;]):,} ({stock_data[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# 차트 영역&lt;br /&gt;
cx, cy = 3, 36&lt;br /&gt;
cw, ch = 200, 82&lt;br /&gt;
&lt;br /&gt;
if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
    draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
closes = [c[1] for c in chart_data]&lt;br /&gt;
pmin, pmax = min(closes), max(closes)&lt;br /&gt;
if pmax == pmin:&lt;br /&gt;
    pmax = pmin + 1&lt;br /&gt;
span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
mid_y = cy + ch // 2&lt;br /&gt;
for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
    draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
n = len(closes)&lt;br /&gt;
pts = []&lt;br /&gt;
for i, c in enumerate(closes):&lt;br /&gt;
    px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
    py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
    pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
for i in range(len(pts) - 1):&lt;br /&gt;
    draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
last_x, last_y = pts[-1]&lt;br /&gt;
draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
label_x = cx + cw + 3&lt;br /&gt;
draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
if chart_data:&lt;br /&gt;
    d_start = chart_data[0][0]&lt;br /&gt;
    d_end   = chart_data[-1][0]&lt;br /&gt;
    s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
    e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
    draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
    draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1),&lt;br /&gt;
              e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 페이지 라우팅&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def total_pages():&lt;br /&gt;
return SUMMARY_PAGES + len(STOCKS)&lt;br /&gt;
&lt;br /&gt;
def page_label(page):&lt;br /&gt;
if page &amp;lt; SUMMARY_PAGES:&lt;br /&gt;
return f’요약 {page+1}/{SUMMARY_PAGES}’&lt;br /&gt;
return STOCKS[page - SUMMARY_PAGES][‘short’]&lt;br /&gt;
&lt;br /&gt;
def render_page(epd, page, stocks_data, chart_cache):&lt;br /&gt;
if page &amp;lt; SUMMARY_PAGES:&lt;br /&gt;
return build_summary_image(epd, stocks_data, page)&lt;br /&gt;
idx = page - SUMMARY_PAGES&lt;br /&gt;
code = STOCKS[idx][‘code’]&lt;br /&gt;
chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
return build_chart_image(epd, STOCKS[idx], stocks_data[idx], chart)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# e-Paper 갱신 (잔상 방지 개선판)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def display_full_clear(epd, image):&lt;br /&gt;
“”“페이지 전환용: 화면을 흰색으로 완전히 지운 뒤 새로 그림”””&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.Clear(0xFF)              # 화면 전체를 흰색으로 강제 클리어 (잔상 제거)&lt;br /&gt;
time.sleep(0.3)              # 클리어 안정화&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
def display_full(epd, image):&lt;br /&gt;
“”“전체 갱신 (Clear 없이)”””&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
def display_partial(epd, image):&lt;br /&gt;
“”“부분 갱신 (깜빡임 없음, 잔상 누적됨)”””&lt;br /&gt;
epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
global running&lt;br /&gt;
log.info(f”신호 수신({signum}) - 종료 처리”)&lt;br /&gt;
running = False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
log.info(f&amp;quot;주가 모니터 시작 - 종목 {len(STOCKS)}개, 총 {total_pages()}페이지&amp;quot;)&lt;br /&gt;
epd = epd_module.EPD()&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
stocks_data = [None] * len(STOCKS)&lt;br /&gt;
chart_cache = {}&lt;br /&gt;
&lt;br /&gt;
current_page = 0&lt;br /&gt;
prev_page    = -1               # 페이지 전환 감지용&lt;br /&gt;
partial_count = 0&lt;br /&gt;
&lt;br /&gt;
last_data_fetch  = 0&lt;br /&gt;
last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while running:&lt;br /&gt;
        now = time.time()&lt;br /&gt;
        need_redraw = False&lt;br /&gt;
&lt;br /&gt;
        # ── 시세 갱신 ──&lt;br /&gt;
        interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
        if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
            for i, s in enumerate(STOCKS):&lt;br /&gt;
                d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                stocks_data[i] = d&lt;br /&gt;
                if d:&lt;br /&gt;
                    log.info(f&amp;quot;{s[&#039;short&#039;]:10s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                time.sleep(0.2)&lt;br /&gt;
            last_data_fetch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
&lt;br /&gt;
        # ── 주봉 캐시 갱신 ──&lt;br /&gt;
        for s in STOCKS:&lt;br /&gt;
            code = s[&#039;code&#039;]&lt;br /&gt;
            cached = chart_cache.get(code)&lt;br /&gt;
            if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                chart = fetch_weekly_chart(code)&lt;br /&gt;
                chart_cache[code] = (chart, now)&lt;br /&gt;
                time.sleep(0.2)&lt;br /&gt;
&lt;br /&gt;
        # ── 페이지 자동 전환 ──&lt;br /&gt;
        if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
            current_page = (current_page + 1) % total_pages()&lt;br /&gt;
            last_page_switch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
            log.info(f&amp;quot;페이지 전환 → {current_page} ({page_label(current_page)})&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        # ── 그리기 ──&lt;br /&gt;
        if need_redraw:&lt;br /&gt;
            image = render_page(epd, current_page, stocks_data, chart_cache)&lt;br /&gt;
            page_changed = (current_page != prev_page)&lt;br /&gt;
&lt;br /&gt;
            if page_changed:&lt;br /&gt;
                # 페이지 전환: Clear로 화면 완전히 지우고 새로 그림 (잔상 제거)&lt;br /&gt;
                display_full_clear(epd, image)&lt;br /&gt;
                partial_count = 0&lt;br /&gt;
                log.info(f&amp;quot;갱신: FULL+CLEAR ({page_label(current_page)})&amp;quot;)&lt;br /&gt;
            elif partial_count &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
                # 같은 페이지에서 부분갱신 누적 → 전체갱신&lt;br /&gt;
                display_full(epd, image)&lt;br /&gt;
                partial_count = 0&lt;br /&gt;
                log.info(&amp;quot;갱신: FULL&amp;quot;)&lt;br /&gt;
            else:&lt;br /&gt;
                # 같은 페이지 데이터만 변경 → 부분갱신&lt;br /&gt;
                try:&lt;br /&gt;
                    display_partial(epd, image)&lt;br /&gt;
                    partial_count += 1&lt;br /&gt;
                    log.info(f&amp;quot;갱신: PARTIAL ({partial_count}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
                except AttributeError:&lt;br /&gt;
                    display_full(epd, image)&lt;br /&gt;
                    log.info(&amp;quot;갱신: FULL (부분갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            prev_page = current_page&lt;br /&gt;
&lt;br /&gt;
        time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
finally:&lt;br /&gt;
    log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
    try:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.Clear(0xFF)&lt;br /&gt;
        epd.sleep()&lt;br /&gt;
    except Exception:&lt;br /&gt;
        pass&lt;br /&gt;
    log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == ‘**main**’:&lt;br /&gt;
main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2395</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2395"/>
		<updated>2026-05-06T14:21:06Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 소스 코드 stock_monitor.py&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13&amp;quot; e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
  Page 0 - 요약 (양 종목)&lt;br /&gt;
  Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
  Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(&#039;~/e-Paper/RaspberryPi_JetsonNano/python/lib&#039;)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
    sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 설정&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
STOCKS = [&lt;br /&gt;
    {&#039;code&#039;: &#039;005930&#039;, &#039;name&#039;: &#039;삼성전자&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005380&#039;, &#039;name&#039;: &#039;현대차&#039;},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = &#039;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#039;&lt;br /&gt;
FONT_BOLD    = &#039;/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf&#039;&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
    level=logging.INFO,&lt;br /&gt;
    format=&#039;%(asctime)s [%(levelname)s] %(message)s&#039;&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(__name__)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;현재가 (네이버 모바일 API)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    url = f&#039;https://m.stock.naver.com/api/stock/{code}/basic&#039;&lt;br /&gt;
    headers = {&lt;br /&gt;
        &#039;User-Agent&#039;: &#039;Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36&#039;,&lt;br /&gt;
        &#039;Referer&#039;: &#039;https://m.stock.naver.com/&#039;,&lt;br /&gt;
    }&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        d = r.json()&lt;br /&gt;
&lt;br /&gt;
        def to_int(s):&lt;br /&gt;
            return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
        change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
        sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
        if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
            change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
            &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;change&#039;      : change,&lt;br /&gt;
            &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;sign&#039;        : sign,&lt;br /&gt;
            &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        }&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    주봉 종가 리스트 [(YYYYMMDD, close), ...]&lt;br /&gt;
    네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    end = datetime.now()&lt;br /&gt;
    # 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
    start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
    url = (f&#039;https://api.finance.naver.com/siseJson.naver&#039;&lt;br /&gt;
           f&#039;?symbol={code}&amp;amp;requestType=1&#039;&lt;br /&gt;
           f&#039;&amp;amp;startTime={start.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;endTime={end.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;timeframe=week&#039;)&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers={&#039;User-Agent&#039;: &#039;Mozilla/5.0&#039;}, timeout=8)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
        # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
        rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
        data = []&lt;br /&gt;
        for row in rows:&lt;br /&gt;
            parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
            if len(parts) &amp;lt; 5:&lt;br /&gt;
                continue&lt;br /&gt;
            date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
            if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
                continue&lt;br /&gt;
            try:&lt;br /&gt;
                close = float(parts[4])&lt;br /&gt;
                data.append((date_str, close))&lt;br /&gt;
            except (ValueError, IndexError):&lt;br /&gt;
                continue&lt;br /&gt;
&lt;br /&gt;
        return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 유틸&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
    return f&amp;quot;{p:,}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 y축 라벨용 포맷&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if v &amp;gt;= 10000:&lt;br /&gt;
        return f&#039;{v/10000:.1f}만&#039;&lt;br /&gt;
    if v &amp;gt;= 1000:&lt;br /&gt;
        return f&#039;{v/1000:.1f}k&#039;&lt;br /&gt;
    return f&#039;{int(v)}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
    if sign in (&#039;1&#039;, &#039;2&#039;):&lt;br /&gt;
        return &#039;▲&#039;&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        return &#039;▼&#039;&lt;br /&gt;
    return &#039;-&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
    now = datetime.now()&lt;br /&gt;
    if now.weekday() &amp;gt;= 5:&lt;br /&gt;
        return False&lt;br /&gt;
    return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;가로 모드 빈 캔버스 (250x122)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    width, height = epd.height, epd.width&lt;br /&gt;
    image = Image.new(&#039;1&#039;, (width, height), 255)&lt;br /&gt;
    return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;요약 페이지: 양 종목을 위/아래 분할 표시&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
    draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
    now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
    draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
    draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
    y = 15&lt;br /&gt;
&lt;br /&gt;
    for i, d in enumerate(stocks_data):&lt;br /&gt;
        if d is None:&lt;br /&gt;
            draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
            y += section_h&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
        price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
        ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
        draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
        y += section_h&lt;br /&gt;
        if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
            draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 페이지: 단일 종목 + 주봉 라인 차트&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    if stock is None:&lt;br /&gt;
        draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    # 헤더: 종목명 + 현재가&lt;br /&gt;
    draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 등락 + 페이지 라벨&lt;br /&gt;
    sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # ── 차트 영역 ──&lt;br /&gt;
    cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
    cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
    if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
        draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    closes = [c[1] for c in chart_data]&lt;br /&gt;
    pmin, pmax = min(closes), max(closes)&lt;br /&gt;
    if pmax == pmin:&lt;br /&gt;
        pmax = pmin + 1&lt;br /&gt;
    span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
    # 축&lt;br /&gt;
    draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
    draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 가로 그리드 (중간선)&lt;br /&gt;
    mid_y = cy + ch // 2&lt;br /&gt;
    for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
        draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 라인 그리기&lt;br /&gt;
    n = len(closes)&lt;br /&gt;
    pts = []&lt;br /&gt;
    for i, c in enumerate(closes):&lt;br /&gt;
        px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
        py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
        pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
    for i in range(len(pts) - 1):&lt;br /&gt;
        draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 현재가(마지막 봉) 강조 표시&lt;br /&gt;
    last_x, last_y = pts[-1]&lt;br /&gt;
    draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
    # Y축 라벨 (오른쪽)&lt;br /&gt;
    label_x = cx + cw + 3&lt;br /&gt;
    draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
    if chart_data:&lt;br /&gt;
        d_start = chart_data[0][0]&lt;br /&gt;
        d_end   = chart_data[-1][0]&lt;br /&gt;
        # YYYYMMDD → MM/DD&lt;br /&gt;
        s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
        e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
        draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
        bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
        draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 메인 루프&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
    global running&lt;br /&gt;
    log.info(f&amp;quot;신호 수신({signum}) - 종료 처리&amp;quot;)&lt;br /&gt;
    running = False&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;전체/부분 갱신을 자동 선택해 화면 출력&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if state[&#039;first&#039;] or state[&#039;partial_count&#039;] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.display(epd.getbuffer(image))&lt;br /&gt;
        state[&#039;partial_count&#039;] = 0&lt;br /&gt;
        state[&#039;first&#039;] = False&lt;br /&gt;
        log.info(&amp;quot;전체 갱신&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
            state[&#039;partial_count&#039;] += 1&lt;br /&gt;
            log.info(f&amp;quot;부분 갱신 ({state[&#039;partial_count&#039;]}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
        except AttributeError:&lt;br /&gt;
            epd.display(epd.getbuffer(image))&lt;br /&gt;
            log.info(&amp;quot;전체 갱신 (부분 갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
    signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
    log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
    epd = epd_module.EPD()&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
    stocks_data = [None] * len(STOCKS)&lt;br /&gt;
    chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
    state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
    total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
    current_page = 0&lt;br /&gt;
&lt;br /&gt;
    last_data_fetch = 0&lt;br /&gt;
    last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
    try:&lt;br /&gt;
        while running:&lt;br /&gt;
            now = time.time()&lt;br /&gt;
            need_redraw = False&lt;br /&gt;
&lt;br /&gt;
            # ── 시세 갱신 ──&lt;br /&gt;
            interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
            if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
                for i, s in enumerate(STOCKS):&lt;br /&gt;
                    d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                    stocks_data[i] = d&lt;br /&gt;
                    if d:&lt;br /&gt;
                        log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                last_data_fetch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 주봉 캐시 갱신 ──&lt;br /&gt;
            for s in STOCKS:&lt;br /&gt;
                code = s[&#039;code&#039;]&lt;br /&gt;
                cached = chart_cache.get(code)&lt;br /&gt;
                if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                    log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                    chart = fetch_weekly_chart(code)&lt;br /&gt;
                    chart_cache[code] = (chart, now)&lt;br /&gt;
                    need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 페이지 전환 ──&lt;br /&gt;
            if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
                current_page = (current_page + 1) % total_pages&lt;br /&gt;
                last_page_switch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
                log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            # ── 그리기 ──&lt;br /&gt;
            if need_redraw:&lt;br /&gt;
                if current_page == 0:&lt;br /&gt;
                    image = build_summary_image(epd, stocks_data)&lt;br /&gt;
                else:&lt;br /&gt;
                    idx = current_page - 1&lt;br /&gt;
                    code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                    chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                    image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
                render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
            # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
            for _ in range(1):&lt;br /&gt;
                if not running:&lt;br /&gt;
                    break&lt;br /&gt;
                time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
    finally:&lt;br /&gt;
        log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
        try:&lt;br /&gt;
            epd.init()&lt;br /&gt;
            epd.Clear(0xFF)&lt;br /&gt;
            epd.sleep()&lt;br /&gt;
        except Exception:&lt;br /&gt;
            pass&lt;br /&gt;
        log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2394</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2394"/>
		<updated>2026-05-06T14:17:29Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: Devcafe (토론)의 2391 판 편집을 되돌림&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 삼성전자 , 현대차 주가 표시 ===&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13” e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
Page 0 - 요약 (양 종목)&lt;br /&gt;
Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(’~/e-Paper/RaspberryPi_JetsonNano/python/lib’)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 설정&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
STOCKS = [&lt;br /&gt;
{‘code’: ‘005930’, ‘name’: ‘삼성전자’},&lt;br /&gt;
{‘code’: ‘005380’, ‘name’: ‘현대차’},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = ‘/usr/share/fonts/truetype/nanum/NanumGothic.ttf’&lt;br /&gt;
FONT_BOLD    = ‘/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf’&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
level=logging.INFO,&lt;br /&gt;
format=’%(asctime)s [%(levelname)s] %(message)s’&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(**name**)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
“”“현재가 (네이버 모바일 API)”””&lt;br /&gt;
url = f’https://m.stock.naver.com/api/stock/{code}/basic’&lt;br /&gt;
headers = {&lt;br /&gt;
‘User-Agent’: ‘Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36’,&lt;br /&gt;
‘Referer’: ‘https://m.stock.naver.com/’,&lt;br /&gt;
}&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
d = r.json()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    def to_int(s):&lt;br /&gt;
        return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
    change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
    sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
    return {&lt;br /&gt;
        &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
        &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;change&#039;      : change,&lt;br /&gt;
        &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;sign&#039;        : sign,&lt;br /&gt;
        &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
    }&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return None&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
“””&lt;br /&gt;
주봉 종가 리스트 [(YYYYMMDD, close), …]&lt;br /&gt;
네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
“””&lt;br /&gt;
end = datetime.now()&lt;br /&gt;
# 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
url = (f’https://api.finance.naver.com/siseJson.naver’&lt;br /&gt;
f’?symbol={code}&amp;amp;requestType=1’&lt;br /&gt;
f’&amp;amp;startTime={start.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;endTime={end.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;timeframe=week’)&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers={‘User-Agent’: ‘Mozilla/5.0’}, timeout=8)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
    rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
    data = []&lt;br /&gt;
    for row in rows:&lt;br /&gt;
        parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
        if len(parts) &amp;lt; 5:&lt;br /&gt;
            continue&lt;br /&gt;
        date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
        if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
            continue&lt;br /&gt;
        try:&lt;br /&gt;
            close = float(parts[4])&lt;br /&gt;
            data.append((date_str, close))&lt;br /&gt;
        except (ValueError, IndexError):&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
    return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return []&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 유틸&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
return f”{p:,}”&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
“”“차트 y축 라벨용 포맷”””&lt;br /&gt;
if v &amp;gt;= 10000:&lt;br /&gt;
return f’{v/10000:.1f}만’&lt;br /&gt;
if v &amp;gt;= 1000:&lt;br /&gt;
return f’{v/1000:.1f}k’&lt;br /&gt;
return f’{int(v)}’&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
if sign in (‘1’, ‘2’):&lt;br /&gt;
return ‘▲’&lt;br /&gt;
if sign in (‘4’, ‘5’):&lt;br /&gt;
return ‘▼’&lt;br /&gt;
return ‘-’&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
now = datetime.now()&lt;br /&gt;
if now.weekday() &amp;gt;= 5:&lt;br /&gt;
return False&lt;br /&gt;
return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
“”“가로 모드 빈 캔버스 (250x122)”””&lt;br /&gt;
width, height = epd.height, epd.width&lt;br /&gt;
image = Image.new(‘1’, (width, height), 255)&lt;br /&gt;
return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
“”“요약 페이지: 양 종목을 위/아래 분할 표시”””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
y = 15&lt;br /&gt;
&lt;br /&gt;
for i, d in enumerate(stocks_data):&lt;br /&gt;
    if d is None:&lt;br /&gt;
        draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        y += section_h&lt;br /&gt;
        continue&lt;br /&gt;
&lt;br /&gt;
    draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
    draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    y += section_h&lt;br /&gt;
    if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
        draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
“”“차트 페이지: 단일 종목 + 주봉 라인 차트”””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
if stock is None:&lt;br /&gt;
    draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
# 헤더: 종목명 + 현재가&lt;br /&gt;
draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
# 등락 + 페이지 라벨&lt;br /&gt;
sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# ── 차트 영역 ──&lt;br /&gt;
cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
    draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
closes = [c[1] for c in chart_data]&lt;br /&gt;
pmin, pmax = min(closes), max(closes)&lt;br /&gt;
if pmax == pmin:&lt;br /&gt;
    pmax = pmin + 1&lt;br /&gt;
span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
# 축&lt;br /&gt;
draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# 가로 그리드 (중간선)&lt;br /&gt;
mid_y = cy + ch // 2&lt;br /&gt;
for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
    draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
# 라인 그리기&lt;br /&gt;
n = len(closes)&lt;br /&gt;
pts = []&lt;br /&gt;
for i, c in enumerate(closes):&lt;br /&gt;
    px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
    py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
    pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
for i in range(len(pts) - 1):&lt;br /&gt;
    draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# 현재가(마지막 봉) 강조 표시&lt;br /&gt;
last_x, last_y = pts[-1]&lt;br /&gt;
draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
# Y축 라벨 (오른쪽)&lt;br /&gt;
label_x = cx + cw + 3&lt;br /&gt;
draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
# X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
if chart_data:&lt;br /&gt;
    d_start = chart_data[0][0]&lt;br /&gt;
    d_end   = chart_data[-1][0]&lt;br /&gt;
    # YYYYMMDD → MM/DD&lt;br /&gt;
    s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
    e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
    draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
    draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
global running&lt;br /&gt;
log.info(f”신호 수신({signum}) - 종료 처리”)&lt;br /&gt;
running = False&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
“”“전체/부분 갱신을 자동 선택해 화면 출력”””&lt;br /&gt;
if state[‘first’] or state[‘partial_count’] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
state[‘partial_count’] = 0&lt;br /&gt;
state[‘first’] = False&lt;br /&gt;
log.info(“전체 갱신”)&lt;br /&gt;
else:&lt;br /&gt;
try:&lt;br /&gt;
epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
state[‘partial_count’] += 1&lt;br /&gt;
log.info(f”부분 갱신 ({state[‘partial_count’]}/{FULL_REFRESH_EVERY})”)&lt;br /&gt;
except AttributeError:&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
log.info(“전체 갱신 (부분 갱신 미지원)”)&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
epd = epd_module.EPD()&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
stocks_data = [None] * len(STOCKS)&lt;br /&gt;
chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
current_page = 0&lt;br /&gt;
&lt;br /&gt;
last_data_fetch = 0&lt;br /&gt;
last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while running:&lt;br /&gt;
        now = time.time()&lt;br /&gt;
        need_redraw = False&lt;br /&gt;
&lt;br /&gt;
        # ── 시세 갱신 ──&lt;br /&gt;
        interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
        if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
            for i, s in enumerate(STOCKS):&lt;br /&gt;
                d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                stocks_data[i] = d&lt;br /&gt;
                if d:&lt;br /&gt;
                    log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
            last_data_fetch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
&lt;br /&gt;
        # ── 주봉 캐시 갱신 ──&lt;br /&gt;
        for s in STOCKS:&lt;br /&gt;
            code = s[&#039;code&#039;]&lt;br /&gt;
            cached = chart_cache.get(code)&lt;br /&gt;
            if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                chart = fetch_weekly_chart(code)&lt;br /&gt;
                chart_cache[code] = (chart, now)&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
        # ── 페이지 전환 ──&lt;br /&gt;
        if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
            current_page = (current_page + 1) % total_pages&lt;br /&gt;
            last_page_switch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
            log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        # ── 그리기 ──&lt;br /&gt;
        if need_redraw:&lt;br /&gt;
            if current_page == 0:&lt;br /&gt;
                image = build_summary_image(epd, stocks_data)&lt;br /&gt;
            else:&lt;br /&gt;
                idx = current_page - 1&lt;br /&gt;
                code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
            render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
        # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
        for _ in range(1):&lt;br /&gt;
            if not running:&lt;br /&gt;
                break&lt;br /&gt;
            time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
finally:&lt;br /&gt;
    log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
    try:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.Clear(0xFF)&lt;br /&gt;
        epd.sleep()&lt;br /&gt;
    except Exception:&lt;br /&gt;
        pass&lt;br /&gt;
    log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == ‘**main**’:&lt;br /&gt;
main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2393</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2393"/>
		<updated>2026-05-06T14:17:11Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: Devcafe (토론)의 2392 판 편집을 되돌림&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2392</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2392"/>
		<updated>2026-05-06T14:15:35Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 라이브러리 설치 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 프로그램 코드 &lt;br /&gt;
&lt;br /&gt;
=== 삼성전자 , 현대차 주가 표시 ===&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13&amp;quot; e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
  Page 0 - 요약 (양 종목)&lt;br /&gt;
  Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
  Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(&#039;~/e-Paper/RaspberryPi_JetsonNano/python/lib&#039;)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
    sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 설정&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
STOCKS = [&lt;br /&gt;
    {&#039;code&#039;: &#039;005930&#039;, &#039;name&#039;: &#039;삼성전자&#039;},&lt;br /&gt;
    {&#039;code&#039;: &#039;005380&#039;, &#039;name&#039;: &#039;현대차&#039;},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = &#039;/usr/share/fonts/truetype/nanum/NanumGothic.ttf&#039;&lt;br /&gt;
FONT_BOLD    = &#039;/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf&#039;&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
    level=logging.INFO,&lt;br /&gt;
    format=&#039;%(asctime)s [%(levelname)s] %(message)s&#039;&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(__name__)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;현재가 (네이버 모바일 API)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    url = f&#039;https://m.stock.naver.com/api/stock/{code}/basic&#039;&lt;br /&gt;
    headers = {&lt;br /&gt;
        &#039;User-Agent&#039;: &#039;Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36&#039;,&lt;br /&gt;
        &#039;Referer&#039;: &#039;https://m.stock.naver.com/&#039;,&lt;br /&gt;
    }&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        d = r.json()&lt;br /&gt;
&lt;br /&gt;
        def to_int(s):&lt;br /&gt;
            return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
        change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
        sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
        if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
            change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
            &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;change&#039;      : change,&lt;br /&gt;
            &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;sign&#039;        : sign,&lt;br /&gt;
            &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
            &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        }&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return None&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    주봉 종가 리스트 [(YYYYMMDD, close), ...]&lt;br /&gt;
    네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    end = datetime.now()&lt;br /&gt;
    # 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
    start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
    url = (f&#039;https://api.finance.naver.com/siseJson.naver&#039;&lt;br /&gt;
           f&#039;?symbol={code}&amp;amp;requestType=1&#039;&lt;br /&gt;
           f&#039;&amp;amp;startTime={start.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;endTime={end.strftime(&amp;quot;%Y%m%d&amp;quot;)}&#039;&lt;br /&gt;
           f&#039;&amp;amp;timeframe=week&#039;)&lt;br /&gt;
    try:&lt;br /&gt;
        r = requests.get(url, headers={&#039;User-Agent&#039;: &#039;Mozilla/5.0&#039;}, timeout=8)&lt;br /&gt;
        r.raise_for_status()&lt;br /&gt;
        text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
        # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
        rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
        data = []&lt;br /&gt;
        for row in rows:&lt;br /&gt;
            parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
            if len(parts) &amp;lt; 5:&lt;br /&gt;
                continue&lt;br /&gt;
            date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
            if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
                continue&lt;br /&gt;
            try:&lt;br /&gt;
                close = float(parts[4])&lt;br /&gt;
                data.append((date_str, close))&lt;br /&gt;
            except (ValueError, IndexError):&lt;br /&gt;
                continue&lt;br /&gt;
&lt;br /&gt;
        return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 유틸&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
    return f&amp;quot;{p:,}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 y축 라벨용 포맷&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if v &amp;gt;= 10000:&lt;br /&gt;
        return f&#039;{v/10000:.1f}만&#039;&lt;br /&gt;
    if v &amp;gt;= 1000:&lt;br /&gt;
        return f&#039;{v/1000:.1f}k&#039;&lt;br /&gt;
    return f&#039;{int(v)}&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
    if sign in (&#039;1&#039;, &#039;2&#039;):&lt;br /&gt;
        return &#039;▲&#039;&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        return &#039;▼&#039;&lt;br /&gt;
    return &#039;-&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
    now = datetime.now()&lt;br /&gt;
    if now.weekday() &amp;gt;= 5:&lt;br /&gt;
        return False&lt;br /&gt;
    return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;가로 모드 빈 캔버스 (250x122)&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    width, height = epd.height, epd.width&lt;br /&gt;
    image = Image.new(&#039;1&#039;, (width, height), 255)&lt;br /&gt;
    return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;요약 페이지: 양 종목을 위/아래 분할 표시&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
    draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
    now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
    draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
    draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
    y = 15&lt;br /&gt;
&lt;br /&gt;
    for i, d in enumerate(stocks_data):&lt;br /&gt;
        if d is None:&lt;br /&gt;
            draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
            y += section_h&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
        price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
        bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
        draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
        sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
        chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
        draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
        ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
        draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
        y += section_h&lt;br /&gt;
        if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
            draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;차트 페이지: 단일 종목 + 주봉 라인 차트&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
    f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
    f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
    f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
    f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
    if stock is None:&lt;br /&gt;
        draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    # 헤더: 종목명 + 현재가&lt;br /&gt;
    draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 등락 + 페이지 라벨&lt;br /&gt;
    sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
    bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # ── 차트 영역 ──&lt;br /&gt;
    cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
    cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
    if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
        draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
        return image&lt;br /&gt;
&lt;br /&gt;
    closes = [c[1] for c in chart_data]&lt;br /&gt;
    pmin, pmax = min(closes), max(closes)&lt;br /&gt;
    if pmax == pmin:&lt;br /&gt;
        pmax = pmin + 1&lt;br /&gt;
    span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
    # 축&lt;br /&gt;
    draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
    draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 가로 그리드 (중간선)&lt;br /&gt;
    mid_y = cy + ch // 2&lt;br /&gt;
    for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
        draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
    # 라인 그리기&lt;br /&gt;
    n = len(closes)&lt;br /&gt;
    pts = []&lt;br /&gt;
    for i, c in enumerate(closes):&lt;br /&gt;
        px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
        py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
        pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
    for i in range(len(pts) - 1):&lt;br /&gt;
        draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
    # 현재가(마지막 봉) 강조 표시&lt;br /&gt;
    last_x, last_y = pts[-1]&lt;br /&gt;
    draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
    # Y축 라벨 (오른쪽)&lt;br /&gt;
    label_x = cx + cw + 3&lt;br /&gt;
    draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
    draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    # X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
    if chart_data:&lt;br /&gt;
        d_start = chart_data[0][0]&lt;br /&gt;
        d_end   = chart_data[-1][0]&lt;br /&gt;
        # YYYYMMDD → MM/DD&lt;br /&gt;
        s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
        e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
        draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
        bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
        draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
# 메인 루프&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
    global running&lt;br /&gt;
    log.info(f&amp;quot;신호 수신({signum}) - 종료 처리&amp;quot;)&lt;br /&gt;
    running = False&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;전체/부분 갱신을 자동 선택해 화면 출력&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    if state[&#039;first&#039;] or state[&#039;partial_count&#039;] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.display(epd.getbuffer(image))&lt;br /&gt;
        state[&#039;partial_count&#039;] = 0&lt;br /&gt;
        state[&#039;first&#039;] = False&lt;br /&gt;
        log.info(&amp;quot;전체 갱신&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
            state[&#039;partial_count&#039;] += 1&lt;br /&gt;
            log.info(f&amp;quot;부분 갱신 ({state[&#039;partial_count&#039;]}/{FULL_REFRESH_EVERY})&amp;quot;)&lt;br /&gt;
        except AttributeError:&lt;br /&gt;
            epd.display(epd.getbuffer(image))&lt;br /&gt;
            log.info(&amp;quot;전체 갱신 (부분 갱신 미지원)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
    signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
    log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
    epd = epd_module.EPD()&lt;br /&gt;
    epd.init()&lt;br /&gt;
    epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
    stocks_data = [None] * len(STOCKS)&lt;br /&gt;
    chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
    state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
    total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
    current_page = 0&lt;br /&gt;
&lt;br /&gt;
    last_data_fetch = 0&lt;br /&gt;
    last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
    try:&lt;br /&gt;
        while running:&lt;br /&gt;
            now = time.time()&lt;br /&gt;
            need_redraw = False&lt;br /&gt;
&lt;br /&gt;
            # ── 시세 갱신 ──&lt;br /&gt;
            interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
            if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
                for i, s in enumerate(STOCKS):&lt;br /&gt;
                    d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                    stocks_data[i] = d&lt;br /&gt;
                    if d:&lt;br /&gt;
                        log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                                 f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
                last_data_fetch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 주봉 캐시 갱신 ──&lt;br /&gt;
            for s in STOCKS:&lt;br /&gt;
                code = s[&#039;code&#039;]&lt;br /&gt;
                cached = chart_cache.get(code)&lt;br /&gt;
                if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                    log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                    chart = fetch_weekly_chart(code)&lt;br /&gt;
                    chart_cache[code] = (chart, now)&lt;br /&gt;
                    need_redraw = True&lt;br /&gt;
&lt;br /&gt;
            # ── 페이지 전환 ──&lt;br /&gt;
            if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
                current_page = (current_page + 1) % total_pages&lt;br /&gt;
                last_page_switch = now&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
                log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
            # ── 그리기 ──&lt;br /&gt;
            if need_redraw:&lt;br /&gt;
                if current_page == 0:&lt;br /&gt;
                    image = build_summary_image(epd, stocks_data)&lt;br /&gt;
                else:&lt;br /&gt;
                    idx = current_page - 1&lt;br /&gt;
                    code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                    chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                    image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
                render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
            # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
            for _ in range(1):&lt;br /&gt;
                if not running:&lt;br /&gt;
                    break&lt;br /&gt;
                time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
    finally:&lt;br /&gt;
        log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
        try:&lt;br /&gt;
            epd.init()&lt;br /&gt;
            epd.Clear(0xFF)&lt;br /&gt;
            epd.sleep()&lt;br /&gt;
        except Exception:&lt;br /&gt;
            pass&lt;br /&gt;
        log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2391</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2391"/>
		<updated>2026-05-06T14:13:47Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 삼성전자 , 현대차 주가 표시 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2390</id>
		<title>라즈베리파이 제로 e-ink 사용하기</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4_%EC%A0%9C%EB%A1%9C_e-ink_%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&amp;diff=2390"/>
		<updated>2026-05-05T11:28:02Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서:  ==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 == === 개요 === 데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다.  Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​  === 주요 특징...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==라즈베리파이 + Waveshare e-Paper로 삼성전자/현대차 실시간 주가 모니터링 ==&lt;br /&gt;
=== 개요 ===&lt;br /&gt;
데이터는 네이버 금융 API(m.stock.naver.com)에서 실시간으로 가져오고, e-ink 수명 보호를 위해 가격이 바뀔 때만 화면을 갱신하도록 구성했습니다. &lt;br /&gt;
Waveshare 2.13” 모델 기준이고, 다른 사이즈는 코드 상단에서 모듈만 바꾸면 됩니다.​​​​​​​​​​​​​​​​&lt;br /&gt;
&lt;br /&gt;
=== 주요 특징 ===&lt;br /&gt;
* 주요 특징&lt;br /&gt;
* 데이터 소스: 네이버 모바일 금융 API(m.stock.naver.com/api/stock/{code}/basic) — 별도 API 키 불필요, 응답 안정적&lt;br /&gt;
e-ink 보호 로직:&lt;br /&gt;
	•	가격/등락이 바뀌었을 때만 화면 갱신 (해시 비교)&lt;br /&gt;
	•	장중 1분, 장외 30분 주기로 자동 전환&lt;br /&gt;
	•	SIGTERM/SIGINT 수신 시 화면 클리어 후 sleep 모드 진입&lt;br /&gt;
화면 구성 (2.7” 264x176 가로 모드):&lt;br /&gt;
	•	상단: 현재 시각 + [장중/장외] 표시&lt;br /&gt;
	•	종목별 영역에 종목명, 현재가(큰 글씨), ▲▼ 등락폭/등락률, 시가/고가/저가&lt;br /&gt;
확인이 필요한 부분&lt;br /&gt;
=== 라이브러리 설치 ===&lt;br /&gt;
&lt;br /&gt;
1. 의존 패키지 설치&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
# 시스템 패키지&lt;br /&gt;
sudo apt update&lt;br /&gt;
sudo apt install -y python3-pip python3-pil python3-numpy fonts-nanum&lt;br /&gt;
sudo apt install -y python3-spidev python3-rpi.gpio&lt;br /&gt;
&lt;br /&gt;
# 파이썬 패키지&lt;br /&gt;
pip3 install requests Pillow&lt;br /&gt;
&lt;br /&gt;
# SPI 활성화 (raspi-config → Interface Options → SPI → Enable)&lt;br /&gt;
sudo raspi-config&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
2. Waveshare e-Paper 라이브러리 설치&lt;br /&gt;
코드 상단 libdir 경로가 위와 일치하는지 확인.&lt;br /&gt;
다른 모델 사용 시 from waveshare_epd import epd2in7 as epd_module 부분만 변경:&lt;br /&gt;
	•	2.13”: epd2in13_V3&lt;br /&gt;
	•	2.9”:  epd2in9_V2&lt;br /&gt;
	•	4.2”:  epd4in2&lt;br /&gt;
	•	7.5”:  epd7in5_V2&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
cd ~&lt;br /&gt;
git clone https://github.com/waveshare/e-Paper.git&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. 실행 테스트&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
python3 stock_monitor.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. 부팅 시 자동 실행 (systemd)&lt;br /&gt;
/etc/systemd/system/stock-monitor.service 파일 생성:&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Stock Price e-Paper Monitor&lt;br /&gt;
After=network-online.target&lt;br /&gt;
Wants=network-online.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=pi&lt;br /&gt;
WorkingDirectory=/home/pi/stock_monitor&lt;br /&gt;
ExecStart=/usr/bin/python3 /home/pi/stock_monitor/stock_monitor.py&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=10&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
활성화&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload&lt;br /&gt;
sudo systemctl enable stock-monitor.service&lt;br /&gt;
sudo systemctl start stock-monitor.service&lt;br /&gt;
sudo systemctl status stock-monitor.service&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
로그확인&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
journalctl -u stock-monitor.service -f&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 삼성전자 , 현대차 주가 표시 ===&lt;br /&gt;
&amp;lt;source lang=python&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
&lt;br /&gt;
# -*- coding: utf-8 -*-&lt;br /&gt;
&lt;br /&gt;
“””&lt;br /&gt;
삼성전자(005930) / 현대차(005380) 실시간 주가 모니터 + 주봉 차트&lt;br /&gt;
Raspberry Pi + Waveshare 2.13” e-Paper (250x122)&lt;br /&gt;
&lt;br /&gt;
페이지 자동 전환:&lt;br /&gt;
Page 0 - 요약 (양 종목)&lt;br /&gt;
Page 1 - 삼성전자 + 주봉 차트&lt;br /&gt;
Page 2 - 현대차 + 주봉 차트&lt;br /&gt;
&lt;br /&gt;
Author: 치치 (dbaworks)&lt;br /&gt;
“””&lt;br /&gt;
&lt;br /&gt;
import os&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
import time&lt;br /&gt;
import logging&lt;br /&gt;
import signal&lt;br /&gt;
import requests&lt;br /&gt;
from datetime import datetime, timedelta, time as dtime&lt;br /&gt;
from PIL import Image, ImageDraw, ImageFont&lt;br /&gt;
&lt;br /&gt;
libdir = os.path.expanduser(’~/e-Paper/RaspberryPi_JetsonNano/python/lib’)&lt;br /&gt;
if os.path.exists(libdir):&lt;br /&gt;
sys.path.append(libdir)&lt;br /&gt;
&lt;br /&gt;
from waveshare_epd import epd2in13_V4 as epd_module   # ← 모델에 맞게 변경&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 설정&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
STOCKS = [&lt;br /&gt;
{‘code’: ‘005930’, ‘name’: ‘삼성전자’},&lt;br /&gt;
{‘code’: ‘005380’, ‘name’: ‘현대차’},&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
CHART_WEEKS = 26                  # 주봉 표시 기간&lt;br /&gt;
DATA_REFRESH_OPEN  = 60           # 장중 시세 갱신 주기 (초)&lt;br /&gt;
DATA_REFRESH_CLOSE = 60 * 30      # 장외 시세 갱신 주기&lt;br /&gt;
CHART_CACHE_TTL    = 60 * 60      # 주봉 캐시 유지 시간 (1시간)&lt;br /&gt;
PAGE_DURATION      = 20           # 페이지 자동 전환 주기 (초)&lt;br /&gt;
FULL_REFRESH_EVERY = 30           # 부분 갱신 N회마다 전체 갱신&lt;br /&gt;
&lt;br /&gt;
FONT_REGULAR = ‘/usr/share/fonts/truetype/nanum/NanumGothic.ttf’&lt;br /&gt;
FONT_BOLD    = ‘/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf’&lt;br /&gt;
&lt;br /&gt;
MARKET_OPEN  = dtime(9, 0)&lt;br /&gt;
MARKET_CLOSE = dtime(15, 30)&lt;br /&gt;
&lt;br /&gt;
logging.basicConfig(&lt;br /&gt;
level=logging.INFO,&lt;br /&gt;
format=’%(asctime)s [%(levelname)s] %(message)s’&lt;br /&gt;
)&lt;br /&gt;
log = logging.getLogger(**name**)&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 데이터 조회&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fetch_stock(code):&lt;br /&gt;
“”“현재가 (네이버 모바일 API)”””&lt;br /&gt;
url = f’https://m.stock.naver.com/api/stock/{code}/basic’&lt;br /&gt;
headers = {&lt;br /&gt;
‘User-Agent’: ‘Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36’,&lt;br /&gt;
‘Referer’: ‘https://m.stock.naver.com/’,&lt;br /&gt;
}&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers=headers, timeout=5)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
d = r.json()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    def to_int(s):&lt;br /&gt;
        return int(str(s).replace(&#039;,&#039;, &#039;&#039;).replace(&#039;+&#039;, &#039;&#039;).replace(&#039;-&#039;, &#039;&#039;)) if s else 0&lt;br /&gt;
&lt;br /&gt;
    change = to_int(d.get(&#039;compareToPreviousClosePrice&#039;, &#039;0&#039;))&lt;br /&gt;
    sign = d.get(&#039;compareToPreviousPrice&#039;, {}).get(&#039;code&#039;, &#039;3&#039;)&lt;br /&gt;
    if sign in (&#039;4&#039;, &#039;5&#039;):&lt;br /&gt;
        change = -abs(change)&lt;br /&gt;
&lt;br /&gt;
    return {&lt;br /&gt;
        &#039;name&#039;        : d.get(&#039;stockName&#039;, &#039;&#039;),&lt;br /&gt;
        &#039;price&#039;       : to_int(d.get(&#039;closePrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;change&#039;      : change,&lt;br /&gt;
        &#039;change_rate&#039; : float(d.get(&#039;fluctuationsRatio&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;sign&#039;        : sign,&lt;br /&gt;
        &#039;open&#039;        : to_int(d.get(&#039;openPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;high&#039;        : to_int(d.get(&#039;highPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
        &#039;low&#039;         : to_int(d.get(&#039;lowPrice&#039;, &#039;0&#039;)),&lt;br /&gt;
    }&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 시세 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return None&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def fetch_weekly_chart(code, weeks=CHART_WEEKS):&lt;br /&gt;
“””&lt;br /&gt;
주봉 종가 리스트 [(YYYYMMDD, close), …]&lt;br /&gt;
네이버 차트 API: 응답이 Python list-literal 형태의 텍스트&lt;br /&gt;
“””&lt;br /&gt;
end = datetime.now()&lt;br /&gt;
# 여유분 포함해서 weeks*7+30일치 요청&lt;br /&gt;
start = end - timedelta(days=weeks * 7 + 30)&lt;br /&gt;
url = (f’https://api.finance.naver.com/siseJson.naver’&lt;br /&gt;
f’?symbol={code}&amp;amp;requestType=1’&lt;br /&gt;
f’&amp;amp;startTime={start.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;endTime={end.strftime(”%Y%m%d”)}’&lt;br /&gt;
f’&amp;amp;timeframe=week’)&lt;br /&gt;
try:&lt;br /&gt;
r = requests.get(url, headers={‘User-Agent’: ‘Mozilla/5.0’}, timeout=8)&lt;br /&gt;
r.raise_for_status()&lt;br /&gt;
text = r.text.strip()&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
    # 각 [...] 블록 추출 후 파싱&lt;br /&gt;
    rows = re.findall(r&amp;quot;\[([^\[\]]+)\]&amp;quot;, text)&lt;br /&gt;
    data = []&lt;br /&gt;
    for row in rows:&lt;br /&gt;
        parts = [p.strip() for p in row.split(&#039;,&#039;)]&lt;br /&gt;
        if len(parts) &amp;lt; 5:&lt;br /&gt;
            continue&lt;br /&gt;
        date_str = parts[0].strip(&amp;quot;&#039;\&amp;quot; &amp;quot;)&lt;br /&gt;
        if not date_str.isdigit():     # 헤더 행 스킵&lt;br /&gt;
            continue&lt;br /&gt;
        try:&lt;br /&gt;
            close = float(parts[4])&lt;br /&gt;
            data.append((date_str, close))&lt;br /&gt;
        except (ValueError, IndexError):&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
    return data[-weeks:] if len(data) &amp;gt; weeks else data&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.error(f&amp;quot;[{code}] 주봉 조회 실패: {e}&amp;quot;)&lt;br /&gt;
    return []&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 유틸&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def fmt_price(p):&lt;br /&gt;
return f”{p:,}”&lt;br /&gt;
&lt;br /&gt;
def fmt_axis(v):&lt;br /&gt;
“”“차트 y축 라벨용 포맷”””&lt;br /&gt;
if v &amp;gt;= 10000:&lt;br /&gt;
return f’{v/10000:.1f}만’&lt;br /&gt;
if v &amp;gt;= 1000:&lt;br /&gt;
return f’{v/1000:.1f}k’&lt;br /&gt;
return f’{int(v)}’&lt;br /&gt;
&lt;br /&gt;
def change_symbol(sign):&lt;br /&gt;
if sign in (‘1’, ‘2’):&lt;br /&gt;
return ‘▲’&lt;br /&gt;
if sign in (‘4’, ‘5’):&lt;br /&gt;
return ‘▼’&lt;br /&gt;
return ‘-’&lt;br /&gt;
&lt;br /&gt;
def is_market_hours():&lt;br /&gt;
now = datetime.now()&lt;br /&gt;
if now.weekday() &amp;gt;= 5:&lt;br /&gt;
return False&lt;br /&gt;
return MARKET_OPEN &amp;lt;= now.time() &amp;lt;= MARKET_CLOSE&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 화면 그리기&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
def _new_canvas(epd):&lt;br /&gt;
“”“가로 모드 빈 캔버스 (250x122)”””&lt;br /&gt;
width, height = epd.height, epd.width&lt;br /&gt;
image = Image.new(‘1’, (width, height), 255)&lt;br /&gt;
return image, ImageDraw.Draw(image), width, height&lt;br /&gt;
&lt;br /&gt;
def build_summary_image(epd, stocks_data):&lt;br /&gt;
“”“요약 페이지: 양 종목을 위/아래 분할 표시”””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_hdr   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_time  = ImageFont.truetype(FONT_REGULAR, 10)&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    22)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
market = &#039;장중&#039; if is_market_hours() else &#039;장외&#039;&lt;br /&gt;
draw.text((2, 0), f&#039;주가[{market}]&#039;, font=f_hdr, fill=0)&lt;br /&gt;
now_str = datetime.now().strftime(&#039;%m-%d %H:%M&#039;)&lt;br /&gt;
bbox = draw.textbbox((0, 0), now_str, font=f_time)&lt;br /&gt;
draw.text((width - (bbox[2] - bbox[0]) - 2, 1), now_str, font=f_time, fill=0)&lt;br /&gt;
draw.line([(0, 13), (width, 13)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
section_h = (height - 14) // len(stocks_data)&lt;br /&gt;
y = 15&lt;br /&gt;
&lt;br /&gt;
for i, d in enumerate(stocks_data):&lt;br /&gt;
    if d is None:&lt;br /&gt;
        draw.text((4, y + 18), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
        y += section_h&lt;br /&gt;
        continue&lt;br /&gt;
&lt;br /&gt;
    draw.text((3, y + 2), d[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
    price_str = fmt_price(d[&#039;price&#039;])&lt;br /&gt;
    bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
    draw.text((width - (bbox[2]-bbox[0]) - 3, y + 1), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
    sym = change_symbol(d[&#039;sign&#039;])&lt;br /&gt;
    chg_str = f&amp;quot;{sym} {abs(d[&#039;change&#039;]):,} ({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
    draw.text((3, y + 26), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
    ohl = f&amp;quot;시{fmt_price(d[&#039;open&#039;])} 고{fmt_price(d[&#039;high&#039;])} 저{fmt_price(d[&#039;low&#039;])}&amp;quot;&lt;br /&gt;
    draw.text((3, y + 41), ohl, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
    y += section_h&lt;br /&gt;
    if i &amp;lt; len(stocks_data) - 1:&lt;br /&gt;
        draw.line([(0, y - 1), (width, y - 1)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
def build_chart_image(epd, stock, chart_data):&lt;br /&gt;
“”“차트 페이지: 단일 종목 + 주봉 라인 차트”””&lt;br /&gt;
image, draw, width, height = _new_canvas(epd)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
f_name  = ImageFont.truetype(FONT_BOLD,    13)&lt;br /&gt;
f_price = ImageFont.truetype(FONT_BOLD,    16)&lt;br /&gt;
f_chg   = ImageFont.truetype(FONT_BOLD,    11)&lt;br /&gt;
f_tiny  = ImageFont.truetype(FONT_REGULAR,  9)&lt;br /&gt;
&lt;br /&gt;
if stock is None:&lt;br /&gt;
    draw.text((60, 50), &#039;데이터 조회 실패&#039;, font=f_name, fill=0)&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
# 헤더: 종목명 + 현재가&lt;br /&gt;
draw.text((3, 0), stock[&#039;name&#039;], font=f_name, fill=0)&lt;br /&gt;
price_str = fmt_price(stock[&#039;price&#039;])&lt;br /&gt;
bbox = draw.textbbox((0, 0), price_str, font=f_price)&lt;br /&gt;
draw.text((width - (bbox[2]-bbox[0]) - 3, 0), price_str, font=f_price, fill=0)&lt;br /&gt;
&lt;br /&gt;
# 등락 + 페이지 라벨&lt;br /&gt;
sym = change_symbol(stock[&#039;sign&#039;])&lt;br /&gt;
chg_str = f&amp;quot;{sym} {abs(stock[&#039;change&#039;]):,} ({stock[&#039;change_rate&#039;]:+.2f}%)&amp;quot;&lt;br /&gt;
draw.text((3, 18), chg_str, font=f_chg, fill=0)&lt;br /&gt;
&lt;br /&gt;
label = f&#039;주봉 {len(chart_data)}주&#039;&lt;br /&gt;
bbox = draw.textbbox((0, 0), label, font=f_tiny)&lt;br /&gt;
draw.text((width - (bbox[2]-bbox[0]) - 3, 21), label, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
draw.line([(0, 32), (width, 32)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# ── 차트 영역 ──&lt;br /&gt;
cx, cy = 3, 36                  # 좌상단&lt;br /&gt;
cw, ch = 200, 82                 # 폭, 높이 (오른쪽에 라벨 공간 확보)&lt;br /&gt;
&lt;br /&gt;
if not chart_data or len(chart_data) &amp;lt; 2:&lt;br /&gt;
    draw.text((cx + 40, cy + ch//2 - 5), &#039;차트 데이터 없음&#039;, font=f_tiny, fill=0)&lt;br /&gt;
    return image&lt;br /&gt;
&lt;br /&gt;
closes = [c[1] for c in chart_data]&lt;br /&gt;
pmin, pmax = min(closes), max(closes)&lt;br /&gt;
if pmax == pmin:&lt;br /&gt;
    pmax = pmin + 1&lt;br /&gt;
span = pmax - pmin&lt;br /&gt;
&lt;br /&gt;
# 축&lt;br /&gt;
draw.line([(cx, cy), (cx, cy + ch)], fill=0, width=1)&lt;br /&gt;
draw.line([(cx, cy + ch), (cx + cw, cy + ch)], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# 가로 그리드 (중간선)&lt;br /&gt;
mid_y = cy + ch // 2&lt;br /&gt;
for x in range(cx + 2, cx + cw, 6):&lt;br /&gt;
    draw.point((x, mid_y), fill=0)&lt;br /&gt;
&lt;br /&gt;
# 라인 그리기&lt;br /&gt;
n = len(closes)&lt;br /&gt;
pts = []&lt;br /&gt;
for i, c in enumerate(closes):&lt;br /&gt;
    px = cx + 1 + int(i * (cw - 2) / max(n - 1, 1))&lt;br /&gt;
    py = cy + ch - 1 - int((c - pmin) / span * (ch - 3))&lt;br /&gt;
    pts.append((px, py))&lt;br /&gt;
&lt;br /&gt;
for i in range(len(pts) - 1):&lt;br /&gt;
    draw.line([pts[i], pts[i + 1]], fill=0, width=1)&lt;br /&gt;
&lt;br /&gt;
# 현재가(마지막 봉) 강조 표시&lt;br /&gt;
last_x, last_y = pts[-1]&lt;br /&gt;
draw.ellipse([last_x - 2, last_y - 2, last_x + 2, last_y + 2], fill=0)&lt;br /&gt;
&lt;br /&gt;
# Y축 라벨 (오른쪽)&lt;br /&gt;
label_x = cx + cw + 3&lt;br /&gt;
draw.text((label_x, cy - 4),         fmt_axis(pmax), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, mid_y - 5),      fmt_axis((pmax + pmin) / 2), font=f_tiny, fill=0)&lt;br /&gt;
draw.text((label_x, cy + ch - 9),    fmt_axis(pmin), font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
# X축 라벨 (시작 / 끝 날짜)&lt;br /&gt;
if chart_data:&lt;br /&gt;
    d_start = chart_data[0][0]&lt;br /&gt;
    d_end   = chart_data[-1][0]&lt;br /&gt;
    # YYYYMMDD → MM/DD&lt;br /&gt;
    s_str = f&#039;{d_start[4:6]}/{d_start[6:8]}&#039;&lt;br /&gt;
    e_str = f&#039;{d_end[4:6]}/{d_end[6:8]}&#039;&lt;br /&gt;
    draw.text((cx + 2, cy + ch + 1), s_str, font=f_tiny, fill=0)&lt;br /&gt;
    bbox = draw.textbbox((0, 0), e_str, font=f_tiny)&lt;br /&gt;
    draw.text((cx + cw - (bbox[2]-bbox[0]) - 2, cy + ch + 1), e_str, font=f_tiny, fill=0)&lt;br /&gt;
&lt;br /&gt;
return image&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
# 메인 루프&lt;br /&gt;
&lt;br /&gt;
# ─────────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
running = True&lt;br /&gt;
&lt;br /&gt;
def handle_signal(signum, frame):&lt;br /&gt;
global running&lt;br /&gt;
log.info(f”신호 수신({signum}) - 종료 처리”)&lt;br /&gt;
running = False&lt;br /&gt;
&lt;br /&gt;
def render(epd, image, state):&lt;br /&gt;
“”“전체/부분 갱신을 자동 선택해 화면 출력”””&lt;br /&gt;
if state[‘first’] or state[‘partial_count’] &amp;gt;= FULL_REFRESH_EVERY:&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
state[‘partial_count’] = 0&lt;br /&gt;
state[‘first’] = False&lt;br /&gt;
log.info(“전체 갱신”)&lt;br /&gt;
else:&lt;br /&gt;
try:&lt;br /&gt;
epd.displayPartial(epd.getbuffer(image))&lt;br /&gt;
state[‘partial_count’] += 1&lt;br /&gt;
log.info(f”부분 갱신 ({state[‘partial_count’]}/{FULL_REFRESH_EVERY})”)&lt;br /&gt;
except AttributeError:&lt;br /&gt;
epd.display(epd.getbuffer(image))&lt;br /&gt;
log.info(“전체 갱신 (부분 갱신 미지원)”)&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
signal.signal(signal.SIGINT,  handle_signal)&lt;br /&gt;
signal.signal(signal.SIGTERM, handle_signal)&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
log.info(&amp;quot;주가 모니터 시작 (Waveshare 2.13\&amp;quot;)&amp;quot;)&lt;br /&gt;
epd = epd_module.EPD()&lt;br /&gt;
epd.init()&lt;br /&gt;
epd.Clear(0xFF)&lt;br /&gt;
&lt;br /&gt;
stocks_data = [None] * len(STOCKS)&lt;br /&gt;
chart_cache = {}                 # code -&amp;gt; ([(date, close), ...], fetched_at)&lt;br /&gt;
state = {&#039;first&#039;: True, &#039;partial_count&#039;: 0}&lt;br /&gt;
&lt;br /&gt;
total_pages = 1 + len(STOCKS)    # 요약 + 종목별 차트&lt;br /&gt;
current_page = 0&lt;br /&gt;
&lt;br /&gt;
last_data_fetch = 0&lt;br /&gt;
last_page_switch = time.time()&lt;br /&gt;
&lt;br /&gt;
try:&lt;br /&gt;
    while running:&lt;br /&gt;
        now = time.time()&lt;br /&gt;
        need_redraw = False&lt;br /&gt;
&lt;br /&gt;
        # ── 시세 갱신 ──&lt;br /&gt;
        interval = DATA_REFRESH_OPEN if is_market_hours() else DATA_REFRESH_CLOSE&lt;br /&gt;
        if now - last_data_fetch &amp;gt;= interval:&lt;br /&gt;
            for i, s in enumerate(STOCKS):&lt;br /&gt;
                d = fetch_stock(s[&#039;code&#039;])&lt;br /&gt;
                stocks_data[i] = d&lt;br /&gt;
                if d:&lt;br /&gt;
                    log.info(f&amp;quot;{d[&#039;name&#039;]:8s} {d[&#039;price&#039;]:&amp;gt;8,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;{change_symbol(d[&#039;sign&#039;])}{abs(d[&#039;change&#039;]):&amp;gt;6,}  &amp;quot;&lt;br /&gt;
                             f&amp;quot;({d[&#039;change_rate&#039;]:+.2f}%)&amp;quot;)&lt;br /&gt;
            last_data_fetch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
&lt;br /&gt;
        # ── 주봉 캐시 갱신 ──&lt;br /&gt;
        for s in STOCKS:&lt;br /&gt;
            code = s[&#039;code&#039;]&lt;br /&gt;
            cached = chart_cache.get(code)&lt;br /&gt;
            if cached is None or now - cached[1] &amp;gt;= CHART_CACHE_TTL:&lt;br /&gt;
                log.info(f&amp;quot;[{code}] 주봉 조회&amp;quot;)&lt;br /&gt;
                chart = fetch_weekly_chart(code)&lt;br /&gt;
                chart_cache[code] = (chart, now)&lt;br /&gt;
                need_redraw = True&lt;br /&gt;
&lt;br /&gt;
        # ── 페이지 전환 ──&lt;br /&gt;
        if now - last_page_switch &amp;gt;= PAGE_DURATION:&lt;br /&gt;
            current_page = (current_page + 1) % total_pages&lt;br /&gt;
            last_page_switch = now&lt;br /&gt;
            need_redraw = True&lt;br /&gt;
            log.info(f&amp;quot;페이지 전환 → {current_page}&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        # ── 그리기 ──&lt;br /&gt;
        if need_redraw:&lt;br /&gt;
            if current_page == 0:&lt;br /&gt;
                image = build_summary_image(epd, stocks_data)&lt;br /&gt;
            else:&lt;br /&gt;
                idx = current_page - 1&lt;br /&gt;
                code = STOCKS[idx][&#039;code&#039;]&lt;br /&gt;
                chart = chart_cache.get(code, ([], 0))[0]&lt;br /&gt;
                image = build_chart_image(epd, stocks_data[idx], chart)&lt;br /&gt;
            render(epd, image, state)&lt;br /&gt;
&lt;br /&gt;
        # 1초 단위로 깨어나서 페이지/데이터 시점 체크&lt;br /&gt;
        for _ in range(1):&lt;br /&gt;
            if not running:&lt;br /&gt;
                break&lt;br /&gt;
            time.sleep(1)&lt;br /&gt;
&lt;br /&gt;
except Exception as e:&lt;br /&gt;
    log.exception(f&amp;quot;오류: {e}&amp;quot;)&lt;br /&gt;
finally:&lt;br /&gt;
    log.info(&amp;quot;e-Paper 정리&amp;quot;)&lt;br /&gt;
    try:&lt;br /&gt;
        epd.init()&lt;br /&gt;
        epd.Clear(0xFF)&lt;br /&gt;
        epd.sleep()&lt;br /&gt;
    except Exception:&lt;br /&gt;
        pass&lt;br /&gt;
    log.info(&amp;quot;종료 완료&amp;quot;)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
if **name** == ‘**main**’:&lt;br /&gt;
main()&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2389</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2389"/>
		<updated>2026-04-08T04:51:21Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 = * 해당 기업 검색&lt;br /&gt;
 &lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2388</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2388"/>
		<updated>2026-04-08T04:49:24Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 =&lt;br /&gt;
|내용 = DART (dart.fss.or.kr)&lt;br /&gt;
 &lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2387</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2387"/>
		<updated>2026-04-08T04:49:01Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =&lt;br /&gt;
 &lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2386</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2386"/>
		<updated>2026-04-08T04:48:50Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =*&lt;br /&gt;
 &lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2385</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2385"/>
		<updated>2026-04-08T04:48:33Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =&lt;br /&gt;
 &lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2384</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2384"/>
		<updated>2026-04-08T04:48:24Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =&lt;br /&gt;
&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2383</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2383"/>
		<updated>2026-04-08T04:48:08Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =:&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2382</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2382"/>
		<updated>2026-04-08T04:47:53Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 유가증권보고서 어디서 찾나? */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =&lt;br /&gt;
:&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2381</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2381"/>
		<updated>2026-04-08T04:47:34Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 유가증권보고서 어디서 찾나? ===&lt;br /&gt;
&lt;br /&gt;
{{심플박스&lt;br /&gt;
|제목 = DART (dart.fss.or.kr)&lt;br /&gt;
|내용 =&lt;br /&gt;
&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2380</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2380"/>
		<updated>2026-04-08T04:45:57Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2379</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2379"/>
		<updated>2026-04-08T04:45:41Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
=== 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용) ===&lt;br /&gt;
&lt;br /&gt;
{{중요&lt;br /&gt;
|제목= * 개념 정리 &lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2378</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2378"/>
		<updated>2026-04-08T04:45:05Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
{{중요&lt;br /&gt;
|제목=&lt;br /&gt;
|내용=&lt;br /&gt;
&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2377</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2377"/>
		<updated>2026-04-08T04:44:43Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
{{중요&lt;br /&gt;
|제목=&lt;br /&gt;
|내용=&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2376</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2376"/>
		<updated>2026-04-08T04:44:16Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
{{중요&lt;br /&gt;
|제목=개념 &lt;br /&gt;
|내용=&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2375</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2375"/>
		<updated>2026-04-08T04:44:06Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
{{:중요&lt;br /&gt;
|제목=개념 &lt;br /&gt;
|내용=&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2374</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2374"/>
		<updated>2026-04-08T04:43:44Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
{:중요&lt;br /&gt;
|제목=개념 &lt;br /&gt;
|내용=&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2373</id>
		<title>주식 시클리컬 가치주 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%A3%BC%EC%8B%9D_%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EA%B0%80%EC%B9%98%EC%A3%BC_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2373"/>
		<updated>2026-04-08T04:38:10Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬 가치주 투자 ==&lt;br /&gt;
&lt;br /&gt;
* - 고정비/변동비 분석 &amp;amp; PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 1️⃣ 고정비 vs 변동비 분리 (유가증권보고서 활용)&lt;br /&gt;
&lt;br /&gt;
### 개념 &lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
매출&lt;br /&gt;
  └─ 변동비 (매출과 함께 움직임)&lt;br /&gt;
       └─ 원재료비, 외주가공비, 포장비 등&lt;br /&gt;
  └─ 고정비 (매출과 무관하게 고정)&lt;br /&gt;
       └─ 감가상각비, 인건비, 임차료, 이자비용 등&lt;br /&gt;
  └─ 영업이익&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식의 핵심은&lt;br /&gt;
&amp;gt; **&amp;quot;업황이 좋아질 때 변동비는 조금만 늘고, 고정비는 그대로라 이익이 폭발적으로 증가&amp;quot;**&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 유가증권보고서 어디서 찾나?&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
DART (dart.fss.or.kr)&lt;br /&gt;
  └─ 해당 기업 검색&lt;br /&gt;
       └─ 사업보고서&lt;br /&gt;
            ├─ [제조원가명세서] → 변동비/고정비 핵심&lt;br /&gt;
            ├─ [손익계산서]    → 매출, 영업이익&lt;br /&gt;
            └─ [주석]          → 비용의 성격별 분류&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스 (철강 시클리컬)&lt;br /&gt;
&lt;br /&gt;
**제조원가명세서 (단순화)**&lt;br /&gt;
&lt;br /&gt;
| 항목 | 금액 | 구분 |&lt;br /&gt;
|------|------|------|&lt;br /&gt;
| 원재료비 (철광석, 석탄) | 18조원 | 🔴 변동비 |&lt;br /&gt;
| 외주가공비 | 1조원 | 🔴 변동비 |&lt;br /&gt;
| 노무비 (인건비) | 2.5조원 | 🟡 준고정비 |&lt;br /&gt;
| 감가상각비 | 1.8조원 | 🔵 고정비 |&lt;br /&gt;
| 임차료/유지보수 | 0.7조원 | 🔵 고정비 |&lt;br /&gt;
| **합계** | **24조원** | |&lt;br /&gt;
&lt;br /&gt;
이걸 분리하면:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
변동비율 = 변동비 / 매출&lt;br /&gt;
         = 19조 / 26조 = 약 73%&lt;br /&gt;
&lt;br /&gt;
공헌이익률 = 1 - 변동비율&lt;br /&gt;
           = 27%&lt;br /&gt;
&lt;br /&gt;
고정비 총액 = 약 5조원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 이게 왜 중요한가? - 영업레버리지 계산&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
영업레버리지 = 공헌이익 / 영업이익&lt;br /&gt;
&lt;br /&gt;
예시)&lt;br /&gt;
  매출 26조, 변동비 19조, 고정비 5조&lt;br /&gt;
  → 공헌이익 = 7조&lt;br /&gt;
  → 영업이익 = 2조&lt;br /&gt;
  → 영업레버리지 = 7조 / 2조 = 3.5배&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
**의미:**&lt;br /&gt;
&amp;gt; 매출이 10% 증가하면 → 영업이익은 **35% 증가**&lt;br /&gt;
&amp;gt; 매출이 10% 감소하면 → 영업이익은 **35% 감소**&lt;br /&gt;
&lt;br /&gt;
이게 시클리컬이 업황에 따라 주가가 폭등/폭락하는 이유입니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
## 2️⃣ 수익 증가 시 PBR/PER 역산&lt;br /&gt;
&lt;br /&gt;
### 개념&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
보통 투자:  현재 주가 → PER/PBR 계산&lt;br /&gt;
역산 투자:  &amp;quot;업황 회복 시 EPS가 얼마일까?&amp;quot; &lt;br /&gt;
            → 적정 PER 적용&lt;br /&gt;
            → 목표 주가 산출&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 구체적 예시 - POSCO홀딩스&lt;br /&gt;
&lt;br /&gt;
**현재 상황 (업황 침체기)**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
현재 주가:       300,000원&lt;br /&gt;
현재 EPS:        10,000원  (불황기 이익)&lt;br /&gt;
현재 PER:        30배       (이익 적으니 PER 높아 보임)&lt;br /&gt;
현재 BPS:        500,000원&lt;br /&gt;
현재 PBR:        0.6배      (저평가 신호)&lt;br /&gt;
발행주식수:      8,500만주&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 1 - 업황 회복 시 이익 추정**&lt;br /&gt;
&lt;br /&gt;
고정비/변동비 분석에서 구한 영업레버리지 3.5배 활용&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
시나리오: 철강 가격 15% 회복 → 매출 15% 증가&lt;br /&gt;
&lt;br /&gt;
영업이익 증가율 = 15% × 3.5배 = 52.5% 증가&lt;br /&gt;
&lt;br /&gt;
현재 영업이익: 1.5조원&lt;br /&gt;
회복 시 영업이익: 1.5조 × 1.525 = 2.3조원&lt;br /&gt;
&lt;br /&gt;
세후 순이익 (세율 25% 가정):&lt;br /&gt;
  2.3조 × 0.75 = 1.72조원&lt;br /&gt;
&lt;br /&gt;
회복 시 EPS:&lt;br /&gt;
  1.72조 / 8,500만주 = 약 20,200원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 2 - PER 역산으로 목표주가 산출**&lt;br /&gt;
&lt;br /&gt;
시클리컬 주식은 **업황 정점일 때 PER 낮게, 침체기에 PER 높게** 적용합니다.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
역사적 정상 PER 범위: 8~12배 (철강 섹터 기준)&lt;br /&gt;
&lt;br /&gt;
보수적 목표주가 = 회복 EPS × 8배&lt;br /&gt;
               = 20,200 × 8 = 161,600원  ← 이미 현재가보다 낮음?&lt;br /&gt;
&lt;br /&gt;
중립 목표주가   = 20,200 × 10배&lt;br /&gt;
               = 202,000원&lt;br /&gt;
&lt;br /&gt;
낙관 목표주가   = 20,200 × 12배&lt;br /&gt;
               = 242,400원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
**STEP 3 - PBR 역산으로 교차 검증**&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
회복 시 ROE 추정:&lt;br /&gt;
  순이익 1.72조 / 자본총계 42.5조 = 4.0%&lt;br /&gt;
&lt;br /&gt;
PBR-ROE 적정 공식 (그레이엄식):&lt;br /&gt;
  적정 PBR = ROE / 기대수익률&lt;br /&gt;
           = 4.0% / 8% = 0.5배&lt;br /&gt;
&lt;br /&gt;
적정 주가 (PBR 기준):&lt;br /&gt;
  BPS 500,000원 × 0.5 = 250,000원&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 역산 결과 종합&lt;br /&gt;
&lt;br /&gt;
| 방법 | 목표주가 |&lt;br /&gt;
|------|---------|&lt;br /&gt;
| PER 역산 (보수) | 161,600원 |&lt;br /&gt;
| PER 역산 (중립) | 202,000원 |&lt;br /&gt;
| PER 역산 (낙관) | 242,400원 |&lt;br /&gt;
| PBR 역산 | 250,000원 |&lt;br /&gt;
| **현재주가** | **300,000원** |&lt;br /&gt;
&lt;br /&gt;
이 경우라면 → **아직 비싸다, 매수 시점 아님** 이라는 결론이 나옵니다.&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
### 📌 시클리컬 투자 핵심 요약&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=c&amp;gt;&lt;br /&gt;
업황 바닥일 때&lt;br /&gt;
  └─ PER 높아 보임 (이익 적어서) → 겁먹고 안 삼&lt;br /&gt;
  └─ PBR 낮음 (자산 대비 주가 쌈) → 매수 신호&lt;br /&gt;
&lt;br /&gt;
업황 정점일 때&lt;br /&gt;
  └─ PER 낮아 보임 (이익 많아서) → 싸 보여서 삼&lt;br /&gt;
  └─ PBR 높음 → 실제로는 고점&lt;br /&gt;
&lt;br /&gt;
시클리컬 황금 매수 타이밍:&lt;br /&gt;
  &amp;quot;PBR 0.5 이하 + 영업레버리지 높음 + 업황 회복 징조&amp;quot;&lt;br /&gt;
&amp;lt;source&amp;gt;&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%B6%84%EB%A5%98:%EC%A3%BC%EC%8B%9D&amp;diff=2372</id>
		<title>분류:주식</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%B6%84%EB%A5%98:%EC%A3%BC%EC%8B%9D&amp;diff=2372"/>
		<updated>2026-04-08T04:32:23Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: {{#default_form:Devcafe}}&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{#default_form:Devcafe}}&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%B6%80%EC%9E%90%EC%95%84%EB%B9%A0_%ED%83%80%EC%A7%B1_%ED%88%AC%EC%9E%90%EA%B8%B0%EB%A1%9D&amp;diff=2371</id>
		<title>부자아빠 타짱 투자기록</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%B6%80%EC%9E%90%EC%95%84%EB%B9%A0_%ED%83%80%EC%A7%B1_%ED%88%AC%EC%9E%90%EA%B8%B0%EB%A1%9D&amp;diff=2371"/>
		<updated>2026-04-02T09:45:25Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: @yhdgj675 (たーちゃん＠バブルおじさん) 씨의 **실제 투자 경험**, **과거 주요 보유·집중 투자 종목**, **인터뷰·책 내용**을 바탕으로 **투자 방법**을 중심으로 요약 정리해 드릴게요.  ### 투자 여정 요약 (50만 엔 → 80억 엔 이상) - **1998년 (대학생 시절)**: 아르바이트 돈으로 **50만 엔** 시작. 『금-rich 아버지 가난한 아버지』 읽고 투자 시작. 첫 종목은 **세가사미 홀딩...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;@yhdgj675 (たーちゃん＠バブルおじさん) 씨의 **실제 투자 경험**, **과거 주요 보유·집중 투자 종목**, **인터뷰·책 내용**을 바탕으로 **투자 방법**을 중심으로 요약 정리해 드릴게요.&lt;br /&gt;
&lt;br /&gt;
### 투자 여정 요약 (50만 엔 → 80억 엔 이상)&lt;br /&gt;
- **1998년 (대학생 시절)**: 아르바이트 돈으로 **50만 엔** 시작. 『금-rich 아버지 가난한 아버지』 읽고 투자 시작. 첫 종목은 **세가사미 홀딩스 (구 사미, 6460)**.&lt;br /&gt;
- **2000년**: 세가 주식 성공으로 자산 500만 엔.&lt;br /&gt;
- **2003~2005년**: 폭락한 **호주 금광주**에 **전 재산 집중 투자** → 자산 **1억 엔** 달성 (첫 억만장자).&lt;br /&gt;
- **2005년 이후**: 집중 투자에서 **바리ュー株 분산 투자**로 전환.&lt;br /&gt;
- **2008년 리먼 쇼크**: 바닥 근처에서 산 종목들이 회복되거나 TOB(공개매수)로 거의 손실 없이 버팀.&lt;br /&gt;
- **2009~2013년**: **아크랜드 서비스 홀딩스 (3085, 카츠야 등)** 지속 매수 → **테ンバ거(10배)**.  &lt;br /&gt;
  **아이풀 (8515)** 과지불금 문제로 타격받던 시점에 집중 투자 → 반년 만에 약 7배.&lt;br /&gt;
- **2013년**: 자산 **10억 엔** 달성, 연 배당금 3,000만 엔 이상 (의사 수입 초과). 38세에 한 번 FIRE했으나 지루해서 복귀.&lt;br /&gt;
- **2016년**: **히로세 통상 (7185)** 거의 유통주식 전량 매수 → 반년 만에 약 3배.&lt;br /&gt;
- **2020년대**: **분산 + 집중** 병행. 2024년경 자산 **50억 엔**, 현재 **80억 엔** 이상으로 성장.&lt;br /&gt;
- **2024~2026년 현재**: **HD한국조선해양 (009540)**에 **거의 전 재산 집중 투자**. 2025년 연초부터 큰 상승 (+96% 이상 보고, 한때 +110% 넘음). 조선업 사이클 회복 기대.&lt;br /&gt;
&lt;br /&gt;
### 주요 투자 방법: **시클리컬 밸류 (Cyclical Value) 주식 투자** + **분산 × 집중**&lt;br /&gt;
그의 핵심 철학은 **“안 싸서 사는 게 아니라, 가치가 있어서 사는 것”**입니다. 책 『50만원을 50억엔으로 불린 투자자의 아버지가 딸에게 주는 가르침』에서 **3가지 바리ュー 투자법**을 체계적으로 설명해요.&lt;br /&gt;
&lt;br /&gt;
1. **자산 바리ュー株 투자**  &lt;br /&gt;
   기업이 보유한 **순자산(자산 가치)**에 비해 주가가 너무 낮은 종목을 사는 방법.  &lt;br /&gt;
   “안전마진”이 크고, 최악의 경우에도 자산 가치로 보호받을 수 있음.&lt;br /&gt;
&lt;br /&gt;
2. **수익 바리ュー株 투자**  &lt;br /&gt;
   기업의 **미래 수익력(이익 창출 능력)**이 주가에 충분히 반영되지 않은 종목.  &lt;br /&gt;
   안정적으로 돈을 버는 기업을 저평가 상태에서 사는 전형적인 바리ュー.&lt;br /&gt;
&lt;br /&gt;
3. **시클리컬 바리ュー株 투자 (그의 메인 무기)**  &lt;br /&gt;
   - **경기 순환(시클리컬)**이 강한 업종(조선, 금광, 금융 등)에서 **불황·적자**일 때 저점 매수.&lt;br /&gt;
   - 경기가 회복되면 **이익이 급증**하면서 주가가 크게 오름 (테ンバ거 가능성 높음).&lt;br /&gt;
   - “적자 기업일수록 대박 날 가능성이 크다”는 게 핵심.  &lt;br /&gt;
   - 중요한 점: 단순히 적자라고 사는 게 아니라, **업황 회복 시 이익 폭발력**을 미리 읽어야 함.  &lt;br /&gt;
   - “4년 주기 경기 순환”을 고려해 타이밍을 잡음.&lt;br /&gt;
&lt;br /&gt;
**실전 운용 스타일**:&lt;br /&gt;
- 평소: **20종목 정도 분산** 투자 (방어적, 안정 위주).&lt;br /&gt;
- 확신 높을 때: **한두 종목에 과감한 집중 투자** (공격적).  &lt;br /&gt;
  예: 호주 금광주, 아크랜드, 아이풀, 히로세 통상, 최근 HD한국조선해양.&lt;br /&gt;
- 매수 기준: **저평가 + 업황 회복 신호** (재무제표, IR, 사계보 등 철저 분석).&lt;br /&gt;
- 매도: 고점에서 팔기보다는 **가치가 실현될 때까지** 오래 보유하는 경우 많음.&lt;br /&gt;
- 전체적으로 **“안 싸서 사는 게 아니라 가치가 있어서 산다”**는 철저한 바리ュー 투자.&lt;br /&gt;
&lt;br /&gt;
### 최근 상황 (2025~2026년)&lt;br /&gt;
- **HD한국조선해양**을 메인으로 거의 올인.  &lt;br /&gt;
  이유: 조선업황 회복 (LNG선, 컨테이너선 수요 증가, 중국 규제 수혜 등).  &lt;br /&gt;
  이익률이 12~25%까지 상승할 여력 있다고 판단.&lt;br /&gt;
- 암 투병 중에도 투자 계속하며, 책을 통해 **딸들에게 남기는 유산**으로 투자법을 정리.&lt;br /&gt;
&lt;br /&gt;
### 한 줄 요약&lt;br /&gt;
타-chan 씨는 **일반적인 바리ュー 투자**를 기반으로 하되, **경기 순환 업종의 불황 저점에서 집중 매수**해 큰 수익을 내는 **시클리컬 밸류 전문가**예요.  &lt;br /&gt;
위험을 감수한 집중 투자와 철저한 분석이 결합된 스타일로, “일하면서도 큰돈을 불릴 수 있다”는 메시지를 강조합니다.&lt;br /&gt;
&lt;br /&gt;
**주의**: 이는 과거 경험과 공개된 내용 요약일 뿐, 현재 추천이 아닙니다.  &lt;br /&gt;
투자는 본인 판단과 책임으로 하시고, 책을 직접 읽어보시는 걸 강력 추천해요 (실전적이고 재현성 높은 내용이 많아요).&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%B6%80%EC%9E%90%EC%95%84%EB%B9%A0_%ED%83%80%EC%A7%B1_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2370</id>
		<title>부자아빠 타짱 투자법</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%B6%80%EC%9E%90%EC%95%84%EB%B9%A0_%ED%83%80%EC%A7%B1_%ED%88%AC%EC%9E%90%EB%B2%95&amp;diff=2370"/>
		<updated>2026-04-02T09:40:42Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: @yhdgj675 (たーちゃん＠バブルおじさん) 씨가 **X(트위터)에 직접 “이 주식을 사라!”라고 적극적으로 추천**한 글은 거의 없어요.   대신 본인의 실제 투자 경험, 과거 보유·집중 투자했던 종목, 인터뷰·책 내용을 바탕으로 **연도별로 주요 투자/주목했던 주식**을 정리해 드릴게요. (책과 인터뷰, 과거 포스트 기반)  ### 초기 (학생 시절 ~ 2000년대 초반) - **1998년경**: **...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;@yhdgj675 (たーちゃん＠バブルおじさん) 씨가 **X(트위터)에 직접 “이 주식을 사라!”라고 적극적으로 추천**한 글은 거의 없어요. &lt;br /&gt;
&lt;br /&gt;
대신 본인의 실제 투자 경험, 과거 보유·집중 투자했던 종목, 인터뷰·책 내용을 바탕으로 **연도별로 주요 투자/주목했던 주식**을 정리해 드릴게요. (책과 인터뷰, 과거 포스트 기반)&lt;br /&gt;
&lt;br /&gt;
### 초기 (학생 시절 ~ 2000년대 초반)&lt;br /&gt;
- **1998년경**: **세가사미 홀딩스 (구 사미, 6460)**  &lt;br /&gt;
  첫 투자 종목. 게임 관련으로 관심을 가지고 매수.&lt;br /&gt;
- **2003~2005년경**: **호주 금광주** (구체적 종목은 공개 적음)  &lt;br /&gt;
  전 재산 집중 투자로 테ンバ거(10배)급 성공. 29세頃에 억만장자 달성의 계기.&lt;br /&gt;
&lt;br /&gt;
### 2010년대&lt;br /&gt;
- **2009~2013년경**: **아크랜드 서비스 홀딩스 (3085, 카츠야 등)**  &lt;br /&gt;
  지속 매수 → **테ンバ거(10배)** 달성. 주요 종목으로 키움.&lt;br /&gt;
- **2012년경**: **아이풀 (8515)**  &lt;br /&gt;
  과지불금 문제로 타격받던 시점에 집중 투자 → 약 6개월 만에 7배 상승.&lt;br /&gt;
- **2016년경**: **히로세 통상 (7185)**  &lt;br /&gt;
  동종업계 GMO보다 저평가된 점을 보고 거의 유통주식 전량 매수 → 6개월 만에 약 3배.&lt;br /&gt;
&lt;br /&gt;
이 시기부터 “평소에는 분산 투자, 확신 높은 시클리컬(경기 순환) 종목에서는 집중 투자” 스타일이 자리 잡음.&lt;br /&gt;
&lt;br /&gt;
### 2020년대 (최근 주요 종목)&lt;br /&gt;
- **2023~2024년 상반기**: **나무라 조선소 (명촌 조선소)**  &lt;br /&gt;
  조선 버블 관련으로 집중 투자.&lt;br /&gt;
- **2024년 하반기 ~ 2025~2026년 현재**: **HD한국조선해양 (009540, 한국 주식)**  &lt;br /&gt;
  “올해 상반기는 나무라 조선소, 하반기는 HD한국조선해양을 메인으로”, “현재는 거의 전 재산을 한 종목에 집중”이라고 직접 언급.  &lt;br /&gt;
  2025년 연초부터 큰 상승을 보이며 +96%~+115%대 성과 보고 (배당·출금 포함).&lt;br /&gt;
&lt;br /&gt;
### 최근 인터뷰·책에서 언급·주목한 섹터·종목 (2025년)&lt;br /&gt;
- **조선업**: HD한국조선해양 (계속 메인)&lt;br /&gt;
- **토목·유리·공구 관련**: 우크라이나 재건 수요 기대&lt;br /&gt;
- **업무용 냉장고**: 요코레이 (밸류에이션 매력적, 과점화 기대)&lt;br /&gt;
- 기타 과거 언급: **고베물산 (업무 슈퍼, 業スー)** – 2006년 상장 전 매수 후 큰 성공 (판매는 일찍 했다고 후회)&lt;br /&gt;
&lt;br /&gt;
### 그의 투자 철학 요약&lt;br /&gt;
- **시클리컬 밸류 (Cyclical Value)**: 경기 순환으로 저평가된 업종·기업을 바닥에서 사서 회복 때까지 기다리는 스타일.&lt;br /&gt;
- 평소 20종목 정도 분산 + 확신 높은 종목에 과감한 집중.&lt;br /&gt;
- X 게시물에서는 구체적 “추천”보다는 본인 보유 상황과 생각을 공유하는 경우가 대부분입니다. 더 구체적인 추천은 TwitCasting(라이브)이나 인터뷰에서 나오는 편.&lt;br /&gt;
&lt;br /&gt;
**주의사항**  &lt;br /&gt;
- 이는 과거 실제 투자·언급 사례를 정리한 것이지, 현재 추천이 아닙니다.  &lt;br /&gt;
- 투자 결정은 본인 책임이며, 과거 성과가 미래를 보장하지 않습니다.&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2369</id>
		<title>시클리컬 주요 산업별 대장주</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2369"/>
		<updated>2026-04-02T09:34:37Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 시클리컬(경기순환) 주식 산업별 분류 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬(경기순환) 주식 산업별 분류 ==&lt;br /&gt;
&lt;br /&gt;
=== 🏗️ 소재 / 철강 / 금속 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|철강  || POSCO홀딩스, 현대제철|| Nucor, US Steel &lt;br /&gt;
|-&lt;br /&gt;
|비철금속|| 고려아연, 풍산      || Freeport-McMoRan&lt;br /&gt;
|-&lt;br /&gt;
|화학  || LG화학, 롯데케미칼   ||BASF, Dow       &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ⚓ 조선 / 해운 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|조선 ||HD현대중공업, 삼성중공업, 한화오션||    &lt;br /&gt;
|-&lt;br /&gt;
|해운 ||HMM, 장금상선           ||Maersk, MSC&lt;br /&gt;
|-&lt;br /&gt;
|벌크선||                    ||COSCO(중국)  &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 🛢️ 에너지 / 정유 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|정유   ||SK이노베이션, GS칼텍스(비상장)||ExxonMobil, Chevron      &lt;br /&gt;
|-&lt;br /&gt;
|유전서비스||                   ||Schlumberger, Halliburton&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 🏠 건설 / 부동산 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|건설 ||삼성물산, 현대건설, GS건설||Lennar, D.R. Horton&lt;br /&gt;
|-&lt;br /&gt;
|시멘트||쌍용C&amp;amp;E, 한일시멘트    ||Martin Marietta    &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 🚗 자동차 / 부품 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|완성차||현대차, 기아    ||GM, Ford, Toyota&lt;br /&gt;
|-&lt;br /&gt;
|부품 ||현대모비스, HL만도||BorgWarner      &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ✈️ 항공 / 여행 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|항공    ||대한항공, 아시아나||Delta, United&lt;br /&gt;
|-&lt;br /&gt;
|호텔/카지노||파라다이스, GKL||MGM, Wynn    &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 🖥️ 반도체 / 디스플레이 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|메모리  ||삼성전자, SK하이닉스||Micron&lt;br /&gt;
|-&lt;br /&gt;
|디스플레이|LG디스플레이     ||      &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== 🔋 배터리 / 소재 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|배터리  ||삼성SDI, LG에너지솔루션, SK온||CATL(중국) &lt;br /&gt;
|-&lt;br /&gt;
|배터리소재||에코프로, 포스코퓨처엠        ||Albemarle&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* 📌 시클리컬 투자 핵심 포인트&lt;br /&gt;
&lt;br /&gt;
* 경기 사이클과 매수 타이밍&lt;br /&gt;
** 바닥 매수 원칙 : 업황 최악일 때 → PBR 낮고 적자일 때가 매수 기회&lt;br /&gt;
** PER 함정 주의 : 호황기엔 PER 낮아 보여도 이미 고점일 수 있음&lt;br /&gt;
** 재고 사이클 확인 필수 (반도체, 화학 특히 중요)&lt;br /&gt;
업종별 선행지표&lt;br /&gt;
**	철강/조선 → 발틱운임지수(BDI), 중국 PMI&lt;br /&gt;
**	반도체 → DRAM 현물가, 재고 수준&lt;br /&gt;
**	정유 → WTI/브렌트유 가격, 정제마진&lt;br /&gt;
**	화학 → 에틸렌 스프레드​​​​​​​​​​​​​​​​&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2368</id>
		<title>시클리컬 주요 산업별 대장주</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2368"/>
		<updated>2026-04-02T09:25:37Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 🏗️ 소재 / 철강 / 금속 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬(경기순환) 주식 산업별 분류 ==&lt;br /&gt;
&lt;br /&gt;
=== 🏗️ 소재 / 철강 / 금속 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|철강  || POSCO홀딩스, 현대제철|| Nucor, US Steel &lt;br /&gt;
|-&lt;br /&gt;
|비철금속|| 고려아연, 풍산      || Freeport-McMoRan&lt;br /&gt;
|-&lt;br /&gt;
|화학  || LG화학, 롯데케미칼   ||BASF, Dow       &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ⚓ 조선 / 해운 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국                  |글로벌        |&lt;br /&gt;
|---|--------------------|-----------|&lt;br /&gt;
|조선 |HD현대중공업, 삼성중공업, 한화오션|           |&lt;br /&gt;
|해운 |HMM, 장금상선           |Maersk, MSC|&lt;br /&gt;
|벌크선|                    |COSCO(중국)  |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🛢️ 에너지 / 정유 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                 |미국                       |&lt;br /&gt;
|-----|-------------------|-------------------------|&lt;br /&gt;
|정유   |SK이노베이션, GS칼텍스(비상장)|ExxonMobil, Chevron      |&lt;br /&gt;
|유전서비스|                   |Schlumberger, Halliburton|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🏠 건설 / 부동산 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국              |미국                 |&lt;br /&gt;
|---|----------------|-------------------|&lt;br /&gt;
|건설 |삼성물산, 현대건설, GS건설|Lennar, D.R. Horton|&lt;br /&gt;
|시멘트|쌍용C&amp;amp;E, 한일시멘트    |Martin Marietta    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🚗 자동차 / 부품 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국         |글로벌             |&lt;br /&gt;
|---|-----------|----------------|&lt;br /&gt;
|완성차|현대차, 기아    |GM, Ford, Toyota|&lt;br /&gt;
|부품 |현대모비스, HL만도|BorgWarner      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ✈️ 항공 / 여행 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분    |한국        |미국           |&lt;br /&gt;
|------|----------|-------------|&lt;br /&gt;
|항공    |대한항공, 아시아나|Delta, United|&lt;br /&gt;
|호텔/카지노|파라다이스, GKL|MGM, Wynn    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🖥️ 반도체 / 디스플레이 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국          |미국    |&lt;br /&gt;
|-----|------------|------|&lt;br /&gt;
|메모리  |삼성전자, SK하이닉스|Micron|&lt;br /&gt;
|디스플레이|LG디스플레이     |      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🔋 배터리 / 소재 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                  |글로벌      |&lt;br /&gt;
|-----|--------------------|---------|&lt;br /&gt;
|배터리  |삼성SDI, LG에너지솔루션, SK온|CATL(중국) |&lt;br /&gt;
|배터리소재|에코프로, 포스코퓨처엠        |Albemarle|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* 📌 시클리컬 투자 핵심 포인트&lt;br /&gt;
&lt;br /&gt;
* 경기 사이클과 매수 타이밍&lt;br /&gt;
** 바닥 매수 원칙 : 업황 최악일 때 → PBR 낮고 적자일 때가 매수 기회&lt;br /&gt;
** PER 함정 주의 : 호황기엔 PER 낮아 보여도 이미 고점일 수 있음&lt;br /&gt;
** 재고 사이클 확인 필수 (반도체, 화학 특히 중요)&lt;br /&gt;
업종별 선행지표&lt;br /&gt;
**	철강/조선 → 발틱운임지수(BDI), 중국 PMI&lt;br /&gt;
**	반도체 → DRAM 현물가, 재고 수준&lt;br /&gt;
**	정유 → WTI/브렌트유 가격, 정제마진&lt;br /&gt;
**	화학 → 에틸렌 스프레드​​​​​​​​​​​​​​​​&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2367</id>
		<title>시클리컬 주요 산업별 대장주</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2367"/>
		<updated>2026-04-02T09:24:58Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 🏗️ 소재 / 철강 / 금속 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬(경기순환) 주식 산업별 분류 ==&lt;br /&gt;
&lt;br /&gt;
=== 🏗️ 소재 / 철강 / 금속 ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+ 캡션 텍스트&lt;br /&gt;
|-&lt;br /&gt;
! 표 제목 !! 표 제목 !! 표 제목&lt;br /&gt;
|-&lt;br /&gt;
| 예시 || 예시 || 예시&lt;br /&gt;
|-&lt;br /&gt;
| 예시 || 예시 || 예시&lt;br /&gt;
|-&lt;br /&gt;
| 예시 || 예시 || 예시&lt;br /&gt;
|}&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! 구분 !! 한국!! 미국/글로벌&lt;br /&gt;
|-&lt;br /&gt;
|철강  || POSCO홀딩스, 현대제철|| Nucor, US Steel &lt;br /&gt;
|-&lt;br /&gt;
|비철금속|| 고려아연, 풍산      || Freeport-McMoRan&lt;br /&gt;
|-&lt;br /&gt;
|화학  || LG화학, 롯데케미칼   ||BASF, Dow       &lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== ⚓ 조선 / 해운 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국                  |글로벌        |&lt;br /&gt;
|---|--------------------|-----------|&lt;br /&gt;
|조선 |HD현대중공업, 삼성중공업, 한화오션|           |&lt;br /&gt;
|해운 |HMM, 장금상선           |Maersk, MSC|&lt;br /&gt;
|벌크선|                    |COSCO(중국)  |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🛢️ 에너지 / 정유 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                 |미국                       |&lt;br /&gt;
|-----|-------------------|-------------------------|&lt;br /&gt;
|정유   |SK이노베이션, GS칼텍스(비상장)|ExxonMobil, Chevron      |&lt;br /&gt;
|유전서비스|                   |Schlumberger, Halliburton|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🏠 건설 / 부동산 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국              |미국                 |&lt;br /&gt;
|---|----------------|-------------------|&lt;br /&gt;
|건설 |삼성물산, 현대건설, GS건설|Lennar, D.R. Horton|&lt;br /&gt;
|시멘트|쌍용C&amp;amp;E, 한일시멘트    |Martin Marietta    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🚗 자동차 / 부품 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국         |글로벌             |&lt;br /&gt;
|---|-----------|----------------|&lt;br /&gt;
|완성차|현대차, 기아    |GM, Ford, Toyota|&lt;br /&gt;
|부품 |현대모비스, HL만도|BorgWarner      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ✈️ 항공 / 여행 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분    |한국        |미국           |&lt;br /&gt;
|------|----------|-------------|&lt;br /&gt;
|항공    |대한항공, 아시아나|Delta, United|&lt;br /&gt;
|호텔/카지노|파라다이스, GKL|MGM, Wynn    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🖥️ 반도체 / 디스플레이 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국          |미국    |&lt;br /&gt;
|-----|------------|------|&lt;br /&gt;
|메모리  |삼성전자, SK하이닉스|Micron|&lt;br /&gt;
|디스플레이|LG디스플레이     |      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🔋 배터리 / 소재 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                  |글로벌      |&lt;br /&gt;
|-----|--------------------|---------|&lt;br /&gt;
|배터리  |삼성SDI, LG에너지솔루션, SK온|CATL(중국) |&lt;br /&gt;
|배터리소재|에코프로, 포스코퓨처엠        |Albemarle|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* 📌 시클리컬 투자 핵심 포인트&lt;br /&gt;
&lt;br /&gt;
* 경기 사이클과 매수 타이밍&lt;br /&gt;
** 바닥 매수 원칙 : 업황 최악일 때 → PBR 낮고 적자일 때가 매수 기회&lt;br /&gt;
** PER 함정 주의 : 호황기엔 PER 낮아 보여도 이미 고점일 수 있음&lt;br /&gt;
** 재고 사이클 확인 필수 (반도체, 화학 특히 중요)&lt;br /&gt;
업종별 선행지표&lt;br /&gt;
**	철강/조선 → 발틱운임지수(BDI), 중국 PMI&lt;br /&gt;
**	반도체 → DRAM 현물가, 재고 수준&lt;br /&gt;
**	정유 → WTI/브렌트유 가격, 정제마진&lt;br /&gt;
**	화학 → 에틸렌 스프레드​​​​​​​​​​​​​​​​&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2366</id>
		<title>시클리컬 주요 산업별 대장주</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2366"/>
		<updated>2026-04-02T09:20:32Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬(경기순환) 주식 산업별 분류 ==&lt;br /&gt;
&lt;br /&gt;
=== 🏗️ 소재 / 철강 / 금속 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분  |한국            |미국/글로벌          |&lt;br /&gt;
|----|--------------|----------------|&lt;br /&gt;
|철강  |POSCO홀딩스, 현대제철|Nucor, US Steel |&lt;br /&gt;
|비철금속|고려아연, 풍산      |Freeport-McMoRan|&lt;br /&gt;
|화학  |LG화학, 롯데케미칼   |BASF, Dow       |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ⚓ 조선 / 해운 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국                  |글로벌        |&lt;br /&gt;
|---|--------------------|-----------|&lt;br /&gt;
|조선 |HD현대중공업, 삼성중공업, 한화오션|           |&lt;br /&gt;
|해운 |HMM, 장금상선           |Maersk, MSC|&lt;br /&gt;
|벌크선|                    |COSCO(중국)  |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🛢️ 에너지 / 정유 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                 |미국                       |&lt;br /&gt;
|-----|-------------------|-------------------------|&lt;br /&gt;
|정유   |SK이노베이션, GS칼텍스(비상장)|ExxonMobil, Chevron      |&lt;br /&gt;
|유전서비스|                   |Schlumberger, Halliburton|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🏠 건설 / 부동산 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국              |미국                 |&lt;br /&gt;
|---|----------------|-------------------|&lt;br /&gt;
|건설 |삼성물산, 현대건설, GS건설|Lennar, D.R. Horton|&lt;br /&gt;
|시멘트|쌍용C&amp;amp;E, 한일시멘트    |Martin Marietta    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🚗 자동차 / 부품 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국         |글로벌             |&lt;br /&gt;
|---|-----------|----------------|&lt;br /&gt;
|완성차|현대차, 기아    |GM, Ford, Toyota|&lt;br /&gt;
|부품 |현대모비스, HL만도|BorgWarner      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ✈️ 항공 / 여행 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분    |한국        |미국           |&lt;br /&gt;
|------|----------|-------------|&lt;br /&gt;
|항공    |대한항공, 아시아나|Delta, United|&lt;br /&gt;
|호텔/카지노|파라다이스, GKL|MGM, Wynn    |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🖥️ 반도체 / 디스플레이 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국          |미국    |&lt;br /&gt;
|-----|------------|------|&lt;br /&gt;
|메모리  |삼성전자, SK하이닉스|Micron|&lt;br /&gt;
|디스플레이|LG디스플레이     |      |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 🔋 배터리 / 소재 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분   |한국                  |글로벌      |&lt;br /&gt;
|-----|--------------------|---------|&lt;br /&gt;
|배터리  |삼성SDI, LG에너지솔루션, SK온|CATL(중국) |&lt;br /&gt;
|배터리소재|에코프로, 포스코퓨처엠        |Albemarle|&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* 📌 시클리컬 투자 핵심 포인트&lt;br /&gt;
&lt;br /&gt;
* 경기 사이클과 매수 타이밍&lt;br /&gt;
** 바닥 매수 원칙 : 업황 최악일 때 → PBR 낮고 적자일 때가 매수 기회&lt;br /&gt;
** PER 함정 주의 : 호황기엔 PER 낮아 보여도 이미 고점일 수 있음&lt;br /&gt;
** 재고 사이클 확인 필수 (반도체, 화학 특히 중요)&lt;br /&gt;
업종별 선행지표&lt;br /&gt;
**	철강/조선 → 발틱운임지수(BDI), 중국 PMI&lt;br /&gt;
**	반도체 → DRAM 현물가, 재고 수준&lt;br /&gt;
**	정유 → WTI/브렌트유 가격, 정제마진&lt;br /&gt;
**	화학 → 에틸렌 스프레드​​​​​​​​​​​​​​​​&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2365</id>
		<title>시클리컬 주요 산업별 대장주</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EC%8B%9C%ED%81%B4%EB%A6%AC%EC%BB%AC_%EC%A3%BC%EC%9A%94_%EC%82%B0%EC%97%85%EB%B3%84_%EB%8C%80%EC%9E%A5%EC%A3%BC&amp;diff=2365"/>
		<updated>2026-04-02T09:17:02Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: 새 문서: == 시클리컬(경기순환) 주식 산업별 분류 ==  === 🏗️ 소재 / 철강 / 금속 === &amp;lt;source lang=bash&amp;gt; |구분  |한국            |미국/글로벌          | |----|--------------|----------------| |철강  |POSCO홀딩스, 현대제철|Nucor, US Steel | |비철금속|고려아연, 풍산      |Freeport-McMoRan| |화학  |LG화학, 롯데케미칼   |BASF, Dow       | &amp;lt;/source&amp;gt;  === ⚓ 조선 / 해운 === &amp;lt;source lang=bash&amp;gt; |구분 |한국                  |글...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== 시클리컬(경기순환) 주식 산업별 분류 ==&lt;br /&gt;
&lt;br /&gt;
=== 🏗️ 소재 / 철강 / 금속 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분  |한국            |미국/글로벌          |&lt;br /&gt;
|----|--------------|----------------|&lt;br /&gt;
|철강  |POSCO홀딩스, 현대제철|Nucor, US Steel |&lt;br /&gt;
|비철금속|고려아연, 풍산      |Freeport-McMoRan|&lt;br /&gt;
|화학  |LG화학, 롯데케미칼   |BASF, Dow       |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== ⚓ 조선 / 해운 ===&lt;br /&gt;
&amp;lt;source lang=bash&amp;gt;&lt;br /&gt;
|구분 |한국                  |글로벌        |&lt;br /&gt;
|---|--------------------|-----------|&lt;br /&gt;
|조선 |HD현대중공업, 삼성중공업, 한화오션|           |&lt;br /&gt;
|해운 |HMM, 장금상선           |Maersk, MSC|&lt;br /&gt;
|벌크선|                    |COSCO(중국)  |&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
🛢️ 에너지 / 정유&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분   |한국                 |미국                       |&lt;br /&gt;
|-----|-------------------|-------------------------|&lt;br /&gt;
|정유   |SK이노베이션, GS칼텍스(비상장)|ExxonMobil, Chevron      |&lt;br /&gt;
|유전서비스|                   |Schlumberger, Halliburton|&lt;br /&gt;
&lt;br /&gt;
🏠 건설 / 부동산&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분 |한국              |미국                 |&lt;br /&gt;
|---|----------------|-------------------|&lt;br /&gt;
|건설 |삼성물산, 현대건설, GS건설|Lennar, D.R. Horton|&lt;br /&gt;
|시멘트|쌍용C&amp;amp;E, 한일시멘트    |Martin Marietta    |&lt;br /&gt;
&lt;br /&gt;
🚗 자동차 / 부품&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분 |한국         |글로벌             |&lt;br /&gt;
|---|-----------|----------------|&lt;br /&gt;
|완성차|현대차, 기아    |GM, Ford, Toyota|&lt;br /&gt;
|부품 |현대모비스, HL만도|BorgWarner      |&lt;br /&gt;
&lt;br /&gt;
✈️ 항공 / 여행&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분    |한국        |미국           |&lt;br /&gt;
|------|----------|-------------|&lt;br /&gt;
|항공    |대한항공, 아시아나|Delta, United|&lt;br /&gt;
|호텔/카지노|파라다이스, GKL|MGM, Wynn    |&lt;br /&gt;
&lt;br /&gt;
🖥️ 반도체 / 디스플레이&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분   |한국          |미국    |&lt;br /&gt;
|-----|------------|------|&lt;br /&gt;
|메모리  |삼성전자, SK하이닉스|Micron|&lt;br /&gt;
|디스플레이|LG디스플레이     |      |&lt;br /&gt;
&lt;br /&gt;
🔋 배터리 / 소재&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|구분   |한국                  |글로벌      |&lt;br /&gt;
|-----|--------------------|---------|&lt;br /&gt;
|배터리  |삼성SDI, LG에너지솔루션, SK온|CATL(중국) |&lt;br /&gt;
|배터리소재|에코프로, 포스코퓨처엠        |Albemarle|&lt;br /&gt;
&lt;br /&gt;
📌 시클리컬 투자 핵심 포인트&lt;br /&gt;
경기 사이클과 매수 타이밍&lt;br /&gt;
	∙	바닥 매수 원칙 : 업황 최악일 때 → PBR 낮고 적자일 때가 매수 기회&lt;br /&gt;
	∙	PER 함정 주의 : 호황기엔 PER 낮아 보여도 이미 고점일 수 있음&lt;br /&gt;
	∙	재고 사이클 확인 필수 (반도체, 화학 특히 중요)&lt;br /&gt;
업종별 선행지표&lt;br /&gt;
	∙	철강/조선 → 발틱운임지수(BDI), 중국 PMI&lt;br /&gt;
	∙	반도체 → DRAM 현물가, 재고 수준&lt;br /&gt;
	∙	정유 → WTI/브렌트유 가격, 정제마진&lt;br /&gt;
	∙	화학 → 에틸렌 스프레드​​​​​​​​​​​​​​​​&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%8C%80%EB%AC%B8&amp;diff=2364</id>
		<title>대문</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%8C%80%EB%AC%B8&amp;diff=2364"/>
		<updated>2026-04-02T09:15:23Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 주식 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__notoc__&lt;br /&gt;
{{#metadata:key=데브카페}}&lt;br /&gt;
{{#metadata:key=파이썬}}&lt;br /&gt;
{{#metadata:key=프로그램개발}}&lt;br /&gt;
{{#metadata:key=autoit}}&lt;br /&gt;
{{#metadata:key=미디어위키}}&lt;br /&gt;
&lt;br /&gt;
= Welcome to DEV CAFE =&lt;br /&gt;
* {{CURRENTYEAR}}.{{CURRENTMONTHNAME}}.{{CURRENTDAY}}({{CURRENTDAYNAME}})  , 이세상 모든 개발에 대한 이야기 {{SERVER}}&lt;br /&gt;
&amp;lt;p sizes=&amp;quot;(max-width: 600px) 480px,800px&amp;quot;&amp;gt;&lt;br /&gt;
https://devcafe.co.kr/w/images/devcafe_main3.jpeg&lt;br /&gt;
&amp;lt;/p&amp;gt;&lt;br /&gt;
{{틀:타이틀 라운드&lt;br /&gt;
|제목=[[:Category:python|파이썬]]: {{PAGESINCATEGORY: python}} 건 ,  [[:Category:미디어위키]] : {{PAGESINCATEGORY: 미디어위키}} 건 ,  [[:Category:autoit|오토잇]]: {{PAGESINCATEGORY: autoit}} 건,  [[:Category:linux|LINUX]]: {{PAGESINCATEGORY: linux}} 건, [[:Category:음악]]: {{PAGESINCATEGORY: 음악}} 건&lt;br /&gt;
|아이콘=emoji_objects&lt;br /&gt;
}}&lt;br /&gt;
== IT/기술 ==&lt;br /&gt;
=== [[파이썬|파이썬 프로그래밍]] ===&lt;br /&gt;
==== 파이썬 설치 ====&lt;br /&gt;
# [[파이썬 설치]]&lt;br /&gt;
# [[Python 가상환경|파이썬 가상화]]&lt;br /&gt;
# [[pip 이용 파이썬 라이브러리 설치|파이썬 라이브러리 설치]]&lt;br /&gt;
# [[pip 이용 파이썬 라이브러리 설치#설치 패키지 추출|설치된 파이썬 라이브러리 추출하기]]&lt;br /&gt;
# [[Python whl 설치|.whl 파일 다운로드 설치법]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 기초 ====&lt;br /&gt;
{{알림&lt;br /&gt;
|내용=&#039;&#039;&#039;[[:category:Python|Python]] Foundation&#039;&#039;&#039; &lt;br /&gt;
# [[PYTHON 데이터형|파이썬 데이터형]]&lt;br /&gt;
# [[튜플 리스트 딕셔너리 차이점,특징,사용예시|튜플 리스트 딕셔너리]]&lt;br /&gt;
# [[파이썬 명명규칙]]&lt;br /&gt;
# [[파이썬 클래스]]&lt;br /&gt;
|아이콘이름=accessibility&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 고급 ====&lt;br /&gt;
:# [[파이썬 개발 추천기능 TOP10|파이썬 고급 기능 TOP 10]]&lt;br /&gt;
:# [[Python Cheat Sheet|파이썬 치트 쉬트 ]]&lt;br /&gt;
:# [[파이썬 config parser|파이썬 .ini 환경파일에 읽고쓰기]]&lt;br /&gt;
:# [[파이썬 로깅]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 응용 ====&lt;br /&gt;
# [[PyQt|파이썬 GUI 프로그램 개발]]&lt;br /&gt;
# [[Process memory share|쉐어 메모리 접근]]&lt;br /&gt;
# [[Python Cheat Sheet#Parallel Processing|병렬처리 프로세싱]]&lt;br /&gt;
# [[멀티프로세싱]]&lt;br /&gt;
# [[파이썬 스케줄러 모듈]]&lt;br /&gt;
# [[파이썬 장고]]&lt;br /&gt;
# [[flask]]&lt;br /&gt;
# [[Json 이용 키-값 추가 수정 삭제 방법|JSON 키값 수정/삭]]&lt;br /&gt;
# [[파이썬 mybatis xml sql 파싱 방법|XML 파싱(Mybatis)]]&lt;br /&gt;
# [[스케줄러 만들기]]&lt;br /&gt;
&lt;br /&gt;
==== 판다스(Pandas) 기초 ====&lt;br /&gt;
{{알림&lt;br /&gt;
|아이콘이름=apps &lt;br /&gt;
|내용= &#039;&#039;&#039;[[:category:pandas|PANDAS]] Foundation&#039;&#039;&#039;&lt;br /&gt;
# [[pandas|판다스(pandas) 기초]]&lt;br /&gt;
# [https://pandas.pydata.org/docs/ pandas 공식문서]&lt;br /&gt;
# [https://realpython.com/pandas-dataframe/ 리얼 판다스 튜토리얼]&lt;br /&gt;
# [https://www.geeksforgeeks.org/pandas-tutorial/ 그릭스 판다스 튜토리얼]&lt;br /&gt;
# [https://towardsdatascience.com/?gi=196f5e0ef243 투왈즈 튜터리얼]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== 판다스 응용 ====&lt;br /&gt;
# [[PANDAS 와 SQL 쿼리]]&lt;br /&gt;
# [[Pandas mysql insert]]&lt;br /&gt;
# [[Pandas 엑셀]]&lt;br /&gt;
&lt;br /&gt;
==== [[:category:streamlit|스트림릿 (Streamlit)]] 기초 ====&lt;br /&gt;
# [[Streamlit 기초]]&lt;br /&gt;
# [[Streamlit 개발 사이트|Streamlit 참고 사이트]]&lt;br /&gt;
# [[Streamlit Extention]]&lt;br /&gt;
# [[Streamlit 개발|Streamlit 프로그램]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 응용프로그램 개발 ====&lt;br /&gt;
# [[주식 자동매매 프로그램 개발]]&lt;br /&gt;
## [[대신증권 API 활용]]&lt;br /&gt;
## [[키움증권 api 매수매도|키움증권 API 활용]]&lt;br /&gt;
# [[개인 PC 텍스트 검색 채봇]]&lt;br /&gt;
## [[개인 PC 텍스트 검색 챗봇 웹 화면]]&lt;br /&gt;
# [[SQL 정리 툴|SQL Formater(정리)]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[:category:java|JAVA 프로그래밍]] ===&lt;br /&gt;
==== [[Java openjdk 설치|JAVA 설치]] ====&lt;br /&gt;
==== [[JAVA 기초]] ====&lt;br /&gt;
# [[JAVA 명명 규칙]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[:category:autoit|Autotit 프로그래밍]] ===&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|아이콘이름=adb&lt;br /&gt;
|내용 = &amp;lt;big&amp;gt;오토잇 프로그래밍 설치 및 기초 문법&amp;lt;/big&amp;gt;&lt;br /&gt;
# [[Auotit 설치]]&lt;br /&gt;
# [[Autoit 기초 문법]]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== [[Autoit gui|Autoit GUI]] ===&lt;br /&gt;
:# [[Autoit GUI 컨트롤 스타일|GUI 컨트롤 스타일]]&lt;br /&gt;
:# [[Handle의 class instance 값 구하는 방법|GUI 컨트롤 핸들 찾기]]&lt;br /&gt;
:# [[Autoit 멀티 윈도우 컨트롤]]&lt;br /&gt;
:# [[리스트뷰(그리드) 생성]]&lt;br /&gt;
&lt;br /&gt;
=== [[Autoit 프로그램 개발]] ===&lt;br /&gt;
:# [[Autoit sqlite|SQLite 디비 연결]]&lt;br /&gt;
:# [[Autoit oracle connect|ORACLE 디비 연결]]&lt;br /&gt;
:# [[Autoit 키보드 후킹|키보드 후킹]]&lt;br /&gt;
:# [[Autoit 자동 업그레이드 프로그램 개발|자동 업데이트 프로그램 개발]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[에디터 활용]] ===&lt;br /&gt;
==== [[:category:vscode|VS 코드(VS Code)]] ====&lt;br /&gt;
# [[Vscode 단축키]]&lt;br /&gt;
&lt;br /&gt;
==== [[:Category:서브라임|서브라임 (sublime)]] ====&lt;br /&gt;
# [[서브라임 sublime 단축키|Sublime 단축키]]&lt;br /&gt;
# [[Sublime plugin 생성 방법|Sublime 플러그인 만들기]]&lt;br /&gt;
# [[sublime emmet]]&lt;br /&gt;
==== [[Vi 명령어|vi 편집기]] ====&lt;br /&gt;
# [[Vi mode - ksh bash zsh]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 미디어위키 사용법 ==&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|아이콘이름=school&lt;br /&gt;
|내용= &#039;&#039;&#039;미디어 위키 초보자 가이드&#039;&#039;&#039; &lt;br /&gt;
# [[미디어위키 기본 사용법]] &lt;br /&gt;
# [[환경세팅 파일|환경설정(LocalSettings.php)]]&lt;br /&gt;
# [[스킨(테마) 추가/변경]]&lt;br /&gt;
# [[미디어위키 확장 기능]]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== [[미디어위키 예약어 와 변수]] ===&lt;br /&gt;
==== [[미디어위키 예약어 와 변수#동작어_(Behavior_Switches)|미디어위키 동작 예약어]] ====&lt;br /&gt;
==== [[미디어위키 예약어 와 변수#변수_(Variables)|미디어위키 변수 예약어]] ====&lt;br /&gt;
&lt;br /&gt;
=== [[미디어위키 기본 사용법]] ===&lt;br /&gt;
* [[미디어위키 기본 사용법#새글 쓰기|새글 쓰기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#대문 수정|첫페이지(대문) 수정]]&lt;br /&gt;
* [[미디어위키 기본 사용법#문단(Section)|목차 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#표(wiki table) 만들기|표 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#틀(Template) 만들기|틀 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#양식(Form) 만들기|폼 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#카테고리 만들기|카테고리 만들기]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 미디어 위키 환경설정 ===&lt;br /&gt;
* [[스킨(테마) 추가/변경]]&lt;br /&gt;
* [[미디어위키 기본 사용법#대문 수정하기|대문 타이틀 글씨 숨기기]]&lt;br /&gt;
* [[특수 명령어]]&lt;br /&gt;
* [[환경세팅 파일|환경세팅 파일(LocalSettings.php)]]&lt;br /&gt;
* [[나눔고딕 웹폰트 설정하기]]&lt;br /&gt;
* [[반응형 이미지 사이즈 조절 방법]]&lt;br /&gt;
* [[스니펫-목차에 순번 붙이기]]&lt;br /&gt;
&lt;br /&gt;
=== 미디어위키 확장 프로그램 가이드===&lt;br /&gt;
==== 페이지 관련 ====&lt;br /&gt;
* [[Syntax Highlight|문법 하이라이트]]&lt;br /&gt;
* [[목차에 순번 붙이기|목차에 순번 붙이기(확장기능 설치)]]&lt;br /&gt;
** [[스니펫-목차에 순번 붙이기 |목차에 순번 붙이기(JS/CSS 이용)]] / [https://www.mediawiki.org/wiki/Manual:Table_of_contents#Auto-numbering 원본페이지 이동]&lt;br /&gt;
* [[페이지에 탭 만들기]]&lt;br /&gt;
* [[관련글 보기 기능 추가 ]]&lt;br /&gt;
* [[방문자 수 보기 기능 추가]]&lt;br /&gt;
* [[문서 목차 불러오기 기능 추가]] &lt;br /&gt;
* [[댓글 기능 추가]]&lt;br /&gt;
* [[미디어위키 유튜브 삽입|유튜브 삽입]]&lt;br /&gt;
==== 로그인 ====&lt;br /&gt;
* [[구글인증 로그인]]&lt;br /&gt;
&lt;br /&gt;
==== 검색 관련 ====&lt;br /&gt;
* [[시맨틱 미디어위키 설치]]&lt;br /&gt;
* [[시맨틱 이용한 태그 달기]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 3D 모델링 ==&lt;br /&gt;
=== [[초보자를 위한 모델링 프로그램 추천]]===&lt;br /&gt;
==== [[Blender 초보자 따라하기 ]] ====&lt;br /&gt;
==== [[Blender로 마우스 모델링]]====&lt;br /&gt;
==== [[Blender 버티컬 마우스 모델링]]====&lt;br /&gt;
=== [[mashmixer 모델링 편집틀 사용법]]===&lt;br /&gt;
=== 3D 프린터 ===&lt;br /&gt;
==== [[3D 프린터 추천 TOP 5]] ====&lt;br /&gt;
====[[3D 프린터 필라멘트 장단점 PLA PETG|3D 프린터 필라멘트 종류 와 장단점]]====&lt;br /&gt;
&lt;br /&gt;
==== [[뱀부랩 스튜디오 튜토리얼]] ====&lt;br /&gt;
==== [[3D 프린터로 메모 프린터 만들기|3D 프린터로 라인 프린터 만들기]] ====&lt;br /&gt;
==== [[3D 프린터 출력 제품 만족도 평가|3D프린터로 만든 제품 추천]] ====&lt;br /&gt;
==== [[3D 포르쉐911 RC카 만들기]] ====&lt;br /&gt;
&lt;br /&gt;
== 재테크 ==&lt;br /&gt;
=== 부동산 ===&lt;br /&gt;
=== 주식 ===&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|내용 = &amp;lt;big&amp;gt;주식 이야기&amp;lt;/big&amp;gt;&lt;br /&gt;
# [[주식 용어]]&lt;br /&gt;
# [[주식거래시간|주식거래시간(한국/미국/중국)]]&lt;br /&gt;
# [[주식 수수료]]&lt;br /&gt;
# [[주식 금리|금리가 오를때/내릴때 주식 투자법]]&lt;br /&gt;
# [[신호등 분석법]]&lt;br /&gt;
# [[미국 배당주 추천|미국 추천 배당주 ]]&lt;br /&gt;
}}&lt;br /&gt;
* [[주식 금액별 호가 단위]]&lt;br /&gt;
* [[주식 시클리컬 가치주 투자법|시클리컬 가치주 투자법]]&lt;br /&gt;
** [[시클리컬 주요 산업별 대장주]]&lt;br /&gt;
* [[주식 테마주]]&lt;br /&gt;
==== 절세 계좌 투자 ====&lt;br /&gt;
# [[Isa 개인종합자산관리계좌|ISA 개인종합 계좌]] &lt;br /&gt;
# [[IRP 개인퇴직연금 계좌]] &lt;br /&gt;
# [[IRP 개인퇴직연금 계좌#IRP계좌 와 연금저축계좌의 차이|연금저축 계좌]]&lt;br /&gt;
# [[IRP ISA 연금저축펀트 최대 투자 금액|ISA IRP 연금저축펀드 비교 ]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 인물 ==&lt;br /&gt;
=== 근대사 ===&lt;br /&gt;
* [[노무현 명연설]]&lt;br /&gt;
* [[인간적인 국회의원 노회찬]]&lt;br /&gt;
* [[나의 스승 이어령]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 취미 ==&lt;br /&gt;
=== [[수영]] ===&lt;br /&gt;
== 휴식 ==&lt;br /&gt;
=== 휴양지 ===&lt;br /&gt;
==== [[서울_노원구_휴양림|서울_노원구_휴양림-&#039;&#039;&#039;수락 휴&#039;&#039;&#039;]]====&lt;br /&gt;
=== 물놀이/스파 ===&lt;br /&gt;
==== [[영종도 파라다이스 시티]] ====&lt;br /&gt;
==== [[경기도 계곡 추천]]====&lt;br /&gt;
==== [[가평 계곡 추천]] ====&lt;br /&gt;
==== [[전라도 계곡 추천]]====&lt;br /&gt;
==== [[경상도 계곡 추천]] ====&lt;br /&gt;
&lt;br /&gt;
=== 여행지 ===&lt;br /&gt;
=== 맛집 ===&lt;br /&gt;
==== [[인생 맛집 경기/서울]] ====&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 음악 ==&lt;br /&gt;
=== 클래식 ===&lt;br /&gt;
* [[구스타프 말러]]&lt;br /&gt;
&lt;br /&gt;
=== 레젼드 ===&lt;br /&gt;
* [[존바이즈 베스트]]&lt;br /&gt;
* [[유키 구라모토]]&lt;br /&gt;
* [[조지 윈스턴]]&lt;br /&gt;
* [[김광석 베스트]]&lt;br /&gt;
* [[신해철 베스트]]&lt;br /&gt;
* [[김건모 베스트]]&lt;br /&gt;
* [[박혜경 베스트]]&lt;br /&gt;
* [[김필 베스트]]&lt;br /&gt;
&lt;br /&gt;
=== 기타 연습 ===&lt;br /&gt;
* [[메탈리카 기타 연습]]&lt;br /&gt;
* [[캐논 락버전]]&lt;br /&gt;
* [[smells like teen spirit - nirvana|Smells Teen Split-너바나 ]]&lt;br /&gt;
* [[Lover&#039;s concerto]]&lt;br /&gt;
[[category:devcafe]] &lt;br /&gt;
[[category:python]] &lt;br /&gt;
[[category:미디어위키]]&lt;br /&gt;
[[category:autoit]]&lt;br /&gt;
[[category:인물]]&lt;br /&gt;
[[category:역사]]&lt;br /&gt;
[[category:음악]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
	<entry>
		<id>https://devcafe.co.kr/w/index.php?title=%EB%8C%80%EB%AC%B8&amp;diff=2363</id>
		<title>대문</title>
		<link rel="alternate" type="text/html" href="https://devcafe.co.kr/w/index.php?title=%EB%8C%80%EB%AC%B8&amp;diff=2363"/>
		<updated>2026-04-02T09:14:59Z</updated>

		<summary type="html">&lt;p&gt;Devcafe: /* 주식 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__notoc__&lt;br /&gt;
{{#metadata:key=데브카페}}&lt;br /&gt;
{{#metadata:key=파이썬}}&lt;br /&gt;
{{#metadata:key=프로그램개발}}&lt;br /&gt;
{{#metadata:key=autoit}}&lt;br /&gt;
{{#metadata:key=미디어위키}}&lt;br /&gt;
&lt;br /&gt;
= Welcome to DEV CAFE =&lt;br /&gt;
* {{CURRENTYEAR}}.{{CURRENTMONTHNAME}}.{{CURRENTDAY}}({{CURRENTDAYNAME}})  , 이세상 모든 개발에 대한 이야기 {{SERVER}}&lt;br /&gt;
&amp;lt;p sizes=&amp;quot;(max-width: 600px) 480px,800px&amp;quot;&amp;gt;&lt;br /&gt;
https://devcafe.co.kr/w/images/devcafe_main3.jpeg&lt;br /&gt;
&amp;lt;/p&amp;gt;&lt;br /&gt;
{{틀:타이틀 라운드&lt;br /&gt;
|제목=[[:Category:python|파이썬]]: {{PAGESINCATEGORY: python}} 건 ,  [[:Category:미디어위키]] : {{PAGESINCATEGORY: 미디어위키}} 건 ,  [[:Category:autoit|오토잇]]: {{PAGESINCATEGORY: autoit}} 건,  [[:Category:linux|LINUX]]: {{PAGESINCATEGORY: linux}} 건, [[:Category:음악]]: {{PAGESINCATEGORY: 음악}} 건&lt;br /&gt;
|아이콘=emoji_objects&lt;br /&gt;
}}&lt;br /&gt;
== IT/기술 ==&lt;br /&gt;
=== [[파이썬|파이썬 프로그래밍]] ===&lt;br /&gt;
==== 파이썬 설치 ====&lt;br /&gt;
# [[파이썬 설치]]&lt;br /&gt;
# [[Python 가상환경|파이썬 가상화]]&lt;br /&gt;
# [[pip 이용 파이썬 라이브러리 설치|파이썬 라이브러리 설치]]&lt;br /&gt;
# [[pip 이용 파이썬 라이브러리 설치#설치 패키지 추출|설치된 파이썬 라이브러리 추출하기]]&lt;br /&gt;
# [[Python whl 설치|.whl 파일 다운로드 설치법]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 기초 ====&lt;br /&gt;
{{알림&lt;br /&gt;
|내용=&#039;&#039;&#039;[[:category:Python|Python]] Foundation&#039;&#039;&#039; &lt;br /&gt;
# [[PYTHON 데이터형|파이썬 데이터형]]&lt;br /&gt;
# [[튜플 리스트 딕셔너리 차이점,특징,사용예시|튜플 리스트 딕셔너리]]&lt;br /&gt;
# [[파이썬 명명규칙]]&lt;br /&gt;
# [[파이썬 클래스]]&lt;br /&gt;
|아이콘이름=accessibility&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 고급 ====&lt;br /&gt;
:# [[파이썬 개발 추천기능 TOP10|파이썬 고급 기능 TOP 10]]&lt;br /&gt;
:# [[Python Cheat Sheet|파이썬 치트 쉬트 ]]&lt;br /&gt;
:# [[파이썬 config parser|파이썬 .ini 환경파일에 읽고쓰기]]&lt;br /&gt;
:# [[파이썬 로깅]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 응용 ====&lt;br /&gt;
# [[PyQt|파이썬 GUI 프로그램 개발]]&lt;br /&gt;
# [[Process memory share|쉐어 메모리 접근]]&lt;br /&gt;
# [[Python Cheat Sheet#Parallel Processing|병렬처리 프로세싱]]&lt;br /&gt;
# [[멀티프로세싱]]&lt;br /&gt;
# [[파이썬 스케줄러 모듈]]&lt;br /&gt;
# [[파이썬 장고]]&lt;br /&gt;
# [[flask]]&lt;br /&gt;
# [[Json 이용 키-값 추가 수정 삭제 방법|JSON 키값 수정/삭]]&lt;br /&gt;
# [[파이썬 mybatis xml sql 파싱 방법|XML 파싱(Mybatis)]]&lt;br /&gt;
# [[스케줄러 만들기]]&lt;br /&gt;
&lt;br /&gt;
==== 판다스(Pandas) 기초 ====&lt;br /&gt;
{{알림&lt;br /&gt;
|아이콘이름=apps &lt;br /&gt;
|내용= &#039;&#039;&#039;[[:category:pandas|PANDAS]] Foundation&#039;&#039;&#039;&lt;br /&gt;
# [[pandas|판다스(pandas) 기초]]&lt;br /&gt;
# [https://pandas.pydata.org/docs/ pandas 공식문서]&lt;br /&gt;
# [https://realpython.com/pandas-dataframe/ 리얼 판다스 튜토리얼]&lt;br /&gt;
# [https://www.geeksforgeeks.org/pandas-tutorial/ 그릭스 판다스 튜토리얼]&lt;br /&gt;
# [https://towardsdatascience.com/?gi=196f5e0ef243 투왈즈 튜터리얼]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
==== 판다스 응용 ====&lt;br /&gt;
# [[PANDAS 와 SQL 쿼리]]&lt;br /&gt;
# [[Pandas mysql insert]]&lt;br /&gt;
# [[Pandas 엑셀]]&lt;br /&gt;
&lt;br /&gt;
==== [[:category:streamlit|스트림릿 (Streamlit)]] 기초 ====&lt;br /&gt;
# [[Streamlit 기초]]&lt;br /&gt;
# [[Streamlit 개발 사이트|Streamlit 참고 사이트]]&lt;br /&gt;
# [[Streamlit Extention]]&lt;br /&gt;
# [[Streamlit 개발|Streamlit 프로그램]]&lt;br /&gt;
&lt;br /&gt;
==== 파이썬 응용프로그램 개발 ====&lt;br /&gt;
# [[주식 자동매매 프로그램 개발]]&lt;br /&gt;
## [[대신증권 API 활용]]&lt;br /&gt;
## [[키움증권 api 매수매도|키움증권 API 활용]]&lt;br /&gt;
# [[개인 PC 텍스트 검색 채봇]]&lt;br /&gt;
## [[개인 PC 텍스트 검색 챗봇 웹 화면]]&lt;br /&gt;
# [[SQL 정리 툴|SQL Formater(정리)]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[:category:java|JAVA 프로그래밍]] ===&lt;br /&gt;
==== [[Java openjdk 설치|JAVA 설치]] ====&lt;br /&gt;
==== [[JAVA 기초]] ====&lt;br /&gt;
# [[JAVA 명명 규칙]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[:category:autoit|Autotit 프로그래밍]] ===&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|아이콘이름=adb&lt;br /&gt;
|내용 = &amp;lt;big&amp;gt;오토잇 프로그래밍 설치 및 기초 문법&amp;lt;/big&amp;gt;&lt;br /&gt;
# [[Auotit 설치]]&lt;br /&gt;
# [[Autoit 기초 문법]]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== [[Autoit gui|Autoit GUI]] ===&lt;br /&gt;
:# [[Autoit GUI 컨트롤 스타일|GUI 컨트롤 스타일]]&lt;br /&gt;
:# [[Handle의 class instance 값 구하는 방법|GUI 컨트롤 핸들 찾기]]&lt;br /&gt;
:# [[Autoit 멀티 윈도우 컨트롤]]&lt;br /&gt;
:# [[리스트뷰(그리드) 생성]]&lt;br /&gt;
&lt;br /&gt;
=== [[Autoit 프로그램 개발]] ===&lt;br /&gt;
:# [[Autoit sqlite|SQLite 디비 연결]]&lt;br /&gt;
:# [[Autoit oracle connect|ORACLE 디비 연결]]&lt;br /&gt;
:# [[Autoit 키보드 후킹|키보드 후킹]]&lt;br /&gt;
:# [[Autoit 자동 업그레이드 프로그램 개발|자동 업데이트 프로그램 개발]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== [[에디터 활용]] ===&lt;br /&gt;
==== [[:category:vscode|VS 코드(VS Code)]] ====&lt;br /&gt;
# [[Vscode 단축키]]&lt;br /&gt;
&lt;br /&gt;
==== [[:Category:서브라임|서브라임 (sublime)]] ====&lt;br /&gt;
# [[서브라임 sublime 단축키|Sublime 단축키]]&lt;br /&gt;
# [[Sublime plugin 생성 방법|Sublime 플러그인 만들기]]&lt;br /&gt;
# [[sublime emmet]]&lt;br /&gt;
==== [[Vi 명령어|vi 편집기]] ====&lt;br /&gt;
# [[Vi mode - ksh bash zsh]]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 미디어위키 사용법 ==&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|아이콘이름=school&lt;br /&gt;
|내용= &#039;&#039;&#039;미디어 위키 초보자 가이드&#039;&#039;&#039; &lt;br /&gt;
# [[미디어위키 기본 사용법]] &lt;br /&gt;
# [[환경세팅 파일|환경설정(LocalSettings.php)]]&lt;br /&gt;
# [[스킨(테마) 추가/변경]]&lt;br /&gt;
# [[미디어위키 확장 기능]]&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
=== [[미디어위키 예약어 와 변수]] ===&lt;br /&gt;
==== [[미디어위키 예약어 와 변수#동작어_(Behavior_Switches)|미디어위키 동작 예약어]] ====&lt;br /&gt;
==== [[미디어위키 예약어 와 변수#변수_(Variables)|미디어위키 변수 예약어]] ====&lt;br /&gt;
&lt;br /&gt;
=== [[미디어위키 기본 사용법]] ===&lt;br /&gt;
* [[미디어위키 기본 사용법#새글 쓰기|새글 쓰기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#대문 수정|첫페이지(대문) 수정]]&lt;br /&gt;
* [[미디어위키 기본 사용법#문단(Section)|목차 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#표(wiki table) 만들기|표 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#틀(Template) 만들기|틀 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#양식(Form) 만들기|폼 만들기]]&lt;br /&gt;
* [[미디어위키 기본 사용법#카테고리 만들기|카테고리 만들기]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== 미디어 위키 환경설정 ===&lt;br /&gt;
* [[스킨(테마) 추가/변경]]&lt;br /&gt;
* [[미디어위키 기본 사용법#대문 수정하기|대문 타이틀 글씨 숨기기]]&lt;br /&gt;
* [[특수 명령어]]&lt;br /&gt;
* [[환경세팅 파일|환경세팅 파일(LocalSettings.php)]]&lt;br /&gt;
* [[나눔고딕 웹폰트 설정하기]]&lt;br /&gt;
* [[반응형 이미지 사이즈 조절 방법]]&lt;br /&gt;
* [[스니펫-목차에 순번 붙이기]]&lt;br /&gt;
&lt;br /&gt;
=== 미디어위키 확장 프로그램 가이드===&lt;br /&gt;
==== 페이지 관련 ====&lt;br /&gt;
* [[Syntax Highlight|문법 하이라이트]]&lt;br /&gt;
* [[목차에 순번 붙이기|목차에 순번 붙이기(확장기능 설치)]]&lt;br /&gt;
** [[스니펫-목차에 순번 붙이기 |목차에 순번 붙이기(JS/CSS 이용)]] / [https://www.mediawiki.org/wiki/Manual:Table_of_contents#Auto-numbering 원본페이지 이동]&lt;br /&gt;
* [[페이지에 탭 만들기]]&lt;br /&gt;
* [[관련글 보기 기능 추가 ]]&lt;br /&gt;
* [[방문자 수 보기 기능 추가]]&lt;br /&gt;
* [[문서 목차 불러오기 기능 추가]] &lt;br /&gt;
* [[댓글 기능 추가]]&lt;br /&gt;
* [[미디어위키 유튜브 삽입|유튜브 삽입]]&lt;br /&gt;
==== 로그인 ====&lt;br /&gt;
* [[구글인증 로그인]]&lt;br /&gt;
&lt;br /&gt;
==== 검색 관련 ====&lt;br /&gt;
* [[시맨틱 미디어위키 설치]]&lt;br /&gt;
* [[시맨틱 이용한 태그 달기]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== 3D 모델링 ==&lt;br /&gt;
=== [[초보자를 위한 모델링 프로그램 추천]]===&lt;br /&gt;
==== [[Blender 초보자 따라하기 ]] ====&lt;br /&gt;
==== [[Blender로 마우스 모델링]]====&lt;br /&gt;
==== [[Blender 버티컬 마우스 모델링]]====&lt;br /&gt;
=== [[mashmixer 모델링 편집틀 사용법]]===&lt;br /&gt;
=== 3D 프린터 ===&lt;br /&gt;
==== [[3D 프린터 추천 TOP 5]] ====&lt;br /&gt;
====[[3D 프린터 필라멘트 장단점 PLA PETG|3D 프린터 필라멘트 종류 와 장단점]]====&lt;br /&gt;
&lt;br /&gt;
==== [[뱀부랩 스튜디오 튜토리얼]] ====&lt;br /&gt;
==== [[3D 프린터로 메모 프린터 만들기|3D 프린터로 라인 프린터 만들기]] ====&lt;br /&gt;
==== [[3D 프린터 출력 제품 만족도 평가|3D프린터로 만든 제품 추천]] ====&lt;br /&gt;
==== [[3D 포르쉐911 RC카 만들기]] ====&lt;br /&gt;
&lt;br /&gt;
== 재테크 ==&lt;br /&gt;
=== 부동산 ===&lt;br /&gt;
=== 주식 ===&lt;br /&gt;
{{틀:알림&lt;br /&gt;
|내용 = &amp;lt;big&amp;gt;주식 이야기&amp;lt;/big&amp;gt;&lt;br /&gt;
# [[주식 용어]]&lt;br /&gt;
# [[주식거래시간|주식거래시간(한국/미국/중국)]]&lt;br /&gt;
# [[주식 수수료]]&lt;br /&gt;
# [[주식 금리|금리가 오를때/내릴때 주식 투자법]]&lt;br /&gt;
# [[신호등 분석법]]&lt;br /&gt;
# [[미국 배당주 추천|미국 추천 배당주 ]]&lt;br /&gt;
}}&lt;br /&gt;
* [[주식 금액별 호가 단위]]&lt;br /&gt;
* [[주식 시클리컬 가치주 투자법|시클리컬 가치주 투자법]]&lt;br /&gt;
## [[시클리컬 주요 산업별 대장주]]&lt;br /&gt;
* [[주식 테마주]]&lt;br /&gt;
==== 절세 계좌 투자 ====&lt;br /&gt;
# [[Isa 개인종합자산관리계좌|ISA 개인종합 계좌]] &lt;br /&gt;
# [[IRP 개인퇴직연금 계좌]] &lt;br /&gt;
# [[IRP 개인퇴직연금 계좌#IRP계좌 와 연금저축계좌의 차이|연금저축 계좌]]&lt;br /&gt;
# [[IRP ISA 연금저축펀트 최대 투자 금액|ISA IRP 연금저축펀드 비교 ]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 인물 ==&lt;br /&gt;
=== 근대사 ===&lt;br /&gt;
* [[노무현 명연설]]&lt;br /&gt;
* [[인간적인 국회의원 노회찬]]&lt;br /&gt;
* [[나의 스승 이어령]]&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 취미 ==&lt;br /&gt;
=== [[수영]] ===&lt;br /&gt;
== 휴식 ==&lt;br /&gt;
=== 휴양지 ===&lt;br /&gt;
==== [[서울_노원구_휴양림|서울_노원구_휴양림-&#039;&#039;&#039;수락 휴&#039;&#039;&#039;]]====&lt;br /&gt;
=== 물놀이/스파 ===&lt;br /&gt;
==== [[영종도 파라다이스 시티]] ====&lt;br /&gt;
==== [[경기도 계곡 추천]]====&lt;br /&gt;
==== [[가평 계곡 추천]] ====&lt;br /&gt;
==== [[전라도 계곡 추천]]====&lt;br /&gt;
==== [[경상도 계곡 추천]] ====&lt;br /&gt;
&lt;br /&gt;
=== 여행지 ===&lt;br /&gt;
=== 맛집 ===&lt;br /&gt;
==== [[인생 맛집 경기/서울]] ====&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== 음악 ==&lt;br /&gt;
=== 클래식 ===&lt;br /&gt;
* [[구스타프 말러]]&lt;br /&gt;
&lt;br /&gt;
=== 레젼드 ===&lt;br /&gt;
* [[존바이즈 베스트]]&lt;br /&gt;
* [[유키 구라모토]]&lt;br /&gt;
* [[조지 윈스턴]]&lt;br /&gt;
* [[김광석 베스트]]&lt;br /&gt;
* [[신해철 베스트]]&lt;br /&gt;
* [[김건모 베스트]]&lt;br /&gt;
* [[박혜경 베스트]]&lt;br /&gt;
* [[김필 베스트]]&lt;br /&gt;
&lt;br /&gt;
=== 기타 연습 ===&lt;br /&gt;
* [[메탈리카 기타 연습]]&lt;br /&gt;
* [[캐논 락버전]]&lt;br /&gt;
* [[smells like teen spirit - nirvana|Smells Teen Split-너바나 ]]&lt;br /&gt;
* [[Lover&#039;s concerto]]&lt;br /&gt;
[[category:devcafe]] &lt;br /&gt;
[[category:python]] &lt;br /&gt;
[[category:미디어위키]]&lt;br /&gt;
[[category:autoit]]&lt;br /&gt;
[[category:인물]]&lt;br /&gt;
[[category:역사]]&lt;br /&gt;
[[category:음악]]&lt;/div&gt;</summary>
		<author><name>Devcafe</name></author>
	</entry>
</feed>