commit a0d9b4364a5c3363c59780966e49b132ece5a754 Author: 청춘약국 Date: Fri Mar 27 23:08:29 2026 +0900 Initial commit: Payment Bridge (올댓페이 C# DLL Wrapper) - PaymentBridge.cs: HTTP API 서버 (포트 7779) - /api/status: 연결 상태 확인 - /api/card/approve: 카드 승인 (D1) - /api/card/cancel: 카드 취소 (D2) - /api/card/stop: 결제 진행 중 취소 시도 - /api/cash/receipt: 현금영수증 (B1) - /api/cash/cancel: 현금영수증 취소 (B2) - PaymentStop.cs: 별도 취소 exe (테스트용) - 사용 DLL: - AllthatModule.dll (카드결제) - PosToCatReq.dll (ReqStop) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..355510e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Build outputs +*.exe +*.pdb +*.dll + +# But keep source +!*.cs + +# IDE +.vs/ +*.user diff --git a/PaymentBridge.cs b/PaymentBridge.cs new file mode 100644 index 0000000..6f147ab --- /dev/null +++ b/PaymentBridge.cs @@ -0,0 +1,420 @@ +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(":")) + { + GW_PORT = addr.Split(':')[1]; + 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(body); + responseString = ProcessCard("D1", data); + } + else if (path == "/api/card/cancel" && request.HttpMethod == "POST") + { + var body = ReadBody(request); + var data = json.Deserialize(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(body); + responseString = ProcessCashReceipt("B1", data); + } + else if (path == "/api/cash/cancel" && request.HttpMethod == "POST") + { + var body = ReadBody(request); + var data = json.Deserialize(body); + responseString = ProcessCashReceipt("B2", 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 approvalNo = GetRetData(6); + + 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", 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(); + + 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 void Cleanup() + { + try + { + if (isConnected) AllThatPayCloseModule(); + if (listener != null) listener.Stop(); + if (trayIcon != null) trayIcon.Visible = false; + } + catch { } + } + } +} diff --git a/PaymentStop.cs b/PaymentStop.cs new file mode 100644 index 0000000..309a964 --- /dev/null +++ b/PaymentStop.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +class PaymentStop +{ + private const string POS_DLL = @"C:\Program Files (x86)\AllthatpayClient\PosToCatReq.dll"; + + [DllImport(POS_DLL, CallingConvention = CallingConvention.StdCall)] + public static extern int ReqStop(); + + [DllImport(POS_DLL, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern int ReqStop_TCP(string gwIp, string gwPort); + + static void Main(string[] args) + { + Console.WriteLine("=== Payment Stop ==="); + + try + { + int r1 = ReqStop(); + Console.WriteLine("ReqStop: " + r1); + } + catch (Exception ex) + { + Console.WriteLine("ReqStop error: " + ex.Message); + } + + try + { + int r2 = ReqStop_TCP("127.0.0.1", "49884"); + Console.WriteLine("ReqStop_TCP: " + r2); + } + catch (Exception ex) + { + Console.WriteLine("ReqStop_TCP error: " + ex.Message); + } + } +} diff --git a/payment_bridge.log b/payment_bridge.log new file mode 100644 index 0000000..2725cac --- /dev/null +++ b/payment_bridge.log @@ -0,0 +1,45 @@ +2026-03-23 23:06:26 GW Port found: 49884 +2026-03-23 23:06:26 DLL Connect: OK +2026-03-23 23:06:26 HTTP Server started on port 7779 +2026-03-23 23:07:06 CashReceipt B1 result: 0 +2026-03-23 23:13:34 GW Port found: 49884 +2026-03-23 23:13:34 DLL Connect: OK +2026-03-23 23:13:34 HTTP Server started on port 7779 +2026-03-23 23:14:43 GW Port found: 49884 +2026-03-23 23:14:43 DLL Connect: OK +2026-03-23 23:14:43 HTTP Server started on port 7779 +2026-03-27 22:07:51 GW Port found: 65238 +2026-03-27 22:07:51 ConnectDLL error: 프로그램을 잘못된 형식으로 로드하려고 했습니다. (예외가 발생한 HRESULT: 0x8007000B) +2026-03-27 22:07:51 HTTP Server started on port 7779 +2026-03-27 22:08:39 ProcessStop called +2026-03-27 22:08:39 ProcessStop error: 프로그램을 잘못된 형식으로 로드하려고 했습니다. (예외가 발생한 HRESULT: 0x8007000B) +2026-03-27 22:08:56 GW Port found: 65238 +2026-03-27 22:08:57 DLL Connect: OK +2026-03-27 22:08:57 HTTP Server started on port 7779 +2026-03-27 22:12:33 GW Port found: 65238 +2026-03-27 22:12:33 DLL Connect: OK +2026-03-27 22:12:33 HTTP Server started on port 7779 +2026-03-27 22:12:43 ProcessStop called +2026-03-27 22:12:43 ReqStop_TCP result: 1 +2026-03-27 22:17:10 ProcessStop called +2026-03-27 22:17:10 ReqStop_TCP result: 1 +2026-03-27 22:20:15 GW Port found: 65238 +2026-03-27 22:20:15 DLL Connect: OK +2026-03-27 22:20:15 HTTP Server started on port 7779 +2026-03-27 22:20:24 ProcessStop called +2026-03-27 22:20:25 ReqStop result: 1 +2026-03-27 22:20:25 ReqStop_TCP result: 1 +2026-03-27 22:20:25 Direct ESC/CAN sent to GW +2026-03-27 22:22:53 GW Port found: 65238 +2026-03-27 22:22:54 DLL Connect: OK +2026-03-27 22:22:54 HTTP Server started on port 7779 +2026-03-27 22:23:02 ProcessStop called +2026-03-27 22:23:03 ReqStop result: 1 +2026-03-27 22:23:03 ReqStop_TCP result: 1 +2026-03-27 22:23:03 Direct ESC/CAN sent to GW +2026-03-27 22:25:20 GW Port found: 65238 +2026-03-27 22:25:20 DLL Connect: OK +2026-03-27 22:25:20 HTTP Server started on port 7779 +2026-03-27 22:56:58 GW Port found: 65238 +2026-03-27 22:56:58 DLL Connect: OK +2026-03-27 22:56:58 HTTP Server started on port 7779 diff --git a/shc.ico b/shc.ico new file mode 100644 index 0000000..4103130 Binary files /dev/null and b/shc.ico differ