Files
payment-bridge/PaymentBridge.cs
청춘약국 c658ac4c43 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>
2026-04-11 10:47:37 +09:00

525 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Net;
using System.Threading;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
using System.Web.Script.Serialization;
namespace PaymentBridge
{
public class Program
{
// AllThatPay DLL Imports
private const string DLL_PATH = @"C:\Program Files (x86)\AllthatpayClient\AllthatModule.dll";
private const string POS_DLL_PATH = @"C:\Program Files (x86)\AllthatpayClient\PosToCatReq.dll";
[DllImport(DLL_PATH, 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_PATH, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayCloseModule();
[DllImport(DLL_PATH, CallingConvention = CallingConvention.StdCall)]
public static extern int AllThatPayCatReqMC(int iCmd, byte[] pInBuf, int iInLength);
[DllImport(DLL_PATH, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr AllThatPayRetData(int iPosition);
// PosToCatReq DLL - 결제 진행 중 취소
[DllImport(POS_DLL_PATH, CallingConvention = CallingConvention.StdCall)]
public static extern int ReqStop();
[DllImport(POS_DLL_PATH, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern int ReqStop_TCP(string gwIp, string gwPort);
// 설정
private static string GW_PORT = "49884";
private static string BIZ_NO = "8134500294";
private static string EDI_TYPE = "P01";
private static int HTTP_PORT = 7779;
private static HttpListener listener;
private static NotifyIcon trayIcon;
private static bool isConnected = false;
private static JavaScriptSerializer json = new JavaScriptSerializer();
[STAThread]
static void Main(string[] args)
{
Application.EnableVisualStyles();
// 트레이 아이콘 설정
trayIcon = new NotifyIcon();
trayIcon.Text = "Payment Bridge";
// 커스텀 아이콘 로드
string iconPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "shc.ico");
if (File.Exists(iconPath))
trayIcon.Icon = new Icon(iconPath);
else
trayIcon.Icon = SystemIcons.Application;
trayIcon.Visible = true;
// 컨텍스트 메뉴
var menu = new ContextMenu();
menu.MenuItems.Add(new MenuItem("상태: 시작 중..."));
menu.MenuItems.Add(new MenuItem("-"));
var exitItem = new MenuItem("종료");
exitItem.Click += delegate { Cleanup(); Application.Exit(); };
menu.MenuItems.Add(exitItem);
trayIcon.ContextMenu = menu;
// GW 포트 자동 탐지
FindGWPort();
// HTTP 서버 시작
Thread serverThread = new Thread(StartHttpServer);
serverThread.IsBackground = true;
serverThread.Start();
// DLL 연결
ConnectDLL();
Application.Run();
}
static void FindGWPort()
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo("netstat", "-ano");
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
var proc = System.Diagnostics.Process.Start(psi);
string output = proc.StandardOutput.ReadToEnd();
// AllthatpayClient PID 찾기
foreach (var p in System.Diagnostics.Process.GetProcessesByName("AllthatpayClient"))
{
foreach (var line in output.Split('\n'))
{
if (line.Contains("LISTENING") && line.Contains(p.Id.ToString()))
{
// 포트 추출
var parts = line.Trim().Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
var addr = parts[1];
if (addr.Contains(":"))
{
string port = addr.Split(':')[1];
if (port == "8080" || port == "7779") continue; // 웹UI/Bridge 포트 제외
GW_PORT = port;
Log("GW Port found: " + GW_PORT);
break;
}
}
}
}
}
}
catch (Exception ex)
{
Log("FindGWPort error: " + ex.Message);
}
}
static void ConnectDLL()
{
try
{
int result = AllThatPayOpenModule("127.0.0.1", GW_PORT, "", "", BIZ_NO, EDI_TYPE, 0); // 0=동기모드
isConnected = (result == 1);
UpdateStatus();
Log("DLL Connect: " + (isConnected ? "OK" : "FAIL"));
}
catch (Exception ex)
{
Log("ConnectDLL error: " + ex.Message);
isConnected = false;
}
}
static void UpdateStatus()
{
if (trayIcon.ContextMenu != null && trayIcon.ContextMenu.MenuItems.Count > 0)
{
string status = isConnected ? "연결됨 ✓" : "연결 안됨 ✗";
trayIcon.ContextMenu.MenuItems[0].Text = "상태: " + status + " (port:" + HTTP_PORT + ")";
trayIcon.Text = "Payment Bridge - " + status;
}
}
static void StartHttpServer()
{
try
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:" + HTTP_PORT + "/");
listener.Prefixes.Add("http://127.0.0.1:" + HTTP_PORT + "/");
listener.Start();
Log("HTTP Server started on port " + HTTP_PORT);
while (true)
{
var context = listener.GetContext();
ThreadPool.QueueUserWorkItem(HandleRequest, context);
}
}
catch (Exception ex)
{
Log("HTTP Server error: " + ex.Message);
}
}
static void HandleRequest(object state)
{
var context = (HttpListenerContext)state;
var request = context.Request;
var response = context.Response;
string responseString = "";
try
{
string path = request.Url.AbsolutePath.ToLower();
if (path == "/api/status")
{
responseString = json.Serialize(new { ok = true, connected = isConnected, port = GW_PORT });
}
else if (path == "/api/card/approve" && request.HttpMethod == "POST")
{
var body = ReadBody(request);
var data = json.Deserialize<dynamic>(body);
responseString = ProcessCard("D1", data);
}
else if (path == "/api/card/cancel" && request.HttpMethod == "POST")
{
var body = ReadBody(request);
var data = json.Deserialize<dynamic>(body);
responseString = ProcessCard("D2", data);
}
else if (path == "/api/card/stop" && request.HttpMethod == "POST")
{
// 결제 진행 중 취소
responseString = ProcessStop();
}
else if (path == "/api/cash/receipt" && request.HttpMethod == "POST")
{
var body = ReadBody(request);
var data = json.Deserialize<dynamic>(body);
responseString = ProcessCashReceipt("B1", data);
}
else if (path == "/api/cash/cancel" && request.HttpMethod == "POST")
{
var body = ReadBody(request);
var data = json.Deserialize<dynamic>(body);
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
{
responseString = json.Serialize(new { ok = false, error = "Unknown endpoint" });
}
}
catch (Exception ex)
{
responseString = json.Serialize(new { ok = false, error = ex.Message });
}
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentType = "application/json; charset=utf-8";
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.Close();
}
static string ReadBody(HttpListenerRequest request)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
return reader.ReadToEnd();
}
}
static string ProcessCard(string cmd, dynamic data)
{
// D1: 카드승인, D2: 카드취소
int amount = 0;
int tax = 0;
string installment = "00";
try { amount = Convert.ToInt32(data["amount"]); } catch { }
try { tax = Convert.ToInt32(data["tax"]); } catch { }
try { installment = data["installment"] ?? "00"; } catch { }
// 전문 조립
char fs = (char)0x1C;
string packet = cmd + fs + installment + fs + "0" + fs + amount + fs + tax + fs + fs + fs + fs + fs + fs + fs;
byte[] buf = Encoding.GetEncoding("euc-kr").GetBytes(packet);
// iCmd=3 for D1/D2
int result = AllThatPayCatReqMC(3, buf, buf.Length);
if (result != 0)
{
Thread.Sleep(500);
string retCode = GetRetData(2);
string retMsg = GetRetData(3);
string approvalDatetime = GetRetData(4);
string cardNo = GetRetData(5);
string approvalNo = GetRetData(6);
string retAmount = GetRetData(7);
string merchantNo = GetRetData(8);
string cardCompany = GetRetData(9);
string acquirerName = GetRetData(10);
return json.Serialize(new {
ok = retCode == "0000",
code = retCode,
message = retMsg,
approvalNo = approvalNo,
approval_no = approvalNo,
card_company = cardCompany,
acquirer_name = acquirerName,
approval_datetime = approvalDatetime,
card_no = cardNo,
merchant_no = merchantNo,
ret_amount = retAmount,
cmd = cmd
});
}
return json.Serialize(new { ok = false, error = "DLL returned 0", cmd = cmd });
}
static string ProcessCashReceipt(string cmd, dynamic data)
{
// B1: 현금영수증 발행, B2: 취소
int amount = 0;
int tax = 0;
string phone = "";
string traderType = "00"; // 00=소비자, 01=사업자
try { amount = Convert.ToInt32(data["amount"]); } catch { }
try { tax = Convert.ToInt32(data["tax"]); } catch { }
try { phone = data["phone"] ?? ""; } catch { }
try { traderType = data["traderType"] ?? "00"; } catch { }
int supplyAmt = (int)Math.Floor(amount / 1.1);
int vatAmt = amount - supplyAmt;
// 전문 조립
char fs = (char)0x1C;
string packet = cmd + fs + traderType + fs + supplyAmt + fs + vatAmt + fs + fs + fs + phone + fs + fs + fs + fs;
byte[] buf = Encoding.GetEncoding("euc-kr").GetBytes(packet);
// iCmd=4 for B1/B2
int result = AllThatPayCatReqMC(4, buf, buf.Length);
Log("CashReceipt " + cmd + " result: " + result);
if (result != 0)
{
Thread.Sleep(500);
string retCode = GetRetData(2);
string retMsg = GetRetData(3);
string approvalNo = GetRetData(17);
return json.Serialize(new {
ok = retCode == "0000",
code = retCode,
message = retMsg,
approvalNo = approvalNo,
cmd = cmd
});
}
return json.Serialize(new { ok = false, error = "DLL returned 0 - VAN 현금영수증 미활성화 가능성", cmd = cmd });
}
static string ProcessStop()
{
// 결제 진행 중 취소 (여러 방법 시도)
Log("ProcessStop called");
var results = new System.Collections.Generic.Dictionary<string, object>();
try
{
// 방법 1: ReqStop (직접)
try {
int r1 = ReqStop();
Log("ReqStop result: " + r1);
results["ReqStop"] = r1;
} catch (Exception ex) {
results["ReqStop"] = "error: " + ex.Message;
}
// 방법 2: ReqStop_TCP
try {
int r2 = ReqStop_TCP("127.0.0.1", GW_PORT);
Log("ReqStop_TCP result: " + r2);
results["ReqStop_TCP"] = r2;
} catch (Exception ex) {
results["ReqStop_TCP"] = "error: " + ex.Message;
}
// 방법 3: GW에 직접 취소 바이트 전송
try {
using (var client = new System.Net.Sockets.TcpClient())
{
client.Connect("127.0.0.1", int.Parse(GW_PORT));
var stream = client.GetStream();
// ESC (0x1B) + CAN (0x18) 전송
byte[] cancelBytes = new byte[] { 0x1B, 0x18 };
stream.Write(cancelBytes, 0, cancelBytes.Length);
results["DirectESC"] = "sent";
Log("Direct ESC/CAN sent to GW");
}
} catch (Exception ex) {
results["DirectESC"] = "error: " + ex.Message;
}
return json.Serialize(new { ok = true, results = results });
}
catch (Exception ex)
{
Log("ProcessStop error: " + ex.Message);
return json.Serialize(new { ok = false, error = ex.Message });
}
}
static string GetRetData(int position)
{
try
{
IntPtr ptr = AllThatPayRetData(position);
if (ptr != IntPtr.Zero)
{
return Marshal.PtrToStringAnsi(ptr) ?? "";
}
}
catch { }
return "";
}
static void Log(string message)
{
try
{
string logPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "payment_bridge.log");
File.AppendAllText(logPath, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + message + Environment.NewLine);
}
catch { }
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()
{
try
{
if (isConnected) AllThatPayCloseModule();
if (listener != null) listener.Stop();
if (trayIcon != null) trayIcon.Visible = false;
}
catch { }
}
}
}