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(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 if (path == "/api/print" && request.HttpMethod == "POST") { var body = ReadBody(request); var data = json.Deserialize(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(); 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(); 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 { } } } }