- card_company (position 9) - acquirer_name (position 10) - approval_datetime, card_no, merchant_no 등 - Flask API 응답과 동일한 필드명 사용
434 lines
17 KiB
C#
434 lines
17 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(":"))
|
|
{
|
|
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<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
|
|
{
|
|
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 void Cleanup()
|
|
{
|
|
try
|
|
{
|
|
if (isConnected) AllThatPayCloseModule();
|
|
if (listener != null) listener.Stop();
|
|
if (trayIcon != null) trayIcon.Visible = false;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
}
|