feat: 영수증 인쇄를 TCP→DLL 방식으로 전환

TCP 소켓(PRINTSTART/PRINTEND) 대신 AllThatPayCatReqMC(iCmd=2)로
DLL→MMF 경유 인쇄. GW 포트 변경에 영향받지 않음.

- ProcessPrint: TCP 소켓 제거, DLL CatReqMC(2) 호출로 대체
- FindGWPort: 8080/7779 포트 제외 로직 추가
- 실패 시 FindGWPort+ConnectDLL 재연결 후 1회 재시도
- build.bat, test_print_dll.ps1 추가 (DLL 인쇄 단독 테스트용)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
청춘약국
2026-04-11 10:47:37 +09:00
parent 7f66ee82b6
commit c658ac4c43
4 changed files with 205 additions and 1 deletions

View File

@@ -110,7 +110,9 @@ namespace PaymentBridge
var addr = parts[1]; var addr = parts[1];
if (addr.Contains(":")) if (addr.Contains(":"))
{ {
GW_PORT = addr.Split(':')[1]; string port = addr.Split(':')[1];
if (port == "8080" || port == "7779") continue; // 웹UI/Bridge 포트 제외
GW_PORT = port;
Log("GW Port found: " + GW_PORT); Log("GW Port found: " + GW_PORT);
break; break;
} }
@@ -220,6 +222,12 @@ namespace PaymentBridge
var data = json.Deserialize<dynamic>(body); var data = json.Deserialize<dynamic>(body);
responseString = ProcessCashReceipt("B2", data); responseString = ProcessCashReceipt("B2", data);
} }
else if (path == "/api/print" && request.HttpMethod == "POST")
{
var body = ReadBody(request);
var data = json.Deserialize<dynamic>(body);
responseString = ProcessPrint(data);
}
else else
{ {
responseString = json.Serialize(new { ok = false, error = "Unknown endpoint" }); responseString = json.Serialize(new { ok = false, error = "Unknown endpoint" });
@@ -419,6 +427,89 @@ namespace PaymentBridge
Console.WriteLine(message); Console.WriteLine(message);
} }
static string ProcessPrint(dynamic data)
{
// POS에서 받은 영수증 데이터를 DLL 경유로 인쇄 (MMF → AllthatpayClient → 프린터)
// data: { text: "영수증텍스트", qr_url: "https://...", qr_points: 100 }
try
{
string text = "";
string qrUrl = "";
int qrPoints = 0;
try { text = data["text"] ?? ""; } catch { }
try { qrUrl = data["qr_url"] ?? ""; } catch { }
try { qrPoints = Convert.ToInt32(data["qr_points"]); } catch { }
if (string.IsNullOrEmpty(text))
return json.Serialize(new { ok = false, error = "text is empty" });
// QR 적립 안내 텍스트 추가
if (!string.IsNullOrEmpty(qrUrl) && qrPoints > 0)
{
text += "\n------------------------------------------------\n";
text += " ★ 마일리지 적립 ★\n";
text += " QR 스캔하고 " + qrPoints.ToString("N0") + "P 받으세요!\n";
text += " (유효기간: 30일)\n";
text += "------------------------------------------------\n";
}
// EUC-KR 인코딩
Encoding eucKr = Encoding.GetEncoding("euc-kr");
byte[] textBytes = eucKr.GetBytes(text);
// 패킷 조립: text + FS×4 + ATQR_URL (DLL 방식 — PRINTSTART/END 불필요)
var packet = new System.Collections.Generic.List<byte>();
packet.AddRange(textBytes);
// QR 코드 추가 (ATQR_ 프로토콜)
if (!string.IsNullOrEmpty(qrUrl))
{
byte fs = 0x1C;
packet.Add(fs); packet.Add(fs); packet.Add(fs); packet.Add(fs);
packet.AddRange(Encoding.ASCII.GetBytes("ATQR_" + qrUrl));
}
byte[] buf = packet.ToArray();
// DLL 미연결 시 재연결
if (!isConnected)
{
FindGWPort();
ConnectDLL();
}
// DLL 경유 인쇄 (iCmd=2)
int result = AllThatPayCatReqMC(2, buf, buf.Length);
if (result != 0)
{
Log("Print OK (DLL): " + text.Length + " chars, QR=" + (!string.IsNullOrEmpty(qrUrl)));
return json.Serialize(new { ok = true, method = "dll", has_qr = !string.IsNullOrEmpty(qrUrl), bytes_sent = buf.Length });
}
// DLL 실패 → 재연결 후 1회 재시도
Log("Print DLL failed, reconnecting...");
FindGWPort();
ConnectDLL();
result = AllThatPayCatReqMC(2, buf, buf.Length);
if (result != 0)
{
Log("Print OK (DLL retry): " + text.Length + " chars, QR=" + (!string.IsNullOrEmpty(qrUrl)));
return json.Serialize(new { ok = true, method = "dll_retry", has_qr = !string.IsNullOrEmpty(qrUrl), bytes_sent = buf.Length });
}
return json.Serialize(new { ok = false, error = "DLL CatReqMC(2) returned 0" });
}
catch (Exception ex)
{
Log("ProcessPrint error: " + ex.Message);
return json.Serialize(new { ok = false, error = ex.Message });
}
}
static void Cleanup() static void Cleanup()
{ {
try try

Binary file not shown.

8
build.bat Normal file
View File

@@ -0,0 +1,8 @@
@echo off
cd /d "%~dp0"
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /nologo /platform:x86 /target:winexe /win32icon:shc.ico /reference:System.dll /reference:System.Drawing.dll /reference:System.Windows.Forms.dll /reference:System.Web.Extensions.dll /out:PaymentBridge.exe PaymentBridge.cs
if %errorlevel%==0 (
echo BUILD SUCCESS
) else (
echo BUILD FAILED
)

105
test_print_dll.ps1 Normal file
View File

@@ -0,0 +1,105 @@
# 32비트 PowerShell에서 AllthatModule.dll iCmd=2 영수증출력 테스트
# 실행: C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File test_print_dll.ps1
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class AllThatPay {
private const string DLL = @"C:\Program Files (x86)\AllthatpayClient\AllthatModule.dll";
[DllImport(DLL, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayOpenModule(string gwIp, string gwPort, string atpIp, string atpPort, string bizNo, string ediType, int optFlag);
[DllImport(DLL, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayCloseModule();
[DllImport(DLL, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayCatReqMC(int iCmd, byte[] pInBuf, int iInLength);
[DllImport(DLL, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayRetCon();
[DllImport(DLL, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr AllThatPayRetData(int iPosition);
}
"@
# --- GW 포트 자동 탐지 ---
$gwPort = $null
$proc = Get-Process -Name "AllthatpayClient" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($proc) {
$netstat = netstat -ano 2>$null
foreach ($line in $netstat) {
if ($line -match "LISTENING" -and $line -match "\s$($proc.Id)\s*$") {
if ($line -match ":(\d+)\s") {
$port = [int]$Matches[1]
if ($port -ne 8080 -and $port -ne 7779) {
$gwPort = $port.ToString()
break
}
}
}
}
}
if (-not $gwPort) {
Write-Host "[ERROR] GW port not found" -ForegroundColor Red
exit 1
}
Write-Host "[INFO] GW Port: $gwPort" -ForegroundColor Cyan
# --- DLL 연결 ---
$r = [AllThatPay]::AllThatPayOpenModule("127.0.0.1", $gwPort, "", "", "8134500294", "P01", 0)
Write-Host "[INFO] OpenModule: $r (1=OK)" -ForegroundColor $(if ($r -eq 1) {"Green"} else {"Red"})
if ($r -ne 1) {
Write-Host "[ERROR] OpenModule failed" -ForegroundColor Red
exit 1
}
# --- 영수증 텍스트 (EUC-KR) ---
$eucKr = [System.Text.Encoding]::GetEncoding("euc-kr")
$text = "================================================`n"
$text += " [ 청 춘 약 국 ]`n"
$text += " 경기 양주시 양주역로 7-3 1층`n"
$text += " TEL: 033-481-7390`n"
$text += "================================================`n"
$text += "`n"
$text += " ** DLL iCmd=2 인쇄 테스트 **`n"
$text += " 시간: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`n"
$text += "`n"
$text += "------------------------------------------------`n"
$text += " ★ 마일리지 적립 ★`n"
$text += " QR 스캔하고 100P 받으세요!`n"
$text += " (유효기간: 30일)`n"
$text += "------------------------------------------------`n"
$text += "`n`n"
$textBytes = $eucKr.GetBytes($text)
# --- QR 코드 추가: FS×4 + ATQR_URL (명세서 CMD 2 규격) ---
$qrUrl = "https://shc.pharmq.kr/claim?t=test_dll_print"
[byte]$fs = 0x1C
$qrTag = $eucKr.GetBytes("ATQR_" + $qrUrl)
$buf = New-Object System.Collections.Generic.List[byte]
$buf.AddRange($textBytes)
$buf.Add($fs); $buf.Add($fs); $buf.Add($fs); $buf.Add($fs)
$buf.AddRange($qrTag)
$packet = $buf.ToArray()
Write-Host "[INFO] iCmd=2 send ($($packet.Length) bytes, text=$($textBytes.Length) + QR=$($qrTag.Length))..." -ForegroundColor Yellow
# --- iCmd=2 영수증출력 ---
try {
$pr = [AllThatPay]::AllThatPayCatReqMC(2, $packet, $packet.Length)
Write-Host "[INFO] CatReqMC(2): $pr" -ForegroundColor $(if ($pr -ne 0) {"Green"} else {"Red"})
} catch {
Write-Host "[ERROR] CatReqMC exception: $_" -ForegroundColor Red
}
# --- 정리 ---
try { [AllThatPay]::AllThatPayCloseModule() | Out-Null } catch {}
Write-Host "[DONE]" -ForegroundColor Cyan