<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>dsclub &amp;gt; 블로그 &amp;gt; 코딩 스토리</title>
<link>https://dsclub.kr/code</link>
<language>ko</language>
<description>코딩 스토리 (2025-09-12 23:16:15)</description>

<item>
<title>그누보드5 유령봇</title>
<link>https://dsclub.kr/code/1411</link>
<description><![CDATA[<p>&lt;!doctype html&gt;</p><p>&lt;html lang="ko"&gt;</p><p>&lt;head&gt;</p><p>&lt;meta charset="utf-8"/&gt;</p><p>&lt;meta name="viewport" content="width=device-width,initial-scale=1"/&gt;</p><p>&lt;title&gt;GNUboard5 Ghost Visitor — 브라우저 봇&lt;/title&gt;</p><p>&lt;style&gt;</p><p>  body{font-family:system-ui,-apple-system,Segoe UI,Roboto,"Noto Sans KR",sans-serif;background:#0f1724;color:#e6eef8;padding:18px}</p><p>  h1{font-size:18px;margin:0 0 10px}</p><p>  .card{background:#0b1220;padding:12px;border-radius:8px;margin-bottom:12px;box-shadow:0 2px 8px rgba(0,0,0,0.5)}</p><p>  label{display:block;margin:6px 0 2px;font-size:13px;color:#a8c0e0}</p><p>  input[type=text], input[type=number], select {width:100%;padding:8px;border-radius:6px;border:1px solid #1f2a3a;background:#08101a;color:#e6eef8}</p><p>  button{padding:8px 12px;border-radius:6px;border:none;background:#2b8be6;color:white;cursor:pointer;margin-right:8px}</p><p>  .muted{color:#7f9bb0;font-size:13px}</p><p>  pre{background:#071025;padding:8px;border-radius:6px;color:#cfe8ff;max-height:280px;overflow:auto}</p><p>  .flex{display:flex;gap:8px}</p><p>  .small{font-size:13px}</p><p>  .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:8px;vertical-align:middle}</p><p>  .dot-idle{background:#999}</p><p>  .dot-run{background:#2bd39b}</p><p>  .dot-stop{background:#ffa94d}</p><p>  .controls{margin-top:8px}</p><p>  .tags{font-size:12px;color:#8fb0d8;margin-top:6px}</p><p>  .footer{font-size:12px;color:#7b97b6;margin-top:12px}</p><p>  a.link { color:#7fd0ff }</p><p>&lt;/style&gt;</p><p>&lt;/head&gt;</p><p>&lt;body&gt;</p><p>  &lt;h1&gt;GNUboard5 Ghost Visitor — 브라우저용 유령 봇&lt;/h1&gt;</p><p>​<br /></p><p>  &lt;div class="card"&gt;</p><p>    &lt;div style="display:flex;justify-content:space-between;align-items:center"&gt;</p><p>      &lt;div&gt;</p><p>        &lt;span id="statusDot" class="status-dot dot-idle"&gt;&lt;/span&gt;</p><p>        &lt;strong id="statusText"&gt;대기 중&lt;/strong&gt;</p><p>        &lt;div class="muted small"&gt;브라우저 탭에서 실행됩니다 — 동일 출처에서 호스팅하세요.&lt;/div&gt;</p><p>      &lt;/div&gt;</p><p>      &lt;div&gt;</p><p>        &lt;button id="startBtn"&gt;Start&lt;/button&gt;</p><p>        &lt;button id="stopBtn" disabled&gt;Stop&lt;/button&gt;</p><p>      &lt;/div&gt;</p><p>    &lt;/div&gt;</p><p>  &lt;/div&gt;</p><p>​<br /></p><p>  &lt;div class="card"&gt;</p><p>    &lt;label&gt;시작 URL (같은 도메인 내 페이지)&lt;/label&gt;</p><p>    &lt;input type="text" id="startUrl" value="/" placeholder="예: /, /bbs/board.php?bo_table=free" /&gt;</p><p>​<br /></p><p>    &lt;label&gt;게시판 링크 선택자 (CSS selector, 보통 게시판 목록의 링크)&lt;/label&gt;</p><p>    &lt;input type="text" id="boardSelector" value="a[href*='board.php'], a[href*='bbs/board.php'], a.board, a.board-link" /&gt;</p><p>​<br /></p><p>    &lt;label&gt;게시글 링크 선택자 (CSS selector)&lt;/label&gt;</p><p>    &lt;input type="text" id="postSelector" value="a[href*='view.php'], a[href*='bbs/view.php'], a.post, a.wr_subject a" /&gt;</p><p>​<br /></p><p>    &lt;label&gt;포함/제외할 href 패턴 (정규식, 쉼표로 구분, optional)&lt;/label&gt;</p><p>    &lt;input type="text" id="includePattern" placeholder="예: /bbs/, /board.php" /&gt;</p><p>    &lt;input type="text" id="excludePattern" placeholder="예: /write.php, /login.php" /&gt;</p><p>​<br /></p><p>    &lt;div class="flex"&gt;</p><p>      &lt;div style="flex:1"&gt;</p><p>        &lt;label&gt;최소 대기(ms)&lt;/label&gt;</p><p>        &lt;input type="number" id="minDelay" value="1500" /&gt;</p><p>      &lt;/div&gt;</p><p>      &lt;div style="flex:1"&gt;</p><p>        &lt;label&gt;최대 대기(ms)&lt;/label&gt;</p><p>        &lt;input type="number" id="maxDelay" value="4500" /&gt;</p><p>      &lt;/div&gt;</p><p>      &lt;div style="flex:1"&gt;</p><p>        &lt;label&gt;최대 방문 수 (0 = 무제한)&lt;/label&gt;</p><p>        &lt;input type="number" id="maxVisits" value="0" /&gt;</p><p>      &lt;/div&gt;</p><p>    &lt;/div&gt;</p><p>​<br /></p><p>    &lt;div class="flex controls"&gt;</p><p>      &lt;button id="seedBtn"&gt;수동 시드 추가&lt;/button&gt;</p><p>      &lt;button id="exportLogBtn"&gt;로그 내보내기&lt;/button&gt;</p><p>      &lt;button id="clearLogBtn"&gt;로그 초기화&lt;/button&gt;</p><p>    &lt;/div&gt;</p><p>    &lt;div class="tags"&gt;</p><p>      &lt;div&gt;Tip: 로그인된 상태(쿠키 있음)로 실행하면 인증 페이지도 접근됩니다. 서버에 부담이 되지 않도록 딜레이를 충분히 주세요.&lt;/div&gt;</p><p>    &lt;/div&gt;</p><p>  &lt;/div&gt;</p><p>​<br /></p><p>  &lt;div class="card"&gt;</p><p>    &lt;label&gt;진행 로그&lt;/label&gt;</p><p>    &lt;pre id="log" aria-live="polite"&gt;&lt;/pre&gt;</p><p>  &lt;/div&gt;</p><p>​<br /></p><p>&lt;script&gt;</p><p>/*</p><p>  GNUboard5 Ghost Visitor</p><p>  - 동작 방식:</p><p>    1) 시작 URL에서 HTML을 fetch</p><p>    2) boardSelector, postSelector로 링크 수집</p><p>    3) 방문 큐(visited set) 유지하며 FIFO 탐색</p><p>    4) 각 방문마다 랜덤 지연, 페이지 fetch, 스크롤/머무르기 시뮬레이션</p><p>    5) 설정으로 포함/제외 패턴, 최대 방문수 제어 가능</p><p>​<br /></p><p>  주의:</p><p>  - 동일 출처(same-origin)에서 실행해야 합니다.</p><p>  - 서버 부하를 주지 않도록 delay를 넉넉히(최소 1~1.5초 권장) 설정하세요.</p><p>*/</p><p>​<br /></p><p>(function(){</p><p>  // UI elements</p><p>  const startBtn = document.getElementById('startBtn');</p><p>  const stopBtn = document.getElementById('stopBtn');</p><p>  const statusDot = document.getElementById('statusDot');</p><p>  const statusText = document.getElementById('statusText');</p><p>  const logEl = document.getElementById('log');</p><p>  const seedBtn = document.getElementById('seedBtn');</p><p>  const exportLogBtn = document.getElementById('exportLogBtn');</p><p>  const clearLogBtn = document.getElementById('clearLogBtn');</p><p>​<br /></p><p>  // Settings inputs</p><p>  const startUrlInput = document.getElementById('startUrl');</p><p>  const boardSelectorInput = document.getElementById('boardSelector');</p><p>  const postSelectorInput = document.getElementById('postSelector');</p><p>  const includePatternInput = document.getElementById('includePattern');</p><p>  const excludePatternInput = document.getElementById('excludePattern');</p><p>  const minDelayInput = document.getElementById('minDelay');</p><p>  const maxDelayInput = document.getElementById('maxDelay');</p><p>  const maxVisitsInput = document.getElementById('maxVisits');</p><p>​<br /></p><p>  // State</p><p>  let running = false;</p><p>  let queue = []; // URL strings to visit</p><p>  let visited = new Set();</p><p>  let visitCount = 0;</p><p>  let logs = [];</p><p>  let controller = null; // AbortController for fetches if needed</p><p>​<br /></p><p>  function appendLog(line){</p><p>    const ts = new Date().toLocaleTimeString();</p><p>    const text = `[${ts}] ${line}`;</p><p>    logs.push(text);</p><p>    logEl.textContent = logs.slice(-500).join('\\n'); // keep last 500 lines</p><p>    logEl.scrollTop = logEl.scrollHeight;</p><p>    console.log(text);</p><p>  }</p><p>​<br /></p><p>  function setStatus(state){</p><p>    if(state === 'running'){</p><p>      running = true;</p><p>      statusDot.className = 'status-dot dot-run';</p><p>      statusText.textContent = '실행 중';</p><p>      startBtn.disabled = true;</p><p>      stopBtn.disabled = false;</p><p>    } else if(state === 'stopped'){</p><p>      running = false;</p><p>      statusDot.className = 'status-dot dot-stop';</p><p>      statusText.textContent = '중지됨';</p><p>      startBtn.disabled = false;</p><p>      stopBtn.disabled = true;</p><p>    } else {</p><p>      running = false;</p><p>      statusDot.className = 'status-dot dot-idle';</p><p>      statusText.textContent = '대기 중';</p><p>      startBtn.disabled = false;</p><p>      stopBtn.disabled = true;</p><p>    }</p><p>  }</p><p>​<br /></p><p>  // Utility: safe absolute URL join</p><p>  function absolutize(href){</p><p>    try {</p><p>      return new URL(href, location.href).href;</p><p>    } catch(err){</p><p>      return null;</p><p>    }</p><p>  }</p><p>​<br /></p><p>  function buildRegexList(csv){</p><p>    if(!csv) return null;</p><p>    const parts = csv.split(',').map(s=&gt;s.trim()).filter(Boolean);</p><p>    if(parts.length === 0) return null;</p><p>    // combine with OR, escape slashes? allow as patterns</p><p>    try {</p><p>      const joined = parts.map(p =&gt; {</p><p>        // if p looks like a regex (starts and ends with /) -&gt; use as-is</p><p>        if(p.startsWith('/') &amp;&amp; p.lastIndexOf('/')&gt;0){</p><p>          const last = p.lastIndexOf('/');</p><p>          const pat = p.slice(1,last);</p><p>          const flag = p.slice(last+1);</p><p>          return `(${pat})`;</p><p>        } else {</p><p>          // escape special regex chars</p><p>          const esc = p.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\\\$&amp;");</p><p>          return `(${esc})`;</p><p>        }</p><p>      }).join('|');</p><p>      return new RegExp(joined,'i');</p><p>    } catch(e){</p><p>      console.warn('pattern error', e);</p><p>      return null;</p><p>    }</p><p>  }</p><p>​<br /></p><p>  // parse HTML text into document</p><p>  function parseHTML(text){</p><p>    const parser = new DOMParser();</p><p>    return parser.parseFromString(text, 'text/html');</p><p>  }</p><p>​<br /></p><p>  // random delay</p><p>  function randDelay(minMs, maxMs){</p><p>    const m = Math.max(0, Number(minMs) || 0);</p><p>    const M = Math.max(m, Number(maxMs) || m);</p><p>    return m + Math.floor(Math.random() * (M - m + 1));</p><p>  }</p><p>​<br /></p><p>  // simulate reading/scrolling time on a fetched page</p><p>  async function simulateReading(doc){</p><p>    // approximate reading time based on text length</p><p>    const bodyText = doc.body ? doc.body.innerText || '' : '';</p><p>    const chars = Math.min(20000, bodyText.length);</p><p>    // reading speed rough: 250 chars/sec =&gt; chars/250*1000 ms</p><p>    const baseMs = Math.min(120000, Math.max(1000, Math.floor((chars/250)*1000)));</p><p>    // add random jitter</p><p>    const jitter = Math.floor(Math.random() * 2000) - 1000;</p><p>    const total = Math.max(500, baseMs + jitter);</p><p>    appendLog(`읽는 중... 예상 ${Math.round(total/1000)}초`);</p><p>    // emulate incremental "scroll" events by waiting in chunks</p><p>    const steps = Math.min(12, Math.max(1, Math.floor(total / 2000)));</p><p>    for(let i=0;i&lt;steps &amp;&amp; running;i++){</p><p>      await new Promise(r =&gt; setTimeout(r, Math.floor(total/steps)));</p><p>      // dispatch a fake scroll event to the window (sometimes useful)</p><p>      try{</p><p>        window.dispatchEvent(new Event('scroll'));</p><p>      }catch(e){}</p><p>    }</p><p>  }</p><p>​<br /></p><p>  // main visit function</p><p>  async function visitUrl(url, selectors){</p><p>    if(!running) return;</p><p>    if(!url) return;</p><p>    if(visited.has(url)) return;</p><p>    // respect max visits</p><p>    const maxVisits = Number(maxVisitsInput.value) || 0;</p><p>    if(maxVisits &gt; 0 &amp;&amp; visitCount &gt;= maxVisits) {</p><p>      appendLog(`최대 방문수(${maxVisits}) 도달 — 중지 예정`);</p><p>      stop();</p><p>      return;</p><p>    }</p><p>​<br /></p><p>    visited.add(url);</p><p>    queue = queue.filter(x =&gt; x !== url);</p><p>    visitCount++;</p><p>    appendLog(`방문(${visitCount}): ${url}`);</p><p>​<br /></p><p>    controller = new AbortController();</p><p>    const signal = controller.signal;</p><p>​<br /></p><p>    // add small randomized delay before fetch (simulate think/click)</p><p>    const preDelay = randDelay(Number(minDelayInput.value), Number(maxDelayInput.value));</p><p>    await new Promise(r =&gt; setTimeout(r, preDelay));</p><p>​<br /></p><p>    try {</p><p>      const resp = await fetch(url, {method:'GET', credentials:'same-origin', signal});</p><p>      const text = await resp.text();</p><p>      const doc = parseHTML(text);</p><p>​<br /></p><p>      // optional: if page contains redirect meta refresh, follow once</p><p>      const metaRefresh = doc.querySelector('meta[http-equiv="refresh"]');</p><p>      if(metaRefresh){</p><p>        const content = metaRefresh.getAttribute('content') || '';</p><p>        const m = content.match(/url=(.+)/i);</p><p>        if(m &amp;&amp; m[1]){</p><p>          const next = absolutize(m[1].trim());</p><p>          if(next &amp;&amp; !visited.has(next)){</p><p>            appendLog('meta refresh 감지 — 리다이렉트 방문: ' + next);</p><p>            queue.push(next);</p><p>          }</p><p>        }</p><p>      }</p><p>​<br /></p><p>      // simulate reading/scrolling time</p><p>      await simulateReading(doc);</p><p>​<br /></p><p>      // find links per selectors</p><p>      try {</p><p>        const {boardSelector, postSelector, includeRegex, excludeRegex} = selectors;</p><p>        const foundAnchors = Array.from(doc.querySelectorAll('a[href]'));</p><p>        for(const a of foundAnchors){</p><p>          const href = a.getAttribute('href');</p><p>          const abs = absolutize(href);</p><p>          if(!abs) continue;</p><p>          // only same origin</p><p>          if(new URL(abs).origin !== location.origin) continue;</p><p>​<br /></p><p>          // exclude obvious assets (images, css, js) by extension</p><p>          if(abs.match(/\\.(jpg|jpeg|png|gif|svg|css|js|woff2?|ico)(\\?|$)/i)) continue;</p><p>​<br /></p><p>          // apply include/exclude patterns</p><p>          if(excludeRegex &amp;&amp; excludeRegex.test(abs)) continue;</p><p>          if(includeRegex &amp;&amp; !includeRegex.test(abs)) continue;</p><p>​<br /></p><p>          // if matches board or post selector (on original doc, matching CSS is limited because we used generic anchors)</p><p>          // We'll heuristically check href path for common keywords as fallback</p><p>          const path = new URL(abs).pathname + (new URL(abs).search || '');</p><p>          const boardHeur = /board|bbs|bo_table|bo_list|board.php|bbs\/board.php/i;</p><p>          const postHeur = /view.php|wr_id|wr_subject|read.php|bbs\/view.php/i;</p><p>​<br /></p><p>          // Use provided selectors: if selector matches (we only had access to doc anchors), use that</p><p>          let accepted = false;</p><p>          try {</p><p>            if(boardSelector &amp;&amp; a.matches(boardSelector)) accepted = true;</p><p>            if(postSelector &amp;&amp; a.matches(postSelector)) accepted = true;</p><p>          } catch(e){</p><p>            // selector may be invalid for this document — ignore.</p><p>          }</p><p>​<br /></p><p>          // heuristic fallback</p><p>          if(!accepted){</p><p>            if(boardHeur.test(path) || postHeur.test(path)) accepted = true;</p><p>          }</p><p>​<br /></p><p>          if(accepted &amp;&amp; !visited.has(abs) &amp;&amp; !queue.includes(abs)){</p><p>            queue.push(abs);</p><p>            appendLog('큐에 추가: ' + abs);</p><p>          }</p><p>        }</p><p>      } catch(e){</p><p>        console.warn('링크 수집 실패', e);</p><p>      }</p><p>​<br /></p><p>    } catch(err){</p><p>      if(err.name === 'AbortError'){</p><p>        appendLog('요청 중단: ' + url);</p><p>      } else {</p><p>        appendLog('방문 실패: ' + url + ' — ' + (err &amp;&amp; err.message));</p><p>      }</p><p>    } finally {</p><p>      controller = null;</p><p>    }</p><p>  }</p><p>​<br /></p><p>  // main loop</p><p>  async function loop(){</p><p>    setStatus('running');</p><p>    const selectors = {</p><p>      boardSelector: boardSelectorInput.value || null,</p><p>      postSelector: postSelectorInput.value || null,</p><p>      includeRegex: buildRegexList(includePatternInput.value),</p><p>      excludeRegex: buildRegexList(excludePatternInput.value),</p><p>    };</p><p>​<br /></p><p>    // seed with startUrl if queue empty</p><p>    if(queue.length === 0){</p><p>      const s = startUrlInput.value.trim() || '/';</p><p>      const abs = absolutize(s) || location.href;</p><p>      if(abs) queue.push(abs);</p><p>    }</p><p>​<br /></p><p>    while(running &amp;&amp; queue.length &gt; 0){</p><p>      const next = queue.shift();</p><p>      if(!next) continue;</p><p>      // skip if visited</p><p>      if(visited.has(next)) continue;</p><p>​<br /></p><p>      // visit</p><p>      await visitUrl(next, selectors);</p><p>​<br /></p><p>      // small randomized pause between visits (simulate human pause)</p><p>      const idle = randDelay(Number(minDelayInput.value), Number(maxDelayInput.value));</p><p>      await new Promise(r =&gt; setTimeout(r, idle));</p><p>​<br /></p><p>      // check max visits again</p><p>      const maxV = Number(maxVisitsInput.value) || 0;</p><p>      if(maxV &gt; 0 &amp;&amp; visitCount &gt;= maxV){</p><p>        appendLog('최대 방문수 도달 — 루프 종료');</p><p>        break;</p><p>      }</p><p>    }</p><p>​<br /></p><p>    setStatus('stopped');</p><p>    appendLog('작업 종료 — 큐 길이: ' + queue.length + ' 방문수: ' + visitCount);</p><p>  }</p><p>​<br /></p><p>  function start(){</p><p>    if(running) return;</p><p>    appendLog('시작');</p><p>    running = true;</p><p>    visitCount = 0;</p><p>    // do not clear visited by default (so it won't repeat); if you'd like fresh run, user can refresh page</p><p>    loop().catch(err =&gt; {</p><p>      appendLog('루프 에러: ' + (err &amp;&amp; err.message));</p><p>      setStatus('stopped');</p><p>    });</p><p>  }</p><p>​<br /></p><p>  function stop(){</p><p>    if(!running) return;</p><p>    appendLog('중지 요청됨 — 현재 요청을 취소합니다.');</p><p>    running = false;</p><p>    if(controller) {</p><p>      try { controller.abort(); } catch(e){}</p><p>      controller = null;</p><p>    }</p><p>    setStatus('stopped');</p><p>  }</p><p>​<br /></p><p>  // seed manual (현재 startUrl 추가)</p><p>  seedBtn.addEventListener('click', ()=&gt;{</p><p>    const s = prompt('시드로 추가할 URL을 입력하세요 (상대/절대 가능)', startUrlInput.value || '/');</p><p>    if(!s) return;</p><p>    const abs = absolutize(s);</p><p>    if(!abs){ alert('잘못된 URL'); return; }</p><p>    if(!queue.includes(abs) &amp;&amp; !visited.has(abs)){ queue.push(abs); appendLog('수동 시드 추가: ' + abs); }</p><p>    else appendLog('이미 큐에 존재하거나 방문됨: ' + abs);</p><p>  });</p><p>​<br /></p><p>  startBtn.addEventListener('click', ()=&gt;{</p><p>    // small validation</p><p>    const minD = Number(minDelayInput.value);</p><p>    const maxD = Number(maxDelayInput.value);</p><p>    if(isNaN(minD) || isNaN(maxD) || minD &lt; 0 || maxD &lt; 0 || maxD &lt; minD){</p><p>      alert('지연 설정이 유효하지 않습니다. Min/Max Delay를 확인하세요.');</p><p>      return;</p><p>    }</p><p>    // ensure same origin start</p><p>    const s = startUrlInput.value.trim() || '/';</p><p>    const abs = absolutize(s);</p><p>    if(!abs){</p><p>      alert('시작 URL이 유효하지 않습니다.');</p><p>      return;</p><p>    }</p><p>    if(new URL(abs).origin !== location.origin){</p><p>      if(!confirm('시작 URL이 현재 도메인과 다릅니다. 동일 출처에서 실행해야 정상 동작합니다. 계속하시겠습니까?')) return;</p><p>    }</p><p>    if(!queue.includes(abs) &amp;&amp; !visited.has(abs)) queue.unshift(abs);</p><p>    start();</p><p>  });</p><p>​<br /></p><p>  stopBtn.addEventListener('click', stop);</p><p>​<br /></p><p>  exportLogBtn.addEventListener('click', ()=&gt;{</p><p>    const blob = new Blob([logs.join('\\n')], {type:'text/plain;charset=utf-8'});</p><p>    const url = URL.createObjectURL(blob);</p><p>    const a = document.createElement('a');</p><p>    a.href = url;</p><p>    a.download = 'ghost-visitor-log-' + (new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')) + '.txt';</p><p>    document.body.appendChild(a);</p><p>    a.click();</p><p>    a.remove();</p><p>    URL.revokeObjectURL(url);</p><p>  });</p><p>​<br /></p><p>  clearLogBtn.addEventListener('click', ()=&gt;{</p><p>    if(!confirm('로그를 초기화 하시겠습니까?')) return;</p><p>    logs = [];</p><p>    logEl.textContent = '';</p><p>    appendLog('로그 초기화 완료');</p><p>  });</p><p>​<br /></p><p>  // keyboard shortcut: S to start/stop</p><p>  window.addEventListener('keydown', (e)=&gt;{</p><p>    if(e.key === 's' || e.key === 'S'){</p><p>      if(running) stop(); else start();</p><p>    }</p><p>  });</p><p>​<br /></p><p>  // initial status</p><p>  setStatus('idle');</p><p>  appendLog('로더 준비 완료 — 동일 출처에서 실행하세요.');</p><p>​<br /></p><p>})();</p><p>&lt;/script&gt;</p><p>​<br /></p><p>&lt;div class="footer"&gt;</p><p>  &lt;div&gt;작성자: Ghost Visitor 스크립트 • 사용 전 서버 부하와 법적/윤리적 영향 확인 필수&lt;/div&gt;</p><p>  &lt;div&gt;문제/개선 요청 있으면 알려줘요.&lt;/div&gt;</p><p>&lt;/div&gt;</p><p>&lt;/body&gt;</p><p>&lt;/html&gt;</p><p>​<br /></p>]]></description>
<dc:creator>Tak2</dc:creator>
<dc:date>2025-09-12T23:16:15+09:00</dc:date>
</item>


<item>
<title>t2CAPTCHA (자체 호스팅 무료 CAPTCHA) 캡챠</title>
<link>https://dsclub.kr/code/1409</link>
<description><![CDATA[<p><br /></p><p><br /></p><p></p><div class="t2-media-block"><div style="width:600px;max-width:100%;margin:0px auto;"><img src="https://dsclub.kr/data/editor/250828/6891530171756321182770_0.webp" style="width:600px;" alt="6891530171756321182770_0.webp" /></div></div><p></p><p><br /></p><p><br /></p><p><span style="font-size:30px;"><span style="font-size:30px;"><span style="font-size:30px;font-weight:bold;font-style:italic;"><a href="https://dsclub.kr/service/captcha" target="_blank" style="text-decoration:none;color:rgb(1,135,254);" rel="nofollow noreferrer noopener">t2CAPTCHA</a></span></span></span></p><p>​</p><p><span style="font-style:italic;font-weight:bold;">t2CAPTCHA</span>는 그누보드5(Gnuboard5)를 위해 설계된 자체 호스팅 CAPTCHA 솔루션입니다. reCAPTCHA의 대안으로 개발되었으며, 복잡한 이미지 퍼즐 대신 간단한 텍스트 기반 문제를 통해 사용자 편의성을 높이면서도 효과적인 보안을 제공합니다. 외부 서비스에 의존하지 않는 완전한 자체 호스팅 솔루션입니다.</p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">작동 화면:</span></span></p><p></p><div class="t2-media-block"><div style="width:600px;max-width:100%;margin:0px auto;"><img src="https://dsclub.kr/data/editor/250828/9157867431756321258079_0.webp" style="width:600px;" alt="9157867431756321258079_0.webp" /></div></div><p></p><p><br /></p><p></p><div class="t2-media-block"><div style="width:600px;max-width:100%;margin:0px auto;"><img src="https://dsclub.kr/data/editor/250828/9157867431756321258079_1.webp" style="width:600px;" alt="9157867431756321258079_1.webp" /></div></div><p></p><p><br /></p><p></p><div class="t2-media-block"><div style="width:600px;max-width:100%;margin:0px auto;"><img src="https://dsclub.kr/data/editor/250828/9157867431756321258079_2.webp" style="width:600px;" alt="9157867431756321258079_2.webp" /></div></div><p></p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:24px;">특징 및 기능</span></span></p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">직관적인 사용자 인터페이스 - 누구나 쉽게 사용할 수 있도록 설계된 UI 제공

모바일 환경 최적화 및 높은 호환성 - 안드로이드, iOS를 비롯한 다양한 기기에서 원활한 사용 가능

다국어 지원 - 브라우저 언어 설정을 자동 감지하여 한국어, 영어, 일본어로 문제 제공

허니팟 기술 - CSS로 숨겨진 입력 필드를 통해 자동화 프로그램의 접근을 감지

단계별 난이도 조절 - 틀린 답변 횟수에 따라 문제 난이도가 자동으로 증가
- 1단계: 기본 덧셈 (3+5=?)
- 2단계: 상식 문제 (한국의 수도는?)
- 3단계: 곱셈 (7×9=?)
- 4단계: 나눗셈 (84÷12=?)
- 5단계: 패턴 인식 문제

응답 시간 분석 - 과도하게 빠르거나 늦은 응답을 감지하여 자동화 프로그램 차단

토큰 기반 검증 - 문제 생성, 답안 검증, 최종 승인의 3단계 토큰 시스템으로 보안 강화

IP 기반 모니터링 - IP 주소별 시도 횟수를 추적하여 의심스러운 패턴 감지

사용자 행동 분석 - 마우스 움직임, 입력 패턴, 브라우저 헤더 정보 등 비정상적 행동 모니터링

문제 새로고침 지원 - 사용자가 어려운 문제를 새로 받을 수 있는 기능

인증 시 재인증 방지 - 세션 유지를 통한 사용자 편의성 향상</code></pre></div></div><p></p><p><br /></p><p>​</p><p><span style="font-weight:bold;"><span style="font-size:24px;">라이선스</span></span></p><p>t2CAPTCHA는 그누보드5와 웹 보안의 발전을 위하여 코드를 공개합니다.<br /></p><p>아래의 사항만 지킨다면 누구나 자유롭게 배포할 수 있습니다.</p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">1. 개인(또는 사업자 자체) 사용을 위한 코드 수정 허용
2. 자체 웹사이트 사용을 위한 수정 허용
3. 수정 버전의 배포/공개 시 무료 오픈소스로 배포 필수
4. 원본 및 수정버전의 상업적 유료 배포 불가</code></pre></div></div><p></p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:24px;">다운로드</span></span></p><p><br /></p><p><a href="https://dsclub.kr/service/captcha" target="_blank" style="text-decoration:none;" rel="nofollow noreferrer noopener"><span style="font-weight:bold;font-style:italic;color:rgb(1,135,254);">t2CAPTCHA</span>(https://dsclub.kr/service/captcha)</a></p><p>​</p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:24px;">설치방법</span></span></p><p>​</p><p><span style="font-size:19px;font-weight:bold;">그누보드5 환경</span></p><p>1. /plugin/ 디렉토리에 첨부파일의 압축을 풀어 t2captcha 폴더를 업로드 합니다.<br /></p><p><br /></p><p>2. 관리자 페이지 - 환경설정 &gt; 기본환경설정으로 이동, 캡챠 선택 항목에서 t2CAPTCHA를 선택합니다.</p><p><br /></p><p>3. config_form.php에서 다음과 같이 수정합니다:</p><p><br /></p><p>1) 도움말 설명 추가<br /></p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">&lt;?php echo help('사용할 캡챠를 선택합니다.&lt;br&gt;
1) Kcaptcha 는 그누보드5의 기본캡챠입니다. ( 문자입력 )&lt;br&gt;
2) reCAPTCHA V2 는 구글에서 서비스하는 원클릭 형식의 간편한 캡챠입니다. ( 모바일 친화적 UI )&lt;br&gt;
3) Invisible reCAPTCHA 는 구글에서 서비스하는 안보이는 형식의 캡챠입니다. ( 간혹 퀴즈를 풀어야 합니다. )&lt;br&gt;
4) t2CAPTCHA 는 로컬에서 동작하는 자체 캡챠입니다.'); ?&gt;<br /></code></pre></div></div><p></p><p><br /></p><p>2) 옵션 항목 추가</p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">&lt;option value="t2captcha" &lt;?php echo get_selected($config['cf_captcha'], 't2captcha'); ?&gt;&gt;t2CAPTCHA&lt;/option&gt;</code></pre></div></div><p></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">다른 플랫폼 환경</span></span></p><p>아래의 사항들을 고려하여 직접 일부를 구현해야 합니다.</p><p>​</p><p>t2CAPTCHA 기본 파일 구조:</p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">t2captcha/
├── t2captcha.html
├── t2captcha-api.php
├── t2captcha.class.php
└── css/
└── t2captcha.css</code></pre></div></div><p></p><p><br /></p><p>검증 시스템 설정</p><p>t2captcha.class.php의 검증 로직을 환경에 맞게 적절히 수정해야 합니다.</p><p>​</p><p>예제:</p><p></p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">&lt;!-- HTML 폼에 CAPTCHA 추가 --&gt;
​
&lt;form method="POST" action="your-handler.php"&gt;
&lt;!-- 기존 폼 필드들 --&gt;
​
&lt;!-- t2CAPTCHA 추가 --&gt;
​
&lt;div id="captcha-container"&gt;&lt;/div&gt;
​
&lt;button type="submit"&gt;전송&lt;/button&gt;
​
&lt;/form&gt;
​
&lt;script src="t2captcha.js"&gt;&lt;/script&gt;
​
&lt;script&gt;
// CAPTCHA 초기화
const captcha = new T2Captcha('captcha-container');
&lt;/script&gt;
​
&lt;?php
// PHP 검증 코드
require_once 't2captcha.class.php';
​
$responseToken = $_POST['t2captcha-response'] ?? '';
​
if (empty($responseToken)) {
die('CAPTCHA를 완료해주세요');
}
​
$captcha = new T2Captcha();
if (!$captcha-&gt;verifySuccessToken($responseToken)) {
die('CAPTCHA 검증에 실패했습니다');
}
​
// CAPTCHA 통과, 폼 처리 진행
echo '폼이 성공적으로 전송되었습니다!';
?&gt;</code></pre></div></div><p></p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:24px;">필수 환경</span></span></p><p><span style="color:rgb(125,74,254);font-weight:bold;">PHP 7.0</span> <span style="font-weight:bold;color:rgb(237,47,39);">이상</span><br /></p><p>​</p><p>*이 게시물은 <a href="https://dsclub.kr/service/editor" target="_blank" style="text-decoration:none;" rel="nofollow noreferrer noopener"><b><i style="color:rgb(1,135,254);">T2Editor</i></b></a><b><i></i></b>로 작성되었습니다.​​​​​​​​​​​​​​​​</p><p>​<br /></p><p>#t2CAPTCHA #CAPTCHA #캡챠 #그누보드5 #플러그인</p>]]></description>
<dc:creator>Tak2</dc:creator>
<dc:date>2025-08-28T04:13:56+09:00</dc:date>
</item>


<item>
<title>소프트웨어 버전 관리 가이드라인 [시맨틱 버저닝(Semantic Versioning]</title>
<link>https://dsclub.kr/code/1406</link>
<description><![CDATA[<p>형식: MAJOR.MINOR.PATCH (ex 2.3.5)</p><p>MAJOR(메인 버전): 호환성이 깨지는 큰 구조 변경 시 1씩 올림</p><p>MINOR(사이드 버전): 하위 호환성 유지되는 새 기능 추가 시 1씩 올림</p><p>PATCH(수정 버전): 버그 수정·작은 개선 시 1씩 올림</p><p>프리릴리스 태그(선택): -alpha, -beta, -rc 등으로 안정화 단계 표시</p><p>모두 정수 증가로 통일하고 의미별로 자리 수를 구분</p><p><br /></p><p><br /></p><p>-alpha: 초기 개발 단계의 프리릴리스.</p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">​핵심 기능만 간단히 구현되어 있고, 불안정하거나 자주 깨질 수 있음</code></pre></div></div><p>​<br /></p><p>-beta: 기능 완성 단계의 프리릴리스.</p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">​주요 기능은 다 들어가 있지만, 아직 최종 안정화 작업(버그 잡기, 성능 개선 등)이 남아 있음</code></pre></div></div><p><br /></p><p>-rc: 정식 출시 직전 버전.</p><div class="t2-media-block t2-code-block" contenteditable="false"><div style="width:100%;margin:0px auto;"><pre contenteditable="false"><code style="white-space:pre;">​실제 릴리즈에 큰 문제가 없을 것으로 예상되는 상태로, 마지막 검증만 남아 있는 버전</code></pre></div></div>]]></description>
<dc:creator>Tak2</dc:creator>
<dc:date>2025-08-07T00:16:08+09:00</dc:date>
</item>


<item>
<title>Twave 테마 최적화</title>
<link>https://dsclub.kr/code/1405</link>
<description><![CDATA[<p>Twave테마는 common.php의 용량을 아주 일부를 줄인 버전인 ccommon.php를 회원 프로필, 메시지 페이지에서 로그인 상태에서 사용했다.<br /></p><p>하지만 원본인 common.php를 최대한 줄인 것이 아니라 성능이 좋기는 어렵다.</p><p>다음은 클로드 ai를 활용해 회원 관련 부분만 남긴 ccommon.php 업데이트 버전이다. 기존 822줄에서 420줄이 되었다.</p><p><br /></p><p><br /></p><div class="t2-code-block"><pre><code>&lt;?php</code></pre><p>/*******************************************************************************</p><p>** 회원 관련 공통 변수, 상수, 코드</p><p>*******************************************************************************/</p><p>error_reporting( E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_ERROR | E_WARNING | E_PARSE | E_USER_ERROR | E_USER_WARNING );</p><p>​<br /></p><p>// 보안설정이나 프레임이 달라도 쿠키가 통하도록 설정</p><p>header('P3P: CP="ALL CURa ADMa DEVa TAIa OUR BUS IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE LOC OTC"');</p><p>​<br /></p><p>if (!defined('G5_SET_TIME_LIMIT')) define('G5_SET_TIME_LIMIT', 0);</p><p>@set_time_limit(G5_SET_TIME_LIMIT);</p><p>​<br /></p><p>if( version_compare( PHP_VERSION, '5.2.17' , '&lt;' ) ){</p><p>    die(sprintf('PHP 5.2.17 or higher required. Your PHP version is %s', PHP_VERSION));</p><p>}</p><p>​<br /></p><p>//==========================================================================================================================</p><p>// extract($_GET); 명령으로 인해 page.php?_POST[var1]=data1&amp;_POST[var2]=data2 와 같은 코드가 _POST 변수로 사용되는 것을 막음</p><p>//--------------------------------------------------------------------------------------------------------------------------</p><p>$ext_arr = array ('PHP_SELF', '_ENV', '_GET', '_POST', '_FILES', '_SERVER', '_COOKIE', '_SESSION', '_REQUEST',</p><p>                  'HTTP_ENV_VARS', 'HTTP_GET_VARS', 'HTTP_POST_VARS', 'HTTP_POST_FILES', 'HTTP_SERVER_VARS',</p><p>                  'HTTP_COOKIE_VARS', 'HTTP_SESSION_VARS', 'GLOBALS');</p><p>$ext_cnt = count($ext_arr);</p><p>for ($i=0; $i&lt;$ext_cnt; $i++) {</p><p>    // POST, GET 으로 선언된 전역변수가 있다면 unset() 시킴</p><p>    if (isset($_GET[$ext_arr[$i]]))  unset($_GET[$ext_arr[$i]]);</p><p>    if (isset($_POST[$ext_arr[$i]])) unset($_POST[$ext_arr[$i]]);</p><p>}</p><p>//==========================================================================================================================</p><p>​<br /></p><p>function g5_path()</p><p>{</p><p>    $chroot = substr($_SERVER['SCRIPT_FILENAME'], 0, strpos($_SERVER['SCRIPT_FILENAME'], dirname(__FILE__))); </p><p>    $result['path'] = str_replace('\\', '/', $chroot.dirname(__FILE__)); </p><p>    $server_script_name = preg_replace('/\/+/', '/', str_replace('\\', '/', $_SERVER['SCRIPT_NAME'])); </p><p>    $server_script_filename = preg_replace('/\/+/', '/', str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME'])); </p><p>    $tilde_remove = preg_replace('/^\/\~[^\/]+(.*)$/', '$1', $server_script_name); </p><p>    $document_root = str_replace($tilde_remove, '', $server_script_filename); </p><p>    $pattern = '/.*?' . preg_quote($document_root, '/') . '/i';</p><p>    $root = preg_replace($pattern, '', $result['path']); </p><p>    $port = ($_SERVER['SERVER_PORT'] == 80 || $_SERVER['SERVER_PORT'] == 443) ? '' : ':'.$_SERVER['SERVER_PORT']; </p><p>    $http = 'http' . ((isset($_SERVER['HTTPS']) &amp;&amp; $_SERVER['HTTPS']=='on') ? 's' : '') . '://'; </p><p>    $user = str_replace(preg_replace($pattern, '', $server_script_filename), '', $server_script_name); </p><p>    $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']; </p><p>    if(isset($_SERVER['HTTP_HOST']) &amp;&amp; preg_match('/:[0-9]+$/', $host)) </p><p>        $host = preg_replace('/:[0-9]+$/', '', $host); </p><p>    $host = preg_replace("/[\&lt;\&gt;\'\"\\\'\\\"\%\=\(\)\/\^\*]/", '', $host); </p><p>    $result['url'] = $http.$host.$port.$user.$root; </p><p>    return $result;</p><p>}</p><p>​<br /></p><p>$g5_path = g5_path();</p><p>​<br /></p><p>include_once($g5_path['path'].'/config.php');   // 설정 파일</p><p>​<br /></p><p>unset($g5_path);</p><p>​<br /></p><p>// IIS 에서 SERVER_ADDR 서버변수가 없다면</p><p>if(! isset($_SERVER['SERVER_ADDR'])) {</p><p>    $_SERVER['SERVER_ADDR'] = isset($_SERVER['LOCAL_ADDR']) ? $_SERVER['LOCAL_ADDR'] : '';</p><p>}</p><p>​<br /></p><p>// Cloudflare 환경을 고려한 https 사용여부</p><p>if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &amp;&amp; $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {</p><p>    $_SERVER['HTTPS'] = 'on';</p><p>}</p><p>​<br /></p><p>// multi-dimensional array에 사용자지정 함수적용</p><p>function array_map_deep($fn, $array)</p><p>{</p><p>    if(is_array($array)) {</p><p>        foreach($array as $key =&gt; $value) {</p><p>            if(is_array($value)) {</p><p>                $array[$key] = array_map_deep($fn, $value);</p><p>            } else {</p><p>                $array[$key] = call_user_func($fn, $value);</p><p>            }</p><p>        }</p><p>    } else {</p><p>        $array = call_user_func($fn, $array);</p><p>    }</p><p>​<br /></p><p>    return $array;</p><p>}</p><p>​<br /></p><p>// SQL Injection 대응 문자열 필터링</p><p>function sql_escape_string($str)</p><p>{</p><p>    if(defined('G5_ESCAPE_PATTERN') &amp;&amp; defined('G5_ESCAPE_REPLACE')) {</p><p>        $pattern = G5_ESCAPE_PATTERN;</p><p>        $replace = G5_ESCAPE_REPLACE;</p><p>​<br /></p><p>        if($pattern)</p><p>            $str = preg_replace($pattern, $replace, $str);</p><p>    }</p><p>​<br /></p><p>    $str = call_user_func('addslashes', $str);</p><p>​<br /></p><p>    return $str;</p><p>}</p><p>​<br /></p><p>//==============================================================================</p><p>// SQL Injection 등으로 부터 보호를 위해 sql_escape_string() 적용</p><p>//------------------------------------------------------------------------------</p><p>// magic_quotes_gpc 에 의한 backslashes 제거</p><p>if (7.0 &gt; (float)phpversion()) {</p><p>    if (function_exists('get_magic_quotes_gpc') &amp;&amp; get_magic_quotes_gpc()) {</p><p>        $_POST    = array_map_deep('stripslashes',  $_POST);</p><p>        $_GET     = array_map_deep('stripslashes',  $_GET);</p><p>        $_COOKIE  = array_map_deep('stripslashes',  $_COOKIE);</p><p>        $_REQUEST = array_map_deep('stripslashes',  $_REQUEST);</p><p>    }</p><p>}</p><p>​<br /></p><p>// sql_escape_string 적용</p><p>$_POST    = array_map_deep(G5_ESCAPE_FUNCTION,  $_POST);</p><p>$_GET     = array_map_deep(G5_ESCAPE_FUNCTION,  $_GET);</p><p>$_COOKIE  = array_map_deep(G5_ESCAPE_FUNCTION,  $_COOKIE);</p><p>$_REQUEST = array_map_deep(G5_ESCAPE_FUNCTION,  $_REQUEST);</p><p>//==============================================================================</p><p>​<br /></p><p>// PHP 4.1.0 부터 지원됨</p><p>// php.ini 의 register_globals=off 일 경우</p><p>@extract($_GET);</p><p>@extract($_POST);</p><p>@extract($_SERVER);</p><p>​<br /></p><p>// 완두콩님이 알려주신 보안관련 오류 수정</p><p>// $member 에 값을 직접 넘길 수 있음</p><p>$config = array();</p><p>$member = array('mb_id'=&gt;'', 'mb_level'=&gt; 1, 'mb_name'=&gt; '', 'mb_point'=&gt; 0, 'mb_certify'=&gt;'', 'mb_email'=&gt;'', 'mb_open'=&gt;'', 'mb_homepage'=&gt;'', 'mb_tel'=&gt;'', 'mb_hp'=&gt;'', 'mb_zip1'=&gt;'', 'mb_zip2'=&gt;'', 'mb_addr1'=&gt;'', 'mb_addr2'=&gt;'', 'mb_addr3'=&gt;'', 'mb_addr_jibeon'=&gt;'', 'mb_signature'=&gt;'', 'mb_profile'=&gt;'');</p><p>$g5     = array();</p><p>if( version_compare( phpversion(), '8.0.0', '&gt;=' ) ) { $g5 = array('title'=&gt;''); }</p><p>$g5_debug = array('php'=&gt;array(),'sql'=&gt;array());</p><p>​<br /></p><p>include_once(G5_LIB_PATH.'/hook.lib.php');    // hook 함수 파일</p><p>include_once(G5_LIB_PATH.'/get_data.lib.php');    // 데이타 가져오는 함수 모음</p><p>include_once(G5_LIB_PATH.'/cache.lib.php');     // cache 함수 및 object cache class 모음</p><p>​<br /></p><p>$g5_object = new G5_object_cache();</p><p>​<br /></p><p>//==============================================================================</p><p>// 공통 - 데이터베이스 연결</p><p>//------------------------------------------------------------------------------</p><p>$dbconfig_file = G5_DATA_PATH.'/'.G5_DBCONFIG_FILE;</p><p>if (file_exists($dbconfig_file)) {</p><p>    include_once($dbconfig_file);</p><p>    include_once(G5_LIB_PATH.'/common.lib.php');    // 공통 라이브러리</p><p>​<br /></p><p>    $connect_db = sql_connect(G5_MYSQL_HOST, G5_MYSQL_USER, G5_MYSQL_PASSWORD) or die('MySQL Connect Error!!!');</p><p>    $select_db  = sql_select_db(G5_MYSQL_DB, $connect_db) or die('MySQL DB Error!!!');</p><p>​<br /></p><p>    // mysql connect resource $g5 배열에 저장 - 명랑폐인님 제안</p><p>    $g5['connect_db'] = $connect_db;</p><p>​<br /></p><p>    sql_set_charset(G5_DB_CHARSET, $connect_db);</p><p>    if(defined('G5_MYSQL_SET_MODE') &amp;&amp; G5_MYSQL_SET_MODE) sql_query("SET SESSION sql_mode = ''");</p><p>    if (defined('G5_TIMEZONE')) sql_query(" set time_zone = '".G5_TIMEZONE."'");</p><p>} else {</p><p>    exit;</p><p>}</p><p>//==============================================================================</p><p>​<br /></p><p>//==============================================================================</p><p>// SESSION 설정</p><p>//------------------------------------------------------------------------------</p><p>@ini_set("session.use_trans_sid", 0);    // PHPSESSID를 자동으로 넘기지 않음</p><p>@ini_set("url_rewriter.tags",""); // 링크에 PHPSESSID가 따라다니는것을 무력화함 (해뜰녘님께서 알려주셨습니다.)</p><p>​<br /></p><p>if (isset($SESSION_CACHE_LIMITER))</p><p>    @session_cache_limiter($SESSION_CACHE_LIMITER);</p><p>else</p><p>    @session_cache_limiter("no-cache, must-revalidate");</p><p>​<br /></p><p>ini_set("session.cache_expire", 180); // 세션 캐쉬 보관시간 (분)</p><p>ini_set("session.gc_maxlifetime", 10800); // session data의 garbage collection 존재 기간을 지정 (초)</p><p>ini_set("session.gc_probability", 1); // session.gc_probability는 session.gc_divisor와 연계하여 gc(쓰레기 수거) 루틴의 시작 확률을 관리합니다. 기본값은 1입니다. 자세한 내용은 session.gc_divisor를 참고하십시오.</p><p>ini_set("session.gc_divisor", 100); // session.gc_divisor는 session.gc_probability와 결합하여 각 세션 초기화 시에 gc(쓰레기 수거) 프로세스를 시작할 확률을 정의합니다. 확률은 gc_probability/gc_divisor를 사용하여 계산합니다. 즉, 1/100은 각 요청시에 GC 프로세스를 시작할 확률이 1%입니다. session.gc_divisor의 기본값은 100입니다.</p><p>​<br /></p><p>if (!empty($_SERVER['HTTPS']) &amp;&amp; $_SERVER['HTTPS'] != 'off') {</p><p>    session_set_cookie_params(0, '/', null, true, true);</p><p>} else {</p><p>    session_set_cookie_params(0, '/', null, false, true);</p><p>}</p><p>​<br /></p><p>ini_set("session.cookie_domain", G5_COOKIE_DOMAIN);</p><p>​<br /></p><p>function chrome_domain_session_name(){</p><p>    // 크롬90버전대부터 아래 도메인을 포함된 주소로 접속시 특정조건에서 세션이 생성 안되는 문제가 있을수 있다.</p><p>    $domain_array=array(</p><p>    '.cafe24.com',  // 카페24호스팅</p><p>    '.dothome.co.kr',     // 닷홈호스팅</p><p>    '.phps.kr',     // 스쿨호스팅</p><p>    '.maru.net',    // 마루호스팅</p><p>    );</p><p>​<br /></p><p>    $add_str = '';</p><p>    $document_root_path = str_replace('\\', '/', realpath($_SERVER['DOCUMENT_ROOT']));</p><p>​<br /></p><p>    if( G5_PATH !== $document_root_path ){</p><p>        $add_str = substr_count(G5_PATH, '/').basename(dirname(__FILE__));</p><p>    }</p><p>​<br /></p><p>    if($add_str || (isset($_SERVER['HTTP_HOST']) &amp;&amp; preg_match('/('.implode('|', $domain_array).')/i', $_SERVER['HTTP_HOST'])) ){  // 위의 도메인주소를 포함한 url접속시 기본세션이름을 변경한다.</p><p>        if(! defined('G5_SESSION_NAME')) define('G5_SESSION_NAME', 'G5'.$add_str.'PHPSESSID');</p><p>        @session_name(G5_SESSION_NAME);</p><p>    }</p><p>}</p><p>​<br /></p><p>chrome_domain_session_name();</p><p>​<br /></p><p>if( ! class_exists('XenoPostToForm') ){</p><p>    class XenoPostToForm</p><p>    {</p><p>        public static function g5_session_name(){</p><p>            return (defined('G5_SESSION_NAME') &amp;&amp; G5_SESSION_NAME) ? G5_SESSION_NAME : 'PHPSESSID';</p><p>        }</p><p>​<br /></p><p>        public static function php52_request_check(){</p><p>            $cookie_session_name = self::g5_session_name();</p><p>            if (isset($_REQUEST[$cookie_session_name]) &amp;&amp; $_REQUEST[$cookie_session_name] != session_id())</p><p>                goto_url(G5_BBS_URL.'/logout.php');</p><p>        }</p><p>​<br /></p><p>        public static function check() {</p><p>            $cookie_session_name = self::g5_session_name(); </p><p>​<br /></p><p>            return !isset($_COOKIE[$cookie_session_name]) &amp;&amp; count($_POST) &amp;&amp; ((isset($_SERVER['HTTP_REFERER']) &amp;&amp; !preg_match('~^https://'.preg_quote($_SERVER['HTTP_HOST'], '~').'/~', $_SERVER['HTTP_REFERER']) || ! isset($_SERVER['HTTP_REFERER']) ));</p><p>        }</p><p>​<br /></p><p>        public static function submit($posts) {</p><p>            echo '&lt;form id="f" name="f" method="post"&gt;';</p><p>            echo self::makeInputArray($posts);</p><p>            echo '&lt;/form&gt;';</p><p>            echo '&lt;script&gt;';</p><p>            echo 'document.f.submit();';</p><p>            exit;</p><p>        }</p><p>​<br /></p><p>        public static function makeInputArray($posts) {</p><p>            $res = array();</p><p>            foreach($posts as $k =&gt; $v) {</p><p>                $res[] = self::makeInputArray_($k, $v);</p><p>            }</p><p>            return implode('', $res);</p><p>        }</p><p>​<br /></p><p>        private static function makeInputArray_($k, $v) {</p><p>            if(is_array($v)) {</p><p>                $res = array();</p><p>                foreach($v as $i =&gt; $j) {</p><p>                    $res[] = self::makeInputArray_($k.'['.htmlspecialchars($i).']', $j);</p><p>                }</p><p>                return implode('', $res);</p><p>            }</p><p>            return '&lt;input type="hidden" name="'.$k.'" value="'.htmlspecialchars($v).'" /&gt;';</p><p>        }</p><p>    }</p><p>}</p><p>​<br /></p><p>//==============================================================================</p><p>// 공용 변수</p><p>//------------------------------------------------------------------------------</p><p>// 기본환경설정</p><p>// 기본적으로 사용하는 필드만 얻은 후 상황에 따라 필드를 추가로 얻음</p><p>$config = get_config(true);</p><p>​<br /></p><p>// 본인인증 또는 쇼핑몰 사용시에만 secure; SameSite=None 로 설정합니다.</p><p>if( $config['cf_cert_use'] || (defined('G5_YOUNGCART_VER') &amp;&amp; G5_YOUNGCART_VER) ) {</p><p>    // Chrome 80 버전부터 아래 이슈 대응</p><p>    // https://developers-kr.googleblog.com/2020/01/developers-get-ready-for-new.html?fbclid=IwAR0wnJFGd6Fg9_WIbQPK3_FxSSpFLqDCr9bjicXdzy--CCLJhJgC9pJe5ss</p><p>    if(!function_exists('session_start_samesite')) {</p><p>        function session_start_samesite($options = array())</p><p>        {</p><p>            global $g5;</p><p>​<br /></p><p>            $res = @session_start($options);</p><p>​<br /></p><p>            // IE 브라우저 또는 엣지브라우저 또는 IOS 모바일과 http환경에서는 secure; SameSite=None을 설정하지 않습니다.</p><p>            if (isset($_SERVER['HTTP_USER_AGENT'])) {</p><p>                if (preg_match('/Edge/i', $_SERVER['HTTP_USER_AGENT'])</p><p>                    || preg_match('/(iPhone|iPod|iPad).*AppleWebKit.*Safari/i', $_SERVER['HTTP_USER_AGENT'])</p><p>                    || preg_match('~MSIE|Internet Explorer~i', $_SERVER['HTTP_USER_AGENT'])</p><p>                    || preg_match('~Trident/7.0(; Touch)?; rv:11.0~',$_SERVER['HTTP_USER_AGENT'])</p><p>                    || !(isset($_SERVER['HTTPS']) &amp;&amp; $_SERVER['HTTPS']=='on')) {</p><p>                    return $res;</p><p>                }</p><p>            }</p><p>​<br /></p><p>            $headers = headers_list();</p><p>            krsort($headers);</p><p>            $cookie_session_name = method_exists('XenoPostToForm', 'g5_session_name') ? XenoPostToForm::g5_session_name() : 'PHPSESSID'; </p><p>            foreach ($headers as $header) {</p><p>                if (!preg_match('~^Set-Cookie: '.$cookie_session_name.'=~', $header)) continue;</p><p>                $header = preg_replace('~(; secure; HttpOnly)?$~', '; secure; HttpOnly; SameSite=None', $header);</p><p>                header($header, false);</p><p>                $g5['session_cookie_samesite'] = 'none';</p><p>                break;</p><p>            }</p><p>            return $res;</p><p>        }</p><p>    }</p><p>​<br /></p><p>    session_start_samesite();</p><p>} else {</p><p>    @session_start();</p><p>}</p><p>//==============================================================================</p><p>​<br /></p><p>// 자동로그인 부분에서 첫로그인에 포인트 부여하던것을 로그인중일때로 변경하면서 코드도 대폭 수정하였습니다.</p><p>if (isset($_SESSION['ss_mb_id']) &amp;&amp; $_SESSION['ss_mb_id']) { // 로그인중이라면</p><p>    $member = get_member($_SESSION['ss_mb_id']);</p><p>​<br /></p><p>    // 차단된 회원이면 ss_mb_id 초기화, 또는 세션에 저장된 회원 토큰값을 비교하여 틀리면 초기화</p><p>    if( ($member['mb_intercept_date'] &amp;&amp; $member['mb_intercept_date'] &lt;= date("Ymd", G5_SERVER_TIME)) </p><p>        || ($member['mb_leave_date'] &amp;&amp; $member['mb_leave_date'] &lt;= date("Ymd", G5_SERVER_TIME))</p><p>        || (function_exists('check_auth_session_token') &amp;&amp; !check_auth_session_token($member['mb_datetime'])) </p><p>        ) {</p><p>        set_session('ss_mb_id', '');</p><p>        $member = array();</p><p>    } else {</p><p>        // 오늘 처음 로그인 이라면</p><p>        if (substr($member['mb_today_login'], 0, 10) != G5_TIME_YMD) {</p><p>            // 첫 로그인 포인트 지급</p><p>            insert_point($member['mb_id'], $config['cf_login_point'], G5_TIME_YMD.' 첫로그인', '@login', $member['mb_id'], G5_TIME_YMD);</p><p>​<br /></p><p>            // 오늘의 로그인이 될 수도 있으며 마지막 로그인일 수도 있음</p><p>            // 해당 회원의 접근일시와 IP 를 저장</p><p>            $sql = " update {$g5['member_table']} set mb_today_login = '".G5_TIME_YMDHIS."', mb_login_ip = '{$_SERVER['REMOTE_ADDR']}' where mb_id = '{$member['mb_id']}' ";</p><p>            sql_query($sql);</p><p>        }</p><p>    }</p><p>} else {</p><p>    // 자동로그인 ---------------------------------------</p><p>    // 회원아이디가 쿠키에 저장되어 있다면 (3.27)</p><p>    if ($tmp_mb_id = get_cookie('ck_mb_id')) {</p><p>​<br /></p><p>        $tmp_mb_id = substr(preg_replace("/[^a-zA-Z0-9_]*/", "", $tmp_mb_id), 0, 20);</p><p>        // 관리자는 자동로그인 금지</p><p>        if (strtolower($tmp_mb_id) !== strtolower($config['cf_admin'])) {</p><p>            $sql = " select mb_password, mb_intercept_date, mb_leave_date, mb_email_certify, mb_datetime from {$g5['member_table']} where mb_id = '{$tmp_mb_id}' ";</p><p>            $row = sql_fetch($sql);</p><p>            if($row['mb_password']){</p><p>                $key = md5($_SERVER['SERVER_ADDR'] . $_SERVER['SERVER_SOFTWARE'] . $_SERVER['HTTP_USER_AGENT'] . $row['mb_password']);</p><p>                // 쿠키에 저장된 키와 같다면</p><p>                $tmp_key = get_cookie('ck_auto');</p><p>                if ($tmp_key === $key &amp;&amp; $tmp_key) {</p><p>                    // 차단, 탈퇴가 아니고 메일인증이 사용이면서 인증을 받았다면</p><p>                    if ($row['mb_intercept_date'] == '' &amp;&amp;</p><p>                        $row['mb_leave_date'] == '' &amp;&amp;</p><p>                        (!$config['cf_use_email_certify'] || preg_match('/[1-9]/', $row['mb_email_certify'])) ) {</p><p>                        // 세션에 회원아이디를 저장하여 로그인으로 간주</p><p>                        set_session('ss_mb_id', $tmp_mb_id);</p><p>                        if(function_exists('update_auth_session_token')) update_auth_session_token($row['mb_datetime']);</p><p>​<br /></p><p>                        // 페이지를 재실행</p><p>                        echo "&lt;script type='text/javascript'&gt; window.location.reload(); &lt;/script&gt;";</p><p>                        exit;</p><p>                    }</p><p>                }</p><p>            }</p><p>            // $row 배열변수 해제</p><p>            unset($row);</p><p>        }</p><p>    }</p><p>    // 자동로그인 end ---------------------------------------</p><p>}</p><p>​<br /></p><p>// 회원, 비회원 구분</p><p>$is_member = $is_guest = false;</p><p>$is_admin = '';</p><p>if (isset($member['mb_id']) &amp;&amp; $member['mb_id']) {</p><p>    $is_member = true;</p><p>    $is_admin = is_admin($member['mb_id']);</p><p>    $member['mb_dir'] = substr($member['mb_id'],0,2);</p><p>} else {</p><p>    $is_guest = true;</p><p>    $member['mb_id'] = '';</p><p>    $member['mb_level'] = 1; // 비회원의 경우 회원레벨을 가장 낮게 설정</p><p>}</p><p>​<br /></p><p>if ($is_admin != 'super') {</p><p>    // 접근가능 IP</p><p>    $cf_possible_ip = trim($config['cf_possible_ip']);</p><p>    if ($cf_possible_ip) {</p><p>        $is_possible_ip = false;</p><p>        $pattern = explode("\n", $cf_possible_ip);</p><p>        for ($i=0; $i&lt;count($pattern); $i++) {</p><p>            $pattern[$i] = trim($pattern[$i]);</p><p>            if (empty($pattern[$i]))</p><p>                continue;</p><p>​<br /></p><p>            $pattern[$i] = str_replace(".", "\.", $pattern[$i]);</p><p>            $pattern[$i] = str_replace("+", "[0-9\.]+", $pattern[$i]);</p><p>            $pat = "/^{$pattern[$i]}$/";</p><p>            $is_possible_ip = preg_match($pat, $_SERVER['REMOTE_ADDR']);</p><p>            if ($is_possible_ip)</p><p>                break;</p><p>        }</p><p>        if (!$is_possible_ip)</p><p>            die ("&lt;meta charset=utf-8&gt;접근이 가능하지 않습니다.");</p><p>    }</p><p>​<br /></p><p>    // 접근차단 IP</p><p>    $is_intercept_ip = false;</p><p>    $pattern = explode("\n", trim($config['cf_intercept_ip']));</p><p>    for ($i=0; $i&lt;count($pattern); $i++) {</p><p>        $pattern[$i] = trim($pattern[$i]);</p><p>        if (empty($pattern[$i]))</p><p>            continue;</p><p>​<br /></p><p>        $pattern[$i] = str_replace(".", "\.", $pattern[$i]);</p><p>        $pattern[$i] = str_replace("+", "[0-9\.]+", $pattern[$i]);</p><p>        $pat = "/^{$pattern[$i]}$/";</p><p>        $is_intercept_ip = preg_match($pat, $_SERVER['REMOTE_ADDR']);</p><p>        if ($is_intercept_ip)</p><p>             goto_url("https://dsclub.kr/block.html");</p><p>    }</p><p>}</p><p>​<br /></p><p>// 회원 본인인증 관련 체크</p><p>if($is_member &amp;&amp; !$is_admin &amp;&amp; (!defined("G5_CERT_IN_PROG") || !G5_CERT_IN_PROG) &amp;&amp; $config['cf_cert_use'] &lt;&gt; 0 &amp;&amp; $config['cf_cert_req']) { // 본인인증이 필수일때</p><p>    if ((empty($member['mb_certify']) || (!empty($member['mb_certify']) &amp;&amp; strlen($member['mb_dupinfo']) == 64))) { // di로 인증되어 있거나 본인인증이 안된 계정일때</p><p>        goto_url(G5_BBS_URL."/member_cert_refresh.php");</p><p>    }</p><p>}</p><p>​<br /></p><p>ob_start();</p><p>​<br /></p><p>// 자바스크립트에서 go(-1) 함수를 쓰면 폼값이 사라질때 해당 폼의 상단에 사용하면</p><p>// 캐쉬의 내용을 가져옴. 완전한지는 검증되지 않음</p><p>$gmnow = gmdate('D, d M Y H:i:s') . ' GMT';</p><p>header('Expires: 0'); // rfc2616 - Section 14.21</p><p>header('Last-Modified: ' . $gmnow);</p><p>header('Cache-Control: no-store, no-cache, must-revalidate'); // HTTP/1.1</p><p>header('Cache-Control: pre-check=0, post-check=0, max-age=0'); // HTTP/1.1</p><p>header('Pragma: no-cache'); // HTTP/1.0</p><p>​<br /></p><p>run_event('common_header');</p><p>?&gt;</p></div><p><br /></p>]]></description>
<dc:creator>Tak2</dc:creator>
<dc:date>2025-06-07T19:56:42+09:00</dc:date>
</item>


<item>
<title>T2Editor 서비스 페이지</title>
<link>https://dsclub.kr/code/1404</link>
<description><![CDATA[<p style="text-align:center;"><a href="https://dsclub.kr/service/editor" target="_blank" rel="nofollow noreferrer noopener">https://dsclub.kr/service/editor</a></p><p><br /></p><p style="text-align:center;"><a href="https://dsclub.kr/service/editor/open" target="_blank" rel="nofollow noreferrer noopener">Open_T2Editor</a><br /></p>]]></description>
<dc:creator>Tak2</dc:creator>
<dc:date>2025-04-16T08:09:58+09:00</dc:date>
</item>


<item>
<title>T2Editor 개발 및 관리 가이드 (claude ai)</title>
<link>https://dsclub.kr/code/1403</link>
<description><![CDATA[<p><b>버전: 4.0 (2025.03.16)</b><br /></p><p><br /></p><p><b><span style="font-size:19px;">목차</span></b></p><p>1. 개요</p><p>2. 설치 및 구성</p><p>3. 코어 컴포넌트</p><p>4. 주요 기능</p><p>5. 커스터마이징</p><p>6. 문제 해결</p><p>7. 보안 고려사항</p><p>​<br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">1. 개요</span></span></p><p>T2Editor는 그누보드5 환경에 최적화된 WYSIWYG 에디터로, 한국 사용자를 위해 설계되었습니다. 현대적인 UI와 다양한 편집 기능을 제공합니다.</p><p>주요 기능</p><p>* 텍스트 편집: 글자 크기, 색상, 정렬 등 서식 옵션</p><p>* 미디어 관리: 이미지 업로드(WebP 변환), YouTube 삽입</p><p>* 테이블 기능: 테이블 생성 및 편집, CSV 내보내기</p><p>* 파일 첨부: PDF, ZIP, 오디오 파일 지원</p><p>* PDF 뷰어: 내장형 PDF 문서 열람</p><p>* 자동 저장: 작업 손실 방지</p><p>* HTML 내보내기: HTML 스킨을 통한 HTML 문서 내보내기</p><p>​<br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">2. 설치 및 구성</span></span></p><p><span style="font-weight:bold;">2.1 시스템 <span style="font-size:16px;">요구사항</span></span></p><p>* 서버: PHP 7.2 이상</p><p>* 그누보드: 그누보드5 최신 버전</p><p>* 브라우저: 최신 Chrome, Firefox, Safari, Edge</p><p><br /></p><p><span style="font-weight:bold;">2.2 설치 프로세스</span></p><p>1. 파일 업로드</p><p>    * 다운로드한 파일의 압축을 풀어 압축 해제한 폴더 안의 t2editor 폴더를 그누보드 설치 경로의 /plugin/editor/디렉토리에 업로드</p><p><br /></p><p><span style="font-weight:bold;">2.3 권한 설정 </span></p><div class="t2-code-block"><pre><code># 업로드 디렉토리 권한 설정</code></pre><p>chmod 707 그누보드5루트디렉토리/data/editor/</p><p>​<br /></p><p># 파일 권한 설정</p><p>chmod 755 그누보드5루트디렉토리/plugin/editor/t2editor/file_upload.php</p><p>chmod 755 그누보드5루트디렉토리/plugin/editor/t2editor/image_upload.php</p></div><p><br /></p><p><br /></p><p><span style="font-size:16px;font-weight:bold;">2.4 그누보드 설정 전체 사이트 적용</span></p><p>    * 관리자 &gt; 환경설정 &gt; 기본환경설정 &gt; 에디터 선택: 'T2Editor'</p><p><span style="font-weight:bold;">특정 게시판 적용</span></p><p>    * 관리자 &gt; 게시판관리 &gt; 게시판 수정 &gt; 게시판 에디터 선택: 'T2Editor'</p><p><br /></p><p><span style="font-size:16px;font-weight:bold;">2.5. 미디어 블록 스타일 설정</span></p><p><span style="background-color:rgb(243,243,243);"> head.sub.php </span> 또는 <span style="background-color:rgb(243,243,243);"> view.skin.php </span> 파일에 추가:</p><div class="t2-code-block"><pre><code> &lt;link href="&lt;?php echo G5_PLUGIN_URL ?&gt;/editor/t2editor/css/t2content.css" rel="stylesheet"&gt;</code></pre></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">2.6 업로드 디렉토리</span></p><p>기본 저장 경로:</p><p>* 이미지:</p><div class="t2-code-block"><pre><code>/data/editor/[YYMMDD]/</code></pre></div><p><br /></p><p>* 파일:</p><div class="t2-code-block"><pre><code> /data/editor/t2editor_[YYYYMMDD]/</code></pre></div><p><br /></p><p><br /></p><p>​<br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">3. 코어 컴포넌트</span></span></p><p><span style="font-weight:bold;">3.1 파일 구조</span></p><div class="t2-code-block"><pre><code>/plugin/editor/t2editor/</code></pre><p>├── js/</p><p>│   └── t2editor.js   # t2editor 핵심 js파일</p><p>│   └── pdf.min.js   # pdf 뷰어용 js 파일</p><p>│   └── jszip.min.js   # pdf 뷰어의 파일 압축 기능을 위한 js 파일</p><p>├── css/</p><p>│   └── t2editor.css   # 에디터 스타일</p><p>│   └── t2content.css   # 게시글 미디어 블록용 스타일</p><p>├── editor.lib.php      # 그누보드5용 에디터 실행 파일</p><p>├── image_upload.php    # 이미지 업로드 파일</p><p>├── file_upload.php     # 파일 업로드 파일</p><p>├── pdf_view.php        # PDF 뷰어</p><p>├── export_html_skin.html # HTML 내보내기 스킨</p><p>├── fonts/              # 폰트 파일</p><p>├── License_en.txt      # 영문 라이센스</p><p>├── License_ko.txt      # 한글 라이센스</p><p>└── readme.txt          # 버전 정보</p></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">3.2 핵심 클래스 구조</span></p><p><span style="background-color:rgb(243,243,243);"> t2editor.js </span> 의 기본 구조:</p><div class="t2-code-block"><pre><code>class T2Editor {</code></pre><p>    constructor(container) { // 초기화 }</p><p>    setupEditor() { // 에디터 설정 }</p><p>    handleCommand() { // 명령어 처리 }</p><p>    // 미디어, 테이블, 자동저장 등의 메서드</p><p>}</p></div><p><br /></p><p><span style="font-weight:bold;">3.3 PHP 통합</span></p><p><span style="background-color:rgb(243,243,243);"> editor.lib.php </span> 주요 함수:</p><p>* <span style="background-color:rgb(243,243,243);"> editor_html() </span>: 에디터 HTML 생성</p><p>* <span style="background-color:rgb(243,243,243);"> get_editor_js() </span>: 폼 제출 시 JavaScript 코드</p><p>* <span style="background-color:rgb(243,243,243);"> chk_editor_js() </span>: 입력 검증 코드</p><p>​<br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">4. 주요 기능</span></span></p><p><span style="font-weight:bold;">4.1 텍스트 편집</span></p><p>기본 서식 명령어:</p><p>* bold, italic, underline, strikeThrough</p><p>* justifyContent: 텍스트 정렬</p><p>* fontSize: 글자 크기</p><p>* foreColor, backColor: 색상</p><p>색상 선택기: 기본 색상 팔레트와 HEX 코드 입력 지원</p><p>실행 취소/다시 실행: 내부 스택을 통한 변경 이력 관리</p><p><br /></p><p><span style="font-weight:bold;">4.2 미디어 관리</span></p><p><span style="font-weight:bold;">이미지 업로드:</span></p><p>showImageUploadModal() → 사용자 파일 선택</p><p>handleMultipleImageUpload() → 서버 업로드</p><p>image_upload.php: WebP 변환 (가능한 경우)</p><p><br /></p><p><span style="font-weight:bold;">미디어블록 삽입:</span></p><p>YouTube:</p><p>* URL 입력 → 비디오 ID 추출 → iframe 생성</p><p>파일 첨부:</p><p>* 지원 파일 형식: ZIP, PDF, TXT, MP3 등</p><p>* 파일 유형별 아이콘 및 미리보기 제공</p><p><br /></p><p><span style="font-weight:bold;">4.3 테이블 기능</span></p><p>테이블 생성:</p><p>* 행/열 수 설정</p><p>* 테이블 스타일 지정 (너비, 테두리 등)</p><p>테이블 편집:</p><p>* 행/열 추가 및 삭제</p><p>* 셀 크기 조절</p><p>* CSV 내보내기</p><p>반응형 테이블:</p><p>* 큰 테이블은 스크롤 컨테이너에 자동 배치</p><p><br /></p><p><span style="font-weight:bold;">4.4 자동 저장</span></p><p>* localStorage를 활용한 임시 저장</p><p>* 토글 가능한 자동 저장 옵션</p><p>* 브라우저 새로고침 후에도 데이터 유지</p><p><br /></p><p><span style="font-weight:bold;">4.5 HTML 내보내기</span></p><p>* 현재 에디터 내용을 독립 실행형 HTML 문서로 변환</p><p>* 미디어 요소 및 스타일 포함</p><p>* 커스텀 템플릿 (export_html_skin.html) 사용</p><p>​<br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">5. 커스터마이징</span></span></p><p><span style="font-weight:bold;">5.1 css 수정</span></p><div class="t2-code-block"><pre><code>/* t2editor.css 수정 예시 */</code></pre><p>.t2-editor-container {</p><p>    /* 스타일 수정 */</p><p>}</p></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">5.2 툴바 버튼 추가:</span></p><p><span style="font-weight:bold;"><span style="font-size:16px;">1. editor.lib.php에 HTML 버튼 추가 </span></span></p><div class="t2-code-block"><pre><code>&lt;button class="t2-btn" data-command="customCommand"&gt;</code></pre><p>    &lt;span class="material-icons"&gt;새아이콘명&lt;/span&gt;</p><p>&lt;/button&gt;</p></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">2. t2editor.js에 추가한 버튼의 명령어 처리 추가 </span></p><div class="t2-code-block"><pre><code>case 'customCommand':</code></pre><p>    this.handleCustomCommand();</p><p>    break;</p></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">5.3 기능 확장</span></p><p><b>1. 업로드 허용 확장자 추가</b></p><p>file_upload.php:</p><p><br /></p><div class="t2-code-block"><pre><code> $allowed_types = ['zip', 'pdf', 'txt', 'mp3', '새확장자'];</code></pre></div><p><br /></p><p><br /></p><p><b>2. 파일 유형 색상 및 아이콘 추가 </b></p><p><br /></p><div class="t2-code-block"><pre><code>getFileColor(type) {</code></pre><p>    const colors = {</p><p>        '새확장자': '#새색상코드',</p><p>    };</p><p>    return colors[type.toLowerCase()] || '#E8B56F';</p><p>}</p></div><p><br /></p><p><br /></p><p><b>3. HTML 템플릿 수정:</b></p><p><br /></p><div class="t2-code-block"><pre><code>* export_html_skin.html 파일 편집</code></pre></div><p><br /></p><p>​<br /></p><p><span style="font-size:19px;font-weight:bold;">6. 문제 해결</span></p><p><span style="font-weight:bold;">6.1 일반적인 문제</span></p><p><span style="font-weight:bold;">6.1.1. 에디터가 로드되지 않음:</span></p><p>* 라이센스 파일 확인: License_en.txt와 License_ko.txt</p><p>* JavaScript 오류: 브라우저 콘솔 확인</p><p><br /></p><p><span style="font-weight:bold;">6.1.2. 이미지/파일 업로드 실패:</span><br /></p><p>* 디렉토리 권한: /data/editor/ 권한 확인</p><p>* PHP 설정: upload_max_filesize 및 post_max_size 확인</p><p>WebP 변환 실패:</p><p>* PHP GD 라이브러리 설치 확인</p><p><br /></p><p><span style="font-weight:bold;">6.1.3. 그누보드5 모바일 페이지에서 에디터가 보이지 않음</span></p><p>데스크톱 페이지에서는 에디터가 보이지만 모바일에서는 보이지 않는 경우:</p><p>1. 그누보드 루트 또는 테마의 config.php 에서 아래와 같이 false를 true로 변경</p><p><br /></p><div class="t2-code-block"><pre><code>define('G5_IS_MOBILE_DHTML_USE', false); </code></pre></div><p><br /></p><p><br /></p><p>2. 환경 설정의 에디터 사용 설정에서 t2editor을 사용으로 설정 또는 게시판의 에디터를 t2editor로 설정</p><p><br /></p><p>3. 게시판 관리 수정에서 DHTML 에디터 사용에 체크</p><p><br /></p><p>*https://sir.kr/qa/399462 참고</p><p><span style="font-weight:bold;"><br /></span></p><p><span style="font-weight:bold;">6.2 심각한 문제</span></p><p>​<a href="http://gmail.com" target="_blank" rel="nofollow noreferrer noopener">dsclub2023@gmail.com</a> 로 문의</p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;"><span style="font-size:19px;">7. 보안 고려사항</span></span></span></p><p><span style="font-weight:bold;">7.1 파일 업로드 보안</span></p><p>파일 유형 제한:</p><p><br /></p><div class="t2-code-block"><pre><code>// 이미지 파일 검증</code></pre><p>if (!preg_match("/\.(jpg|jpeg|gif|png|webp)$/i", $filename)) {</p><p>    continue;</p><p>}</p></div><p><br /></p><p><br /></p><p>파일 이름 무작위화:<br /></p><p><br /></p><div class="t2-code-block"><pre><code>$save_filename = $uid.'_'.time().'.'.$file_ext;</code></pre></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;">7.2 XSS 방지</span></p><p>입력 데이터 검증:</p><p><br /></p><div class="t2-code-block"><pre><code>$escaped_content = str_replace(</code></pre><p>    array("\\", "'", "\r", "\n"),</p><p>    array("\\\\", "\\'", "\\r", "\\n"),</p><p>    $content</p><p>);</p></div><p><br /></p><p>​<br /></p><p><br /></p><p>© 2025 Tak2 (dsclub.kr) | 문의: dsclub2023@gmail.com</p><p>​<br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-03-22T22:09:18+09:00</dc:date>
</item>


<item>
<title>T2Editor (그누보드5 간편 에디터)</title>
<link>https://dsclub.kr/code/1102</link>
<description><![CDATA[<p><br /></p><p><br /></p><div class="t2-media-block"><div style="width:452px;max-width:100%;margin:0px auto;text-align:center;"><img src="https://dsclub.kr/data/editor/93/9358315081739504055674_0.jpeg" alt="9358315081739504055674_0.jpeg" style="width:452px;" /></div></div><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><span style="font-size:30px;font-weight:bold;">T2Editor</span><br /></p><p><br /></p><p>T2Editor는 그누보드5(Gnuboard5)를 위해 설계된 WYSIWYG 웹 에디터입니다. 이 에디터는 사용자 친화적인 인터페이스와 함께 강력한 텍스트 편집 기능을 제공하며, 특히 모바일 환경에서의 사용성을 고려하여 개발되었습니다.<br /></p><p><span style="font-size:19px;font-weight:bold;"><br /></span></p><p><span style="font-size:19px;font-weight:bold;"><br /></span></p><p><span style="font-size:19px;font-weight:bold;"><br /></span></p><p><span style="font-size:19px;font-weight:bold;">작동화면:</span><br /></p><p><br /></p><p><br /></p><div class="t2-media-block"><div style="width:619px;max-width:100%;margin:0px auto;"><img src="https://dsclub.kr/data/editor/10/1028867081738528016968_0.jpeg" alt="1028867081738528016968_0.jpeg" style="width:619px;" /></div></div><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><span style="font-size:19px;font-weight:bold;">주요 기능</span><br /></p><div><p><b><br /></b></p><p><b>직관적인 사용자 인터페이스</b> - 누구나 쉽게 사용할 수 있도록 설계된 UI 제공<br /></p><p><b>모바일 환경 최적화 및 높은 호환성</b> - 안드로이드, iOS를 비롯한 다양한 기기에서 원활한 사용 가능<br /></p><p><b>미디어 미리보기</b> - 삽입한 이미지 및 동영상 미리보기 지원<br /></p><p><b>이미지 및 동영상 삽입/편집</b><br /></p><p> - 이미지 추가(파일/링크) 및 동영상(유튜브) 삽입 가능<br /></p><p> - 이미지 드레그&amp;드롭 지원 및 멀티 업로드 지원</p><p> - 간편 이미지 크기 조절 메뉴 제공</p><p><b>코드 블록 지원</b> - 각종 프로그래밍 코드 입력 가능<br /></p><p><b>텍스트 링크 기능</b> - 원하는 텍스트에 하이퍼링크 추가 가능<br /></p><p><b>글꼴 크기 및 색상조절</b> - 손쉬운 폰트 크기 및 색상 변경<br /></p><p><b>실행 취소/다시 실행 지원</b> - 작업 중 실수해도 쉽게 복구 가능<br /></p><p><b>자동 저장 기능</b> - 입력한 내용이 자동으로 저장되어 데이터 손실 방지<br /></p><p><b>자동 글자 수 측정</b> - 작성한 글자의 개수를 실시간으로 확인 가능<br /></p><p><span style="font-size:24px;font-weight:bold;"><br /></span></p><p><br /></p><p><span style="font-size:24px;font-weight:bold;">라이선스</span><br /></p><p>T2Editor은 그누보드5의 발전을 위하여 코드를 공개합니다.</p><p>아래의 사항만 지킨다면 누구나 자유롭게 배포할 수 있습니다.</p><p>1. 개인(또는 사업자 자체) 사용을 위한 코드 수정 허용</p><p>2. 자체 웹사이트 사용을 위한 수정 허용</p><p>3. 수정 버전의 배포/공개 시 무료 오픈소스로 배포 필수</p><p>3. 원본 및 수정버전의 상업적 유료 배포 불가</p><p><br /></p><p><br /></p><p><span style="font-size:24px;font-weight:bold;">다운로드</span><br /></p><p><b> <a href="https://dsclub.kr/zip/280" target="_blank" rel="nofollow noreferrer noopener">T2Editor Ver1.0</a></b> (2025.02.11)</p><p><br /></p><p> <a href="https://dsclub.kr/zip/284" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver1.5</b></a> (2025.02.17)</p><p>추가 기능:</p><p><b>파일 첨부기능 추가</b> (zip,pdf,txt,mp3)</p><p>- 파일 확장자에 따라 각기 다른 ui 표시 (zip,txt,pdf / mp3,m4a 로 구분</p><p><b>pdf 뷰어 추가</b></p><p>- pdf to jpeg 다운로드 기능 추가 (원본 다운로드 옵션 on/off)</p><p>- 트래픽 절감효과</p><p>- 다크모드/라이트모드 지원</p><p><b>자동 저장기능 on/off</b></p><p><br /></p><p><b><a href="https://dsclub.kr/zip/290" rel="nofollow">T2Editor Ver1.75</a></b> (2025.02.24)</p><p>추가 기능:</p><p><b>폰트 색, 폰트 배경 색 직접 입력 옵션 추가</b></p><p><b>이미지 복사 붙여넣기 기능 추가</b></p><p>오류 수정:<br /></p><p><b>텍스트 링크 걸기 기능 링크 걸기 모달 링크 인식 불가 오류</b></p><p><b>텍스트 붙여넣기 일부만 되던 문제 해결</b></p><p><br /></p><p><b><a href="https://dsclub.kr/zip/298" rel="nofollow">T2Editor Ver2.0</a></b> (2025.02.25)<br /></p><p>추가 기능:</p><p><b>간편 테이블 추가/수정 기능</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/371" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver2.25</b></a><b></b> (2025.02.27)<br /></p><p>오류 수정:</p><p><b>테이블 수정 메뉴 및 테이블 너비 오류</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/898" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver2.5</b></a><b></b> (2025.03.02)</p><p>추가 기능:</p><p><b>작성한 테이블 cvs로 내보내기 기능</b></p><p>(에디터 작성 환경에서 테이블 블록 우측 하단에 다운로드 버튼 추가됨)</p><p><br /></p><p> <a href="https://dsclub.kr/zip/899" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver2.75</b></a><b></b> (2025.03.09)</p><p>추가 기능:</p><p><b>툴 바 사용성 향상</b></p><p>오류 수정:</p><p><b>게시글 수정 시 미디어블록 위 아래로 줄바꿈 추가되는 문제 해결</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/900" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver3.0</b></a> (2025.03.15)</p><p>오류 수정:</p><p><b>파일 첨부 기능을 통한 파일 아이콘 삽입 후 작성 완료된 게시글을 수정할 때 파일 아이콘의 구조가 비정상적으로 되는 문제 해결</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/901" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver3.5</b></a> (2025.03.15)</p><p>기능 추가:</p><p><b>에디터로 작성한 내용 html로 내보내기 기능 추가</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/902" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver3.75</b></a> (2025.03.16)</p><p>오류 수정:</p><p><b>업데이트 도중 T2Editor 베타 1.0 버전의 이미지 업로드 파일이 섞여 이미지 업로드 시 webp로 압축이 되지 않던 문제 수정</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/903" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver4.0</b></a> (2025.03.16)</p><p>오류 수정:</p><p><b>푸른산타님의 게시판 관리자모드 에러 수정 버전 적용</b></p><p><br /></p><p> <a href="https://dsclub.kr/zip/904" target="_blank" rel="nofollow noreferrer noopener"><b>T2Editor Ver4.5</b></a><b></b> (2025.03.23)</p><p>기능 추가:</p><p><b>현재 텍스트 또는 선택한 텍스트의 크기를 글자 크기 리스트에 호버 처리를 통해 나타내는 기능 추가</b></p><p><br /></p><p><br /></p><p><span style="font-size:24px;font-weight:bold;">설치방법</span><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">그누보드5 환경</span></span><br /></p><p>1. /plugin/editor/ 디렉토리에 첨부파일의 압축을 풀어 (압축 해제한)폴더 안의 t2editor을 업로드 합니다.<br /></p><p>2. 관리자 페이지 - 환경설정 &gt; 기본환경설정으로 이동, 에디터 선택 항목에서 t2editor를 선택합니다. 또는 관리자 페이지 - 게시판관리 &gt; 게시판관리 에서 원하는 게시판의 수정 버튼을 눌러 게시판 수정 페이지의 게시판 에디터 선택 항목에서 t2editor를 선택합니다.</p><p>3. 미디어블럭 스타일을 위해</p><p>head.sub.php 또는 view.skin.php에 &lt;link href="&lt;?php echo G5_PLUGIN_URL ?&gt;/editor/t2editor/css/t2content.css" rel="stylesheet"&gt;를 추가해주세요</p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">다른 플랫폼 환경</span></span></p><p><font face="Times New Roman"><span style="font-size:16px;">아래의 사항들을 고려하여 직접 일부를 구현해야 합니다.</span></font></p><p><font face="Times New Roman"><span style="font-size:16px;"><br /></span></font></p><p><span style="font-family:'Times New Roman';font-size:19px;">T2Editor 기본 파일 구조:</span></p><p><span style="font-family:'Times New Roman';font-size:19px;"><br /></span></p><div class="t2-code-block"><p style="font-style:normal;font-size:15px;line-height:normal;font-family:'Times New Roman';"><code>t2editor/</code><code>├── css/</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>│   ├── t2editor.css</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>│   └── t2content.css</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>├── js/</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>│   └── t2editor.js</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>└── image_upload.php</code></p><code></code></div><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">이미지 업로드 설정</span></span></p><p>image_upload.php 파일의 경로 및 권한 설정 필요</p><p>업로드 디렉토리 주소를 환경에 맞게 적절히 수정해야 합니다.</p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">예제:</span></span></p><p><span style="font-weight:bold;"><span style="font-size:19px;"><br /></span></span></p><div class="t2-code-block"><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;! -- CSS --&gt;</code></p><code></code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;link href="/path/to/t2editor.css" rel="stylesheet"&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';min-height:21.8px;"><code><br /></code></p><code></code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;!-- HTML --&gt;</code></p><code></code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;div class="t2-editor-container"&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>    &lt;div class="t2-toolbar"&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>        &lt;!-- 툴바 버튼들 --&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>    &lt;/div&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>    &lt;div class="t2-editor" contenteditable="true"&gt;&lt;/div&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>    &lt;textarea name="content" style="display:none;"&gt;&lt;/textarea&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;/div&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';min-height:21.8px;"><code><br /></code></p><code></code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;!-- JavaScript --&gt;</code></p><code></code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;script src="/path/to/t2editor.js"&gt;&lt;/script&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;script&gt;</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>    const editor = new T2Editor(document.querySelector('.t2-editor-container'));</code></p><code>
</code><p style="font-style:normal;font-size:19px;line-height:normal;font-family:'Times New Roman';"><code>&lt;/script&gt;</code></p><code></code></div><p><br /></p><p><br /></p><p><br /></p><p><span style="font-weight:bold;"><span style="font-size:19px;">필수 환경</span></span></p><p><br /></p><p>PHP 7.x ~<br /></p><p>GD Livrary<br /></p><p><br /></p><p><br /></p><p><br /></p><p><b><span style="font-size:24px;">기여</span></b></p><p>펄스나인(false9)님의  IOS/Safari에서 발생하는 IME, 엔터키 문제 해결 방법 제시.<br /></p><p>jihan006(jihan006)님의 IOS의 IME 문제 해결 관련 자료 제공.<br /></p><p><br /></p><p><br /></p><p>*이 게시물은 T2Editor로 작성되었습니다.</p><p><br /></p></div><p><br /></p><p><br /></p><p><br /></p><p><br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-01-24T23:19:41+09:00</dc:date>
</item>


<item>
<title>groq api 용량 부족할 때</title>
<link>https://dsclub.kr/code/1087</link>
<description><![CDATA[<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">계정을 여러개 파서 api 여러개를 돌려가면서 사용할 수 있도록 했습니다. (클로드의 작품)</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;"><br /></p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">api들을 전송해주는 파일:</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">[code]</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">&lt;?php</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">class APIKeyProvider {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private $config = [</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        'access_key' =&gt; '원하는액세스키입력',</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        'api_keys' =&gt; [</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            '계정1의groq_api키', </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            '계정2의groq_api키',</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            '계정3의groq_api키'</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        ]</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    ];</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private $state_dir;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private $state_file;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    public function __construct() {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $this-&gt;state_dir = dirname(__FILE__) . '/cache';</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $this-&gt;state_file = $this-&gt;state_dir . '/api_key_state.json';</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $this-&gt;initStateFile();</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function initStateFile() {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        if (!file_exists($this-&gt;state_dir)) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            mkdir($this-&gt;state_dir, 0755, true);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        if (!file_exists($this-&gt;state_file)) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            $this-&gt;saveState(0);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function loadState() {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        if (!is_readable($this-&gt;state_file)) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            return 0;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $content = file_get_contents($this-&gt;state_file);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        return $content === false ? 0 : (int)$content;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function saveState($current_index) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        if (!is_writable($this-&gt;state_dir)) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            error_log("Cache directory is not writable: " . $this-&gt;state_dir);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            return false;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        return file_put_contents($this-&gt;state_file, $current_index);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function validateAccessKey($provided_key) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        return hash_equals($this-&gt;config['access_key'], $provided_key);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function getNextKey() {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $current_index = $this-&gt;loadState();</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $next_index = ($current_index + 1) % count($this-&gt;config['api_keys']);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        $this-&gt;saveState($next_index);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        return $this-&gt;config['api_keys'][$next_index];</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    public function handleRequest() {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        header('Content-Type: application/json');</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        header('Access-Control-Allow-Origin: *');</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        header('Access-Control-Allow-Methods: GET');</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            http_response_code(200);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            exit;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        try {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            $access_key = $_GET['access_key'] ?? '';</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            if (!$this-&gt;validateAccessKey($access_key)) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">                throw new Exception('Invalid access key');</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            $api_key = $this-&gt;getNextKey();</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            if (!$api_key) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">                throw new Exception('Failed to get next API key');</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            $this-&gt;sendResponse(true, 'Success', ['api_key' =&gt; $api_key]);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        } catch (Exception $e) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            $this-&gt;sendResponse(false, $e-&gt;getMessage());</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    private function sendResponse($success, $message, $data = []) {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        echo json_encode([</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            'success' =&gt; $success,</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            'message' =&gt; $message,</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            'data' =&gt; $data,</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">            'timestamp' =&gt; time()</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        ]);</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">        exit;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">}</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">if ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    $provider = new APIKeyProvider();</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">    $provider-&gt;handleRequest();</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">}</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">?&gt;</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">[/code]</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;"><br /></p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">설명서:</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">API 키 로테이션 시스템 사용 가이드입니다.</p>
<ol style="font-size:16px;padding:0px 40px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">
   <li>설치 및 초기 설정 [code] // api_provider.php 파일을 웹 서버에 업로드 // cache 디렉토리 권한 설정: chmod 755 cache [/code]</li>
   <li>API 키 설정 [code] private $config = [ 'access_key' =&gt; '원하는액세스키입력', // 이 값을 안전한 키로 변경 'api_keys' =&gt; [ '계정1의groq_api키', '계정2의groq_api키', '계정3의groq_api키' ] ]; [/code]</li>
   <li>기존 코드 수정 방법 [code] // 1. API 키 가져오기 $api_key_url = "<a href="http://your-domain.com/api_provider.php?access_key=%EC%9B%90%ED%95%98%EB%8A%94%EC%95%A1%EC%84%B8%EC%8A%A4%ED%82%A4%EC%9E%85%EB%A0%A5" style="color:#0782c1;" target="_blank" rel="nofollow noreferrer noopener">http://your-domain.com/api_provider.php?access_key=원하는액세스키입력</a>"; $response = file_get_contents($api_key_url); $result = json_decode($response, true);</li>
   </ol>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">if (!$result['success']) { die('API 키 획득 실패: ' . $result['message']); }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">// 2. 획득한 API 키로 Groq API 호출 $url = '<a href="https://api.groq.com/openai/v1/chat/completions" style="color:#0782c1;" target="_blank" rel="nofollow noreferrer noopener">https://api.groq.com/openai/v1/chat/completions</a>'; $api_key = $result['data']['api_key'];</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">$data = array( 'model' =&gt; 'whisper-large-v3-turbo', 'messages' =&gt; array( array( 'role' =&gt; 'user', 'content' =&gt; "다음 게시글의 내용을 핵심적인 내용만 간단히 요약해주세요:\n\n제목: {$title}\n내용: {$content}" ) ), 'max_tokens' =&gt; 500, 'temperature' =&gt; 0.3 ); [/code]</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;"> </p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">cURL 구현 예시</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;"><br /></p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">function getGroqResponse($title, $content) { // API 키 획득 $api_key_url = "<a href="http://your-domain.com/api_provider.php?access_key=%EC%9B%90%ED%95%98%EB%8A%94%EC%95%A1%EC%84%B8%EC%8A%A4%ED%82%A4%EC%9E%85%EB%A0%A5" target="_blank" style="color:#0782c1;" rel="nofollow noreferrer noopener">http://your-domain.com/api_provider.php?access_key=원하는액세스키입력</a>"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $api_key_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $result = json_decode($response, true); if (!$result['success']) { throw new Exception('API 키 획득 실패: ' . $result['message']); } // Groq API 호출 $api_key = $result['data']['api_key']; $url = '<a href="https://api.groq.com/openai/v1/chat/completions" target="_blank" style="color:#0782c1;" rel="nofollow noreferrer noopener">https://api.groq.com/openai/v1/chat/completions</a>'; $data = array( 'model' =&gt; 'whisper-large-v3-turbo', 'messages' =&gt; array( array( 'role' =&gt; 'user', 'content' =&gt; "다음 게시글의 내용을 핵심적인 내용만 간단히 요약해주세요:\n\n제목: {$title}\n내용: {$content}" ) ), 'max_tokens' =&gt; 500, 'temperature' =&gt; 0.3 ); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json', 'Authorization: Bearer ' . $api_key )); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); }</p>
<p style="font-size:16px;padding:0px;color:#333333;font-family:'Malgun Gothic', '맑은 고딕';background-color:#ffffff;">// 사용 예시 try { $result = getGroqResponse("제목", "내용"); print_r($result); } catch (Exception $e) { echo "에러: " . $e-&gt;getMessage(); } </p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-02-01T05:31:34+09:00</dc:date>
</item>


<item>
<title>라즈베리파이5 Ollama 모델 리뷰</title>
<link>https://dsclub.kr/code/1084</link>
<description><![CDATA[<p><span style="font-size:12pt;"><br /></span></p>
<div class="ke-component ke-image-container __se__float-none" style="text-align:center;"><img src="https://dsclub.kr/data/editor/2501/1737334867.png" alt="ea89ed088ae2bbc3f462f61a3015b4ef_1737330179_7931.jpeg" style="width:320px;height:320px;" /></div>
<div class="ke-component ke-image-container __se__float-none" style="text-align:center;"><span style="font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none" style="text-align:left;"><span style="font-size:12pt;">테스트 환경:</span> </div>
<p style="text-align:left;"><span style="font-size:12pt;">라즈베리파이5 8기가 (엑티브 쿨러 장착된 상태 + 야매 쿨링 시스템 </span><a href="https://zod.kr/free/293564" target="_blank" rel="nofollow noreferrer noopener"><span style="font-size:12pt;">https://zod.kr/free/293564</span></a><span style="font-size:12pt;"> - 기본 온도 33도 ) - Ubuntu 24.04.1 LTS - 아이패드 ssh로 접속했을 때 (ssh 앱: termius, 온도 측정앱: ServerBox)</span></p>
<p><br /></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/b858d941f9acbf71d726305e605228e5_1737212914_5967.jpeg" title="b858d941f9acbf71d726305e605228e5_1737212914_5967.jpeg" alt="b858d941f9acbf71d726305e605228e5_1737212914_5967.jpeg" /></div>
<p><br /></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">테스트 기준:</span></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">"뻘짓이 뭔지 설명해봐" 를 입력했을 때 cpu 사용률, 최고 온도, 램 사용량, 걸린 시간</span></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">1. Llama 3.2 1B (1.3GB) </span></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 설치: ollama run llama3.2:1b</span></p>
<p class="p2" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 결과:</span></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/b858d941f9acbf71d726305e605228e5_1737212929_2577.jpeg" title="b858d941f9acbf71d726305e605228e5_1737212929_2577.jpeg" alt="b858d941f9acbf71d726305e605228e5_1737212929_2577.jpeg" /></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 걸린 시간: 약 31초</span></div>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- cpu 사용률: 98.4%</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;"> - 최고온도: 48.5도</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 램 사용률: 28%</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">2. Llama 3.2 3B (2GB)</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 설치: ollama run llama3.2</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 결과:</span></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/b858d941f9acbf71d726305e605228e5_1737212956_119.jpeg" title="b858d941f9acbf71d726305e605228e5_1737212956_119.jpeg" alt="b858d941f9acbf71d726305e605228e5_1737212956_119.jpeg" /></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- cpu 사용률: 92.3%</span></div>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> - 최고온도: 57.3도</span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 램 사용률: 45%</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 걸린 시간: 약 1분 25초</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">3.  Phi 3 Mini 3.8B (1.6GB)</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 설치: ollama run phi3</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 결과:</span></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/b858d941f9acbf71d726305e605228e5_1737212970_6615.jpeg" title="b858d941f9acbf71d726305e605228e5_1737212970_6615.jpeg" alt="b858d941f9acbf71d726305e605228e5_1737212970_6615.jpeg" /></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- cpu 사용률: 99.6%</span></div>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 최고온도: 59.5도</span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 램 사용률: 74%</span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 걸린 시간: 약 1분 27초</span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">4.  Gemma 2 2B (1.6GB) </span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 설치: ollama run gemma2:2b</span></p>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 결과:</span></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/b858d941f9acbf71d726305e605228e5_1737212982_92.jpeg" title="b858d941f9acbf71d726305e605228e5_1737212982_92.jpeg" alt="b858d941f9acbf71d726305e605228e5_1737212982_92.jpeg" /></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- cpu 사용률: 98.5%</span></div>
<p><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 최고온도: 59도</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 램 사용률: 39%</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 걸린 시간: 약 1분 26초</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">Exaone3.5 2.4b (1.6GB)</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 설치: ollama pull exaone3.5:2.4b</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 결과:</span></p>
<div class="ke-component ke-image-container __se__float-none"><img src="https://dsclub.kr/data/editor/2501/ea89ed088ae2bbc3f462f61a3015b4ef_1737330179_7931.jpeg" title="ea89ed088ae2bbc3f462f61a3015b4ef_1737330179_7931.jpeg" alt="ea89ed088ae2bbc3f462f61a3015b4ef_1737330179_7931.jpeg" /></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></div>
<div class="ke-component ke-image-container __se__float-none"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">- cpu 사용률: 72~99%</span></div>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 최고 온도: 51.3도</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 램 사용률: 36%</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">- 걸린 시간: 약 57초</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;"> </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span style="font-family:UICTFontTextStyleBody;font-size:12pt;">*EXAONE 3.5는 LG AI Research에서 개발한 영어와 한국어를 지원하는 생성형 AI 모델 시리즈로, 24억 개에서 320억 개에 이르는 다양한 파라미터 규모로 구성되어 있다고하네요.</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;"><br /><br />소감: </span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">한국어로 사용할거면 굳이 쓸 이유를 모르겠음...</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">영어였다면 결과가 아마 유의미하게 달라졌을 것 같네요.</span></p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">그래도 이렇게까지 한국어를 못하는 건 문제가 있는 것 같습니다.<br />Exaone가 제일 나은 느낌입니다.</span></p>
<p class="p2" style="font-size:19px;line-height:normal;min-height:24px;"><span class="s1" style="font-family:UICTFontTextStyleBody;"></span><span style="font-size:12pt;"> </span></p>
<p class="p2" style="line-height:normal;min-height:24px;"><font size="3">라마 모델은 아래 링크에서 검색할 수 있습니다.</font></p>
<p class="p2" style="line-height:normal;min-height:24px;"><span style="font-family:'맑은 고딕', 'malgun gothic';font-size:16px;">(공식 사이트) </span><a href="https://ollama.com/search" target="_blank" rel="nofollow noreferrer noopener"><span style="font-family:'맑은 고딕', 'malgun gothic';font-size:16px;">https://ollama.com/search</span></a></p>
<p class="p2" style="line-height:normal;min-height:24px;"><span style="font-family:'맑은 고딕', 'malgun gothic';font-size:16px;">여기에서 찾은 모델들은 전부 ollama run 모델명 으로 다운로드 가능합니다.</span></p>
<p class="p2" style="font-size:19px;line-height:normal;min-height:24px;"> </p>
<p class="p1" style="font-size:19px;line-height:normal;"><span class="s1" style="font-family:UICTFontTextStyleBody;font-size:12pt;">*영어는 귀찮아서 기회가 된다면 다음번에</span></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-01-19T00:09:49+09:00</dc:date>
</item>


<item>
<title>vpn 차단 자바스크립트</title>
<link>https://dsclub.kr/code/1083</link>
<description><![CDATA[<p>&lt;script&gt;</p><p>async function getClientIP() {</p><p>    try {</p><p>        const response = await fetch('<a href="https://api64.ipify.org?format=json" rel="nofollow">https://api64.ipify.org?format=json</a>');</p><p>        const data = await response.json();</p><p>        return data.ip;</p><p>    } catch (error) {</p><p>        console.error('Error fetching client IP:', error);</p><p>        return null;</p><p>    }</p><p>}</p><p><br /></p><p>async function detectVPNAndRedirect() {</p><p>    const ip = await getClientIP();</p><p>    if (!ip) {</p><p>        console.error('Unable to retrieve IP address.');</p><p>        return;</p><p>    }</p><p><br /></p><p>    const apiUrl = `<a href="https://api.ipquery.io/$%7Bip%7D%60;" rel="nofollow">https://api.ipquery.io/${ip}`;</a></p><p><br /></p><p>    try {</p><p>        const response = await fetch(apiUrl);</p><p>        const data = await response.json();</p><p><br /></p><p>        if (data.risk &amp;&amp; (data.risk.is_vpn || data.risk.is_proxy || data.risk.is_tor || data.risk.is_datacenter)) {</p><p>            // Redirect VPN/proxy/Tor/datacenter users to block.html</p><p>            window.location.href = '/block.html';</p><p>        } else {</p><p>            console.log(`The IP address ${ip} is not associated with a VPN, proxy, Tor, or datacenter.`);</p><p>        }</p><p>    } catch (error) {</p><p>        console.error('Error while detecting VPN:', error);</p><p>    }</p><p>}</p><p><br /></p><p>// Call the function to detect VPN and redirect if necessary</p><p>detectVPNAndRedirect();</p><p><br /></p><p>&lt;/script&gt;</p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-01-08T02:25:43+09:00</dc:date>
</item>


<item>
<title>Twave 채팅 첨부파일 기능 작동 방식</title>
<link>https://dsclub.kr/code/1082</link>
<description><![CDATA[코드1:<br />이미지 업로드 -&gt;시간, 업로드 순서를 토대로 파일명 지정 후 링크 생성 -&gt; 채팅 입력창에 생성한 파일 링크 삽입 -&gt; 채팅 전송 버튼 자동 클릭<br /><br /><br />코드2:<br />실시간으로 파일링크를 감지하여 확장자, 도메인양식 등을 확인 후 적적한 미디어처리를 함]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2025-01-02T01:51:55+09:00</dc:date>
</item>


<item>
<title>그누보드5 회원 프로필이 출력되지 않는 경우</title>
<link>https://dsclub.kr/code/1080</link>
<description><![CDATA[<p>그누보드5가 돌아가고있는 서버의 (외부)아이피가 바뀌었을 경우 네이버, 사파리 등에서 보안인증서 문제가 발생하며 프로필 아이콘이 출력되지 않는다.<br /><br />이 때 관리자 페이지에서 캐시 삭제, 썸네일 파일 삭제를 진행하면 해당 문제가 해결된다.<br /><br /><br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2024-12-26T09:43:06+09:00</dc:date>
</item>


<item>
<title>그누보드5 링크 썸네일을 게시판 첨부파일 사용하도록 하는 코드 (OG이미지메타테그)</title>
<link>https://dsclub.kr/code/1079</link>
<description><![CDATA[<p>아래의 코드를 head.sub.php의 &lt;/head&gt; 위에 삽입<br /><br /> </p><p>&lt;?php</p><p>function override_og_image() {</p><p>    $html = ob_get_contents();</p><p>    libxml_use_internal_errors(true);</p><p>    </p><p>    $dom = new DOMDocument();</p><p>    @$dom-&gt;loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);</p><p>    $xpath = new DOMXPath($dom);</p><p>    </p><p>    // Find view_image class images</p><p>    $images = $xpath-&gt;query('//a[@class="view_image"]//img');</p><p>    </p><p>    // Extract current OG image</p><p>    preg_match('/&lt;meta\s+property="og:image"\s+content="([^"]*)"[^&gt;]*&gt;/i', $html, $matches);</p><p>    $defaultImageUrl = isset($matches[1]) ? $matches[1] : '<a href="https://dsclub.kr/setting/url_logo.png" rel="nofollow">https://</a>내도메인/이미지';</p><p>    </p><p>    // Select random image or fallback</p><p>    if ($images-&gt;length &gt; 0) {</p><p>        $randomIndex = rand(0, $images-&gt;length - 1);</p><p>        $newImageUrl = $images-&gt;item($randomIndex)-&gt;getAttribute('src');</p><p>        </p><p>        // Validate URL format</p><p>        if (!filter_var($newImageUrl, FILTER_VALIDATE_URL)) {</p><p>            $newImageUrl = $defaultImageUrl;</p><p>        }</p><p>    } else {</p><p>        $newImageUrl = $defaultImageUrl;</p><p>    }</p><p>    </p><p>    // Replace OG image meta tag</p><p>    $pattern = '/&lt;meta\s+property="og:image"\s+content="[^"]*"[^&gt;]*&gt;/i';</p><p>    $replacement = '&lt;meta property="og:image" content="' . htmlspecialchars($newImageUrl, ENT_QUOTES, 'UTF-8') . '"&gt;';</p><p>    $modified_html = preg_replace($pattern, $replacement, $html);</p><p>    </p><p>    ob_clean();</p><p>    echo $modified_html;</p><p>}</p><p><br /></p><p>ob_start();</p><p>register_shutdown_function('override_og_image');</p><p>?&gt;<br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2024-12-25T09:30:48+09:00</dc:date>
</item>


<item>
<title>우분투 php 모든 모듈 설치 코드</title>
<link>https://dsclub.kr/code/1076</link>
<description><![CDATA[<p>#최신버전<br />[code]sudo apt update[/code]<br />[code]sudo apt install php-common php-mysql php-xml php-xmlrpc php-curl php-gd php-imagick php-cli php-dev php-imap php-mbstring php-opcache php-soap php-zip php-intl php-bcmath php-ldap php-ssh2 php-sqlite3 php-pgsql php-redis[/code]<br /></p><p><br /><br /><br />#php7.4의 경우<br /><br />[code]sudo apt update[/code]<br />[code]sudo apt install php7.4-common php7.4-mysql php7.4-xml php7.4-xmlrpc php7.4-curl php7.4-gd php7.4-imagick php7.4-cli php7.4-dev php7.4-imap php7.4-mbstring php7.4-opcache php7.4-soap php7.4-zip php7.4-intl php7.4-bcmath php7.4-ldap php7.4-ssh2 php7.4-sqlite3 php7.4-pgsql php7.4-redis[/code]<br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2024-12-09T22:11:24+09:00</dc:date>
</item>


<item>
<title>프로필 보안기능 강화</title>
<link>https://dsclub.kr/code/1075</link>
<description><![CDATA[<p>DSc(dsclub)의 기반인 Twave에서의 개인 프로필 탭(본인 프로필 수정할 수 있는 화면)의 보안, 팔로우 버그유도 부분을 파악하여 적절하게 차단/리다이렉트 처리하여 버그 또는 보안문제가 일어나지 않도록 수정하였습니다.<br /><br /><br />(Twave 테마에 적용 예정)<br /><br /><br />/member_profile.php (member_profile.skin.php)<br /></p><p>&lt;?php</p><p>if(!$member['mb_id']) alert('로그인후 이용해주세요', G5_BBS_URL.'/login.php'); // 로그인안하면 로그인페이지로</p><p>include_once(G5_LIB_PATH.'/latest.lib.php');</p><p><br /></p><p>// add_stylesheet('css 구문', 출력순서); 숫자가 작을 수록 먼저 출력됨</p><p>add_stylesheet('&lt;link rel="stylesheet" href="'.$member_skin_url.'/style.css"&gt;', 0);</p><p><br /></p><p>// 현재 URL의 mb_id 파라미터 값을 가져옵니다.</p><p>$current_mb_id = isset($_GET['mb_id']) ? $_GET['mb_id'] : '';</p><p><br /></p><p>// 회원의 mb_id 값</p><p>$member_mb_id = $member['mb_id'];</p><p><br /></p><p>// mb_id가 일치하지 않는 경우</p><p>if ($current_mb_id !== $member_mb_id) {</p><p>    // 메시지를 출력합니다.</p><p>    echo "&lt;script&gt;alert('타인의 개인 프로필 탭은 확인할 수 없습니다.');&lt;/script&gt;";</p><p>    </p><p>    // 현재 도메인을 자동으로 가져옵니다.</p><p>    $current_domain = ($_SERVER['HTTPS'] ? "https://" : "http://") . $_SERVER['HTTP_HOST'];</p><p>    </p><p>    // 도메인으로 리다이렉트합니다.</p><p>    echo "&lt;script&gt;window.location.href = '$current_domain';&lt;/script&gt;";</p><p>    }</p><p>?&gt;<br /><br /><br />/profile.php (profile.skin.php)<br /></p><p>&lt;?php</p><p>if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가</p><p>include_once(G5_LIB_PATH.'/latest.lib.php');</p><p><br /></p><p>// add_stylesheet('css 구문', 출력순서); 숫자가 작을 수록 먼저 출력됨</p><p>add_stylesheet('&lt;link rel="stylesheet" href="'.$member_skin_url.'/style.css"&gt;', 0);</p><p><br /></p><p>$member_mb_id = get_member($row['mb_id']);</p><p><br /></p><p>// 현재 URL의 mb_id 파라미터 값 가져오기</p><p>$current_mb_id = isset($_GET['mb_id']) ? $_GET['mb_id'] : null;</p><p><br /></p><p>// 회원 정보의 mb_id</p><p>$member_mb_id = $member['mb_id'];</p><p><br /></p><p>// mb_id가 일치하는지 확인</p><p>if ($current_mb_id === $member_mb_id) {</p><p>    // 일치할 경우 리다이렉트</p><p>    header("Location: /bbs/member_profile.php?mb_id=" . urlencode($member_mb_id));</p><p>    exit; // 스크립트 종료</p><p>}</p><p>?&gt;</p><p><br /></p>]]></description>
<dc:creator>tak2</dc:creator>
<dc:date>2024-10-03T04:00:59+09:00</dc:date>
</item>

</channel>
</rss>
