다른 명령
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) 우측정렬·포맷 등이 필요하면 이어서 만들어 드릴게요.