메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

AutoIt에서 sqlplus.exe 호출 결과를 그리드

데브카페

SET MARKUP CSV ON QUOTE ON으로 sqlplus 출력을 CSV로 받아 파싱 → JSON으로 변환하는 방식입니다. 자격증명은 conn.ini에 저장해서 HTTP body로 평문 전송하지 않도록 했습니다.

구조 추가


project/
├── main.au3
├── conn.ini          ← 새로 추가 (접속정보)
├── sqlite3.dll
└── web/index.html


conn.ini (예시)


[prod]
user=scott
pass=tiger
tns=ORCL
nls=KOREAN_KOREA.AL32UTF8

[dev]
user=hr
pass=hr
tns=XEPDB1
nls=KOREAN_KOREA.AL32UTF8


tns는 tnsnames.ora의 엔트리명, 또는 //host:port/service 형태도 가능.

main.au3 에 추가 (상단 include 아래)

#include <Constants.au3>

Global $g_sConnIni = @ScriptDir & "\conn.ini"


Switch $sPath 안에 케이스 두 개 추가:

Case "/api/oracle/connections"
    _ApiOraConns($iSock)
Case "/api/oracle/query"
    _ApiOraQuery($iSock, $sBody)


그리고 파일 끝에 함수 추가:


; ─────────────────────────────────────────────
; Oracle 연동
; ─────────────────────────────────────────────
Func _ApiOraConns($iSock)
    Local $aSec = IniReadSectionNames($g_sConnIni)
    Local $s = '{"ok":true,"connections":['
    If IsArray($aSec) Then
        For $i = 1 To $aSec[0]
            If $i > 1 Then $s &= ","
            $s &= _JsonStr($aSec[$i])
        Next
    EndIf
    $s &= ']}'
    _SendJson($iSock, $s)
EndFunc

Func _ApiOraQuery($iSock, $sBody)
    Local $aSql  = StringRegExp($sBody, '"sql":"((?:\\.|[^"\\])*)"', 1)
    Local $aConn = StringRegExp($sBody, '"conn":"((?:\\.|[^"\\])*)"', 1)
    If Not IsArray($aSql) Or Not IsArray($aConn) Then
        _SendJson($iSock, '{"ok":false,"err":"sql/conn 누락"}')
        Return
    EndIf
    Local $sSQL  = _JsonUnesc($aSql[0])
    Local $sName = _JsonUnesc($aConn[0])

    Local $sUser = IniRead($g_sConnIni, $sName, "user", "")
    Local $sPass = IniRead($g_sConnIni, $sName, "pass", "")
    Local $sTns  = IniRead($g_sConnIni, $sName, "tns",  "")
    Local $sNls  = IniRead($g_sConnIni, $sName, "nls",  "KOREAN_KOREA.AL32UTF8")
    If $sUser = "" Or $sTns = "" Then
        _SendJson($iSock, '{"ok":false,"err":"conn.ini에 [' & $sName & '] 없음"}')
        Return
    EndIf

    ; 임시 SQL 스크립트
    Local $sTmp = @TempDir & "\au_oraq_" & @AutoItPID & ".sql"
    Local $sClean = StringRegExpReplace($sSQL, "[\r\n;\s]+$", "")
    Local $sScript = _
        "SET MARKUP CSV ON QUOTE ON" & @CRLF & _
        "SET FEEDBACK OFF"           & @CRLF & _
        "SET HEADING ON"             & @CRLF & _
        "SET PAGESIZE 50000"         & @CRLF & _
        "SET LINESIZE 32767"         & @CRLF & _
        "SET LONG 4000"              & @CRLF & _
        "SET TRIMSPOOL ON"           & @CRLF & _
        "SET SERVEROUTPUT OFF"       & @CRLF & _
        "WHENEVER SQLERROR EXIT 1"   & @CRLF & _
        $sClean & ";" & @CRLF & _
        "EXIT;" & @CRLF

    Local $hF = FileOpen($sTmp, 2 + 128) ; overwrite + UTF-8
    FileWrite($hF, $sScript)
    FileClose($hF)

    ; NLS_LANG 지정 후 sqlplus -S 실행
    EnvSet("NLS_LANG", $sNls)
    Local $sConnStr = $sUser & '/"' & $sPass & '"@' & $sTns
    Local $sCmd = 'sqlplus -S -L ' & $sConnStr & ' @"' & $sTmp & '"'

    Local $iPID = Run(@ComSpec & ' /c ' & $sCmd, @ScriptDir, _
                     @SW_HIDE, $STDOUT_CHILD + $STDERR_CHILD)
    Local $sOut = "", $sErr = ""
    While ProcessExists($iPID)
        $sOut &= StdoutRead($iPID)
        $sErr &= StderrRead($iPID)
        Sleep(30)
    WEnd
    $sOut &= StdoutRead($iPID)
    $sErr &= StderrRead($iPID)
    FileDelete($sTmp)

    ; ORA-/SP2- 오류 감지
    Local $aOraErr = StringRegExp($sOut & @CRLF & $sErr, _
        "((?:ORA|SP2|TNS)-\d+[^\r\n]*)", 1)
    If IsArray($aOraErr) Then
        _SendJson($iSock, '{"ok":false,"err":' & _JsonStr($aOraErr[0]) & '}')
        Return
    EndIf

    _SendJson($iSock, _CsvOutToJson($sOut))
EndFunc

; sqlplus CSV 출력 → JSON
Func _CsvOutToJson($sRaw)
    Local $aLines = StringSplit(StringStripCR($sRaw), @LF)
    Local $aCols[0], $aRows[0]
    Local $bHeader = False
    For $i = 1 To $aLines[0]
        Local $sLine = $aLines[$i]
        If StringStripWS($sLine, 3) = "" Then ContinueLoop
        ; CSV 라인은 보통 "로 시작하거나 ,를 포함. 그 외 잡라인 스킵
        If StringLeft($sLine, 1) <> '"' And Not StringInStr($sLine, ",") Then
            ContinueLoop
        EndIf
        Local $aFields = _ParseCsvLine($sLine)
        If Not $bHeader Then
            $aCols = $aFields
            $bHeader = True
        Else
            ReDim $aRows[UBound($aRows) + 1]
            $aRows[UBound($aRows) - 1] = $aFields
        EndIf
    Next

    Local $s = '{"ok":true,"columns":['
    For $i = 0 To UBound($aCols) - 1
        If $i > 0 Then $s &= ","
        $s &= _JsonStr($aCols[$i])
    Next
    $s &= '],"rows":['
    For $i = 0 To UBound($aRows) - 1
        If $i > 0 Then $s &= ","
        $s &= "["
        Local $aRow = $aRows[$i]
        For $j = 0 To UBound($aRow) - 1
            If $j > 0 Then $s &= ","
            $s &= _JsonStr($aRow[$j])
        Next
        $s &= "]"
    Next
    $s &= ']}'
    Return $s
EndFunc

Func _ParseCsvLine($sLine)
    Local $aOut[256], $iN = 0
    Local $sCur = "", $bQ = False
    Local $iLen = StringLen($sLine)
    For $i = 1 To $iLen
        Local $c = StringMid($sLine, $i, 1)
        If $bQ Then
            If $c = '"' Then
                If $i < $iLen And StringMid($sLine, $i + 1, 1) = '"' Then
                    $sCur &= '"'
                    $i += 1
                Else
                    $bQ = False
                EndIf
            Else
                $sCur &= $c
            EndIf
        Else
            If $c = '"' Then
                $bQ = True
            ElseIf $c = "," Then
                $aOut[$iN] = $sCur
                $iN += 1
                $sCur = ""
            Else
                $sCur &= $c
            EndIf
        EndIf
    Next
    $aOut[$iN] = $sCur
    $iN += 1
    ReDim $aOut[$iN]
    Return $aOut
EndFunc


web/index.html — 툴바와 그리드 사이에 SQL 패널 추가

다음에 삽입:

<div id="sqlPanel" style="background:#fff;border:1px solid #ddd;
     border-radius:6px;padding:10px;margin-bottom:10px;">
  <div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
    <label>접속:</label>
    <select id="connSel" style="padding:5px;"></select>
    <button onclick="runSQL()">▶ 실행 (Ctrl+Enter)</button>
    <button onclick="document.getElementById('sqlPanel').style.display='none'">접기</button>
  </div>
  <textarea id="sqlArea" rows="5"
    style="width:100%;font-family:Consolas,monospace;font-size:13px;
           padding:6px;box-sizing:border-box;"
    placeholder="SELECT * FROM dual"></textarea>
</div>
<button onclick="document.getElementById('sqlPanel').style.display='block'"
        style="margin-bottom:8px;">🔌 SQL</button>


그리고 <script> 안 loadAll() 위에 추가:


async function loadConns(){
  const j = await (await fetch('/api/oracle/connections')).json();
  const sel = document.getElementById('connSel');
  sel.innerHTML = j.connections.map(c=>`<option>${c}</option>`).join('');
}

async function runSQL(){
  const sql  = document.getElementById('sqlArea').value.trim();
  const conn = document.getElementById('connSel').value;
  if (!sql) return;
  setStatus('Oracle 실행중...');
  const t0 = Date.now();
  const r = await fetch('/api/oracle/query', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body: JSON.stringify({sql, conn})
  });
  const j = await r.json();
  if (!j.ok){
    setStatus('❌ ' + j.err);
    alert(j.err);
    return;
  }
  // 그리드에 결과 표시: 1행=헤더, 이후=데이터
  const data = [j.columns, ...j.rows];
  // 빈 행 패딩
  while (data.length < 30) data.push(Array(j.columns.length).fill(''));
  const el = document.getElementById('sheet');
  el.innerHTML = '';
  sheet = jspreadsheet(el, {
    data,
    columns: j.columns.map((c,i)=>({title:String.fromCharCode(65+i), width:130})),
    minDimensions: [Math.max(j.columns.length,10), Math.max(data.length,30)]
  });
  setStatus(`✅ ${j.rows.length}행 / ${((Date.now()-t0)/1000).toFixed(2)}s`);
}

// Ctrl+Enter 단축키
document.addEventListener('keydown', e=>{
  if (e.ctrlKey && e.key === 'Enter' &&
      document.activeElement.id === 'sqlArea'){
    e.preventDefault(); runSQL();
  }
});

loadConns();


확인 / 주의사항

• sqlplus PATH: Oracle Client 또는 Instant Client의 sqlplus.exe가 PATH에 있어야 함. 없으면 Run 호출에서 sqlplus 대신 전체 경로(C:\app\client\bin\sqlplus.exe) 사용. • NLS_LANG: 한글 깨지면 conn.ini의 nls 값을 DB 캐릭터셋에 맞게 조정. UTF-8 DB면 AL32UTF8, KO16MSWIN949 DB면 KOREAN_KOREA.KO16MSWIN949 + 프론트에서도 인코딩 매칭 필요. • 단일 SELECT 전용: 현재 구조는 SELECT 결과 표시용. DML/DDL은 결과 행이 없어 grid가 비게 됨 — 필요 시 SET FEEDBACK ON으로 두고 raw 출력을 메시지로 반환하는 분기 추가 가능. • 세미콜론 처리: 입력 SQL 끝의 ;은 정규식으로 제거 후 한 번만 붙임. PL/SQL 블록(BEGIN ... END;)은 별도 처리 필요 — 현재는 단일 SELECT 기준. • 결과 크기: 매우 큰 결과셋은 stdout 버퍼/메모리 부담. 운영용은 WHERE ROWNUM <= n 또는 FETCH FIRST n ROWS ONLY로 제한 권장. • conn.ini 보안: 평문 비밀번호이므로 NTFS 권한으로 본인만 읽기 가능하게 설정하거나, AutoIt에서 간단 XOR 암호화 래퍼를 씌우는 것도 가능.

PL/SQL 블록 지원이나 DML 결과 표시, 컬럼별 데이터 타입(NUMBER/DATE) 우측정렬·포맷 등이 필요하면 이어서 만들어 드릴게요.

Comments