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>
525 lines
21 KiB
C#
525 lines
21 KiB
C#
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 { }
|
||
}
|
||
}
|
||
}
|