프로그램 개요
본 프로젝트는 역사 내 무인 보관함의 보관, 찾기, 결제, 원격 제어를 수행하는 MFC 기반의 키오스크 소프트웨어입니다. 비동기 I/O와 이벤트 기반 아키텍처를 통해 사용자에게 끊김 없는 경험을 제공하며, MQTT와 TCP/IP를 활용하여 관제 서버 및 카드 단말기와 실시간으로 통신합니다.
주요 기능
- 비동기 결제 처리: std::thread와 비동기 소켓을 활용하여 UI 프리징 없는 부드러운 카드/전자화폐 결제 UI 흐름
- 신뢰성 있는 서버 통신: MQTT 프로토콜 기반의 실시간 관제 및 로컬 DB 큐(Queue)를 이용한 오프라인 데이터 보존/재전송 로직 구현
- 하드웨어 통합 제어: 시리얼 통신(RS-232)을 통해 보관함 보드, 지폐/동전 투입기를 정밀하게 제어하며 자동 복구 메커니즘 내장
- 접근성(Accessibility): 고대비 모드, 저자세 모드, 음성 안내 등 사회적 약자를 위한 포괄적 접근성 기능 탑재 및 동적 레이아웃 지원
시스템 특징
- 스레드 안전성 (Thread Safety): std::lock_guard, std::mutex를 통한 정교한 동기화로 멀티스레드 환경에서의 데이터 무결성 보장
- 리소스 관리 (RAII): Modern C++의 std::unique_ptr 및 RAII 패턴을 적극 도입하여 메모리 누수 및 자원 관리 오류 원천 차단
- 유연한 아키텍처: CUI_Page 기반의 확장 가능한 UI 프레임워크와 LayoutUtil을 통한 동적 해상도 대응 설계
기능 상세 구현
UI/세션 관리
- UI 아키텍처: CUI_Page를 상속받아 구현되며, GDI+를 사용하여 고품질 렌더링을 수행합니다.
- 레이아웃 시스템: LayoutUtil을 통해 1080x1920 해상도를 기준으로 CSS 좌표를 매핑
- 저자세 모드 시 UI 요소를 화면 하단으로 자동 재배치합니다.
- 결제: CPaymentManager 단일 진입점 → 카드, 현금, 카카오페이
- 서버 통신: CCommRun(MQTT), CProtocolManager
- 하드웨어: CSUB16_BD_Control, BillAcceptor, CoinAcceptor
사용자 편의 (Accessibility)
접근성 기능은 하단 바를 통해 제공되며, 모든 페이지에서 일관되게 동작합니다.

고대비 모드 | 
저자세 모드 | 
돋보기 모드 | 
음성 안내 |
전체 아키텍처 개요
시스템 구성 요소
UI 구현 및 레이아웃 시스템
1. UI 아키텍처 (CUI_Page)
모든 UI 페이지는 CUI_Page 클래스를 상속받습니다. 이 기본 클래스는 다음과 같은 공통 기능을 캡슐화하여 각 페이지(CUI_P1, CUI_CardPay 등)가 비즈니스 로직에만 집중할 수 있게 합니다.
- GDI+ 렌더링: OnPaint에서 더블 버퍼링(CBufferDC)과 GDI+ Graphics를 사용하여 깜빡임 없는 UI를 그립니다.
- 공통 요소 처리: 배경 이미지, 상단 정보(기기번호, 사용가능 함), 타이머, 그리고 하단 바(Bottom Bar)를 자동으로 렌더링하고 이벤트를 처리합니다.
- 프레임 캐싱: 변경이 없는 정적 요소는 비트맵에 캐싱하여 렌더링 성능을 최적화합니다.
2. 동적 레이아웃 시스템 (LayoutUtil)
다양한 해상도 대응과 저자세 모드 지원을 위해 좌표 계산 로직이 LayoutUtil로 중앙화되었습니다.
- Design Canvas: UI는 기본적으로 1080x1920 해상도를 기준으로 디자인됩니다.
- 좌표 변환 (ScaleCssForState):
- 일반 모드: 디자인 좌표를 현재 클라이언트 영역에 맞춰 스케일링합니다.
- 저자세 모드: UI 요소를 화면 하단의 1080x750 영역으로 압축 및 이동시킵니다. LayoutUtil은 상단 좌표를 자동으로 하단 기준 좌표로 변환하여, 별도의 코드 수정 없이도 저자세 모드를 지원하도록 설계되었습니다.
3. 통합 하단 바 (Bottom Bar)
접근성 기능이 CUI_Page 내부에 통합되어 모든 페이지에서 일관되게 동작합니다.
- 구성: 고대비, 저자세, 돋보기, 사운드 도우미 4개의 버튼으로 구성됩니다.
- 사운드 메뉴: 사운드 버튼 클릭 시 볼륨 조절(+, -), 다시 듣기, 음소거 기능을 제공하는 오버레이 메뉴가 나타납니다. 이는 팝업 윈도우가 아닌 CUI_Page 내부의 레이어드 렌더링으로 구현되어 부드러운 UX를 제공합니다.
인터랙션 및 최적화
키오스크 환경에서의 사용자 경험을 극대화하기 위해 터치 피드백과 접근성 도구(돋보기)에 이펙트가 출력되게 했습니다.
1. 터치 이펙트 (Touch Effect)
사용자가 화면을 터치했을 때 즉각적이고 자연스러운 시각적 피드백을 제공하여 "눌렸다"는 느낌을 확실하게 전달합니다.
- Layered Window: WS_EX_LAYERED와 WS_EX_TRANSPARENT 속성을 가진 오버레이 윈도우를 사용하여, 기존 UI에 영향을 주지 않고 투명 배경 위에 이펙트만 렌더링합니다.
- GDI+ 그래픽: 정적 이미지가 아닌 GDI+를 사용하여 동심원이 퍼져나가는 애니메이션을 코드로 직접 구현했습니다. Alpha Blending을 통해 부드럽게 사라지는 효과를 연출합니다.
- 독립된 스레드 처리 (CTouchThread): 메인 UI 스레드가 바쁠 때(화면 전환, DB 작업 등)에도 터치 반응이 끊기지 않도록, 별도의 UI 스레드에서 이펙트 윈도우를 관리하고 렌더링합니다.
프로젝트 요약
- 역할: 키오스크 아키텍처 설계/핵심 모듈 개발, 결제·하드웨어·서버 연동, UI 상태 라우팅
- 핵심:
- UI 표준화: WM_USER_* 기반 비동기 페이지 전환/상태 반영, 공통 라우팅(CUI_Manager)
- 서버 통신 모듈(CCommRun) 리디자인: 송신 이벤트 루프, 연결 감시, 초기연결/재연결 스레드, DB 큐 싱크
- 결제 통합(CPaymentManager): 카드(UbiAnyCardWin TCP, 거래당 연결), 현금(지폐/동전 시리얼), 카카오페이(서버)
- 하드웨어 제어 안정화: CSUB16_BD_Control 리셋/헬스체크/재시도, SerialPort 전용 워커 + 콜백 설계
- 스케줄러(TaskManager): 주기 작업, 재부팅 확인, 외부 결제 프로세스 감시
- 기술스택: C++(MFC/Win32), GDI+, TCP, MQTT, 시리얼 통신
UbiAnyCardWin 연동
- 거래당 연결(Transaction-per-Connection): 요청마다 Winsock로 127.0.0.1:<포트>에 연결 → 명령 전송 → ACK(0x06 0x06 0x06) 수신 → 응답 수신 → (선택)ACK 송신 → 연결 종료
- 비동기 호출·콜백: CUbiAnyCardHandler::RequestPayment는 즉시 반환하고, 백그라운드 스레드에서 처리 후 onSuccess/onFail 콜백 호출
- 명령 포맷(텍스트):
- 승인: APPROVE S01=...;S02=...;S10=<금액>;S23=<거래시리얼>;...\n
- 취소(신용): CANCEL S01=D4;S02=40;S10=<금액>;S12=<원승인번호>;S13=<원승인일자>;\n
- 취소(전자화폐): APPROVE S01=T2;S02=<TM|EB|RP>;S12=<원거래시리얼>;\n
- 핑: PING\n → PONG\n
- 서버 이벤트 처리:
- EVENT=TERMINAL_STATUS;CODE=...;DETAIL=...: 단말 상태 변경 알림
- EVENT=DEVICE_ERROR;CODE=...;DETAIL=...: 장비 에러 알림
- 에러 핸들링: UbiAnyCardError 열거형을 통해 명시적 에러 코드(TransactionInProgress, Timeout, DeviceError 등)를 반환하고 UI에 적절한 메시지 표시
- 타임아웃: ACK 3초, 응답 최대 60초(요구사항에 따라 조절)
- 진행중 강제 취소: 진행 중이면 소켓을 강제 종료하고, 단말 초기화 INIT를 즉시 전송하여 상태 복원
- 응답 파싱: RC, R04, R09(승인번호), R13(발급사), R23(마스킹 카드번호), S23(시리얼) 등 K=V 파싱 후 PaymentTerminalResponse 구성
- 서버 통보 경로: CPaymentManager::HandleUbiPaymentSuccess에서 코레일 신규 0501 또는 구형 0204로 서버 통보 로직 분기
참고: Doxygen 가독성을 위해 pch.* 및 표준 라이브러리/MFC 심볼은 문서·다이어그램에서 제외합니다.
런타임 흐름
- 앱 시작: SESSIONMANAGER->Init() → CCommRun::Start(...) → MQTT 연결/토픽 설정/워커 스레드 시작 → TaskManager::Start()
- 세션: 보관/찾기 선택 → 라커 선택 → 결제 진입 → 성공 시 문 열기(CSUB16_BD_Control::TryOpenLocker)
- UI: 각 CUI_*는 WM_USER_*로 상태를 수신해 페이지 전환(Processing/Success/Fail/OpenDoor 등)
서버 통신(프로토콜)
- 전송 경로: CProtocolManager가 데이터 포맷 → CCommRun::Send() → MQTT/TCP → 서버
- 수신 경로: MQTT 콜백 → CCommRun::OnRecvData() → 디코딩 → CProtocolManager::ProcessRecvData() → Recv010x/020x/0501
- 재전송 제어: 응답 수신 시 CheckRecv()로 DB의 원본 요청 삭제 → 중복 전송 루프 차단
- 원격 제어(0203): 문열기, 초기화, 상태조회, 재부팅, 웹 결제 준비/요청 등 세부 핸들러 제공
- 코레일 신규 0501: 단말 승인 성공 시 CPaymentManager에서 0501 전송(개점/폐점, 장기보관, 임시찾기 분기 포함). 미사용 환경은 0204를 유지
결제 플로우 상세
A. 카드/전자화폐(UbiAnyCardWin)
1) CUI_CardPay::OnShowWindow 진입 시 PAYMENTMANAGER->RequestPayment(info) 호출 2) CPaymentManager::ProcessUbiPayment()
- CUbiAnyCardHandler::SetCallbacks(onSuccess, onFail) 등록
- PaymentTerminalRequest{ nAmount, nPayType, nUILang } 구성 후 RequestPayment() 호출(즉시 반환)
3) CUbiAnyCardHandler
- 명령 문자열 생성: APPROVE ...;S10=<금액>;S23=<자동생성>;\n
- 백그라운드 스레드에서 연결→ACK 대기(3s)→응답 대기(기본 60s)→응답 파싱→콜백 호출
4) 성공 시 CPaymentManager::HandleUbiPaymentSuccess()
- 0501(신규) 또는 0204(구형)로 서버 통보 데이터 구성 후 CCommRun::Send
- UI에 WM_USER_PAYMENT_SUCCESS 포스트, 문 열기 요청
5) 실패 시 WM_USER_PAYMENT_FAIL로 UI 통지, 상태 해제
취소: 진행 중 강제 취소는 CUbiAnyCardHandler::CancelCurrentTransaction() → 소켓 강종 + INIT 전송
B. 현금(지폐/동전)
- BillAcceptor/CoinAcceptor 상태 콜백으로 누적/진행률 업데이트, 목표 도달 시 과납 거스름 처리(CoinAcceptor::RequestReturnAmount) 후 성공 처리
- UI에는 WM_USER_CASH_* 메시지로 세밀한 진행(에스크로/스택/진행률/거스름 실패)을 전달
카드 결제 데이터 흐름 시퀀스
스레드 모델
- CCommRun
- 송신 루프: DB 큐에서 전송, IsSend(대기) 처리, 실패 로그 적재
- 연결 감시: 주기 keepalive/끊김 감지 → 재연결 스레드 기동
- 초기 연결: 앱 레벨 핸드셰이크(0101) 대기/재시도
- 재연결 스레드: 안전한 Stop→Start 시퀀스 + 최신 Topic 반영
- SerialPort 워커: 포트별 1개 스레드, 데이터 수신→파서(Bill/Coin) 전달→콜백
- TaskManager: 주기성 작업(5s/60s), Stop 즉시 반응
UI 메시지 계약(발췌)
- 결제 공통: WM_USER_PAYMENT_PROGRESS, WM_USER_PAYMENT_SUCCESS, WM_USER_PAYMENT_FAIL, WM_USER_PAYMENT_CANCELED
- 현금: WM_USER_CASH_STARTED, WM_USER_CASH_PROGRESS, WM_USER_RETURN_CHANGE_FAIL, WM_USER_CASH_DEVICE_ERROR
- 카카오페이: WM_USER_KAKAO_QR_START, WM_USER_KAKAO_PAY_OPENDOOR, WM_USER_KAKAO_PAY_FAIL (환경별 옵션)
- 원격제어/상태: WM_USER_RECONNECT_MQTT, WM_USER_GOTO_MAIN_PAGE, WM_USER_UPDATE_STATUS_MSG, WM_USER_WINDOW_REBOOT, WM_USER_WEB_*
왜 메시지 기반인가: UI 스레드 안전성과 페이지 간 결합도 최소화를 위해 비동기 메시지 계약을 사용합니다.
하드웨어 통신/제어
- CSUB16_BD_Control: 리셋/헬스체크/문열기/센서/LED. 실패 시 재시도를 포함하여 안정성 확보
- BillAcceptor: 에스크로/스택/리젝트, 요구금액/권종 제한, 에러 콜백
- CoinAcceptor: 상태 프레임 파싱, 입수/불가 전환, 거스름돈 요청/에러 구분
- SerialPort: Open/Close/Write, DTR/RTS/타임아웃/마스크 옵션, 전용 워커 스레드에서 수신
설정/초기화
- INI/DB 로드: CCommRun::Start 시 DB 연결 정보 로드, PaymentManager는 Admin.ini에서 포트/속도/권종/코인포트 로드
- 초기 통신 핸드셰이크: 0101 교환으로 KIOSK ID 동기화, 시간 동기화 권한 시도 및 실패 시 로그
- UbiAnyCard 포트: CDevice::m_nUbiAnyCardWinPort로 TCP 포트 주입 → CUbiAnyCardHandler::Init(port)
로깅/진단/성능
- 로그: writeLog(...) 단일 포맷 유지. 카드 결제 경로는 전용 카드 로그도 보강
- 디버그/릴리즈: 디버그에서 상세 로그 허용, 릴리즈에서는 요약(거래 식별/성공/실패/타임아웃) 중심
- 타임아웃: UbiAnyCard ACK 3s / 응답 60s, MQTT 연결 대기 5s, 초기 핸드셰이크 20s 등 명시
에러 처리 전략
- 가드 절차: 실행 전 상태 검사(진행중 중복 방지, 핸들 NULL 등), 실패 시 조기 반환
- 예외 최소화: C 스타일 오류 코드/반환값 검사 중심, RAII로 자원 정리 보장
- 복구 절차: UbiAnyCard 취소 시 INIT로 단말 상태 복원, MQTT 재연결 워커 상시 동작
주석 규약
- @brief 한줄 요약, @details 상세 맥락(언제/왜/어떻게), @return, @threadsafe 사용 권장
BOOL CCommRun::Start(...);
빌드/실행
- Visual Studio(MFC).
- 실행 전 UbiAnyCardWin(TCP 서버) 포트를 Admin.ini/환경에 맞게 설정
주요 클래스 구현 예시 - 요약
통신 모듈
CCommRun (서버 통신 관리)
BOOL Start(HWND hMainWnd, LPCSTR szSendTopic, LPCSTR szServerIP,
LPCSTR szServerPort, LPCSTR szID, LPCSTR szPW);
void Stop();
BOOL Send(LPCSTR szCmd, LPCSTR szData, BOOL bIsSend = FALSE, INT nPage = 0);
void OnRecvData(LPSTR &szData);
void Reconnect();
핵심 구현 (CCommRun.cpp):
void CCommRun::OnRecvData(LPSTR& szData)
{
::CoInitialize(0);
CString strRecvData(szData);
{
std::lock_guard<std::mutex> lock(m_dbMutex);
m_db.InsertLog(strTime.GetBuffer(), strTime.GetBuffer(),
_T("1"), strRecvData.GetBuffer(), _T(""), _T(""), _T(""), _T("받은 데이터"));
}
char szDecodeData[512] = { 0, };
if (MT_Decoder(szData, szDecodeData, 512) == TRUE)
{
char* pDataCopy = new char[strlen(szDecodeData) + 1];
strcpy_s(pDataCopy, strlen(szDecodeData) + 1, szDecodeData);
PROTOCOLMANAGER->ProcessRecvData(pDataCopy);
}
::CoUninitialize();
}
CProtocolManager (프로토콜 파싱)
void ProcessRecvData(char* pData);
BOOL Recv010x(char* pData);
BOOL Recv020x(char* pData);
BOOL Recv0501(char* pData);
결제 모듈
CPaymentManager (결제 통합 관리)
bool RequestPayment(const PaymentInfo &paymentInfo);
void HandleUbiPaymentSuccess(std::unique_ptr<PaymentTerminalResponse> pResponse);
bool RequestCancel();
핵심 구현:
bool CPaymentManager::ProcessUbiPayment()
{
CUbiAnyCardHandler::SuccessCallback onSuccess =
[this](std::unique_ptr<PaymentTerminalResponse> pResponse)
{
HandleUbiPaymentSuccess(std::move(pResponse));
};
CUbiAnyCardHandler::FailCallback onFail =
[this](int errorCode, const std::string& message)
{
HandlePaymentFail(errorCode, message);
};
UBIHANDLER->SetCallbacks(onSuccess, onFail);
PaymentTerminalRequest req;
req.nAmount = m_pendingInfo.Amount;
req.nPayType = GetPayTypeFromMethod(m_pendingInfo.Method);
req.nUILang = GetCurrentUILanguage();
return UBIHANDLER->RequestPayment(req);
}
CUbiAnyCardHandler (카드/전자화폐 결제)
bool RequestPayment(const PaymentTerminalRequest &req);
bool RequestCreditCancel(int amount, const std::string &approvalNo, const std::string &approvalDate);
bool TestConnection();
핵심 구현:
bool CUbiAnyCardHandler::ExecuteTransaction(const std::string &command,
std::string &response, uint32_t timeoutMs)
{
SOCKET sock = INVALID_SOCKET;
if (!ConnectToServer(sock))
return false;
if (!SendCommand(sock, command))
{
CloseSocket(sock);
return false;
}
std::string ack;
if (!ReceiveResponse(sock, ack, 3000))
{
CloseSocket(sock);
return false;
}
if (!ReceiveResponse(sock, response, timeoutMs))
{
CloseSocket(sock);
return false;
}
CloseSocket(sock);
return true;
}
std::vector<std::pair<std::string, std::string>> ParseKv(const std::string &s)
{
std::vector<std::pair<std::string, std::string>> result;
size_t pos = 0;
while (pos < s.length())
{
size_t eqPos = s.find('=', pos);
if (eqPos == std::string::npos) break;
size_t semiPos = s.find(';', eqPos);
if (semiPos == std::string::npos) semiPos = s.length();
std::string key = Trim(s.substr(pos, eqPos - pos));
std::string value = Trim(s.substr(eqPos + 1, semiPos - eqPos - 1));
result.emplace_back(key, value);
pos = semiPos + 1;
}
return result;
}
하드웨어 모듈
SerialPort (시리얼 통신 래퍼)
void Open(const CString &portName, const CString &settings);
bool Write(const std::vector<BYTE> &data);
void SetDataCallback(PortDataCallback callback);
핵심 구현:
void SerialPort::ThreadWorker()
{
DWORD eventMask;
BYTE buffer[256];
DWORD bytesRead;
while (!m_bIsStopping)
{
if (!WaitCommEvent(m_hComm, &eventMask, nullptr))
{
std::lock_guard<std::mutex> lock(m_callbackMutex);
if (m_statusCallback)
m_statusCallback(PortStatus::Disconnected, GetLastError());
break;
}
if (eventMask & EV_RXCHAR)
{
if (ReadFile(m_hComm, buffer, sizeof(buffer), &bytesRead, nullptr))
{
std::vector<BYTE> data(buffer, buffer + bytesRead);
std::lock_guard<std::mutex> lock(m_callbackMutex);
if (m_dataCallback)
m_dataCallback(data);
}
}
}
}
CSUB16_BD_Control (보드 제어)
BOOL TryOpenLocker(int nBoxNumber);
void ResetBoard();
BOOL GetBoardStatus();
핵심 구현:
BOOL CSUB16_BD_Control::Initialize(const BoardConfig &config)
{
SDLL_SET_INITIAL(config.nBoxCount + 1, config.nMaxCHN);
SDLL_Set_Polling(TRUE);
if (SDLL_OpenComPort(config.nPort, (char *)config.szBPS) == FALSE)
return FALSE;
m_reconnecter.Open(config.nReconnecterPort, dwError);
for (int i = 1; i <= config.nBoxCount; ++i)
{
SDLL_JOIN_BOX_LINK(i, config.pbyCH[i], config.pbyID[i]);
m_byCH[i] = config.pbyCH[i];
m_byID[i] = config.pbyID[i];
}
return TRUE;
}
BOOL CSUB16_BD_Control::TryOpenLocker(int boxNumber, int maxRetries, int retryIntervalMs)
{
for (int i = 0; i < maxRetries; ++i)
{
writeLog(CStringA().Format("[TryOpenLocker] Attempt %d/%d for Box %d",
i + 1, maxRetries, boxNumber));
BOOL open_command_result = OpenLocker(boxNumber);
Sleep(retryIntervalMs);
if (IsDoorOpen(boxNumber))
{
writeLog("[TryOpenLocker] SUCCESS: Door is confirmed open.");
return TRUE;
}
}
writeLog("[TryOpenLocker] FINAL FAILURE: All attempts failed.");
return FALSE;
}
BOOL CSUB16_BD_Control::IsDoorOpen(int boxNumber) const
{
BYTE channel = m_byCH[boxNumber];
BYTE id = m_byID[boxNumber];
BOOL sensor_raw_value = SDLL_Get_DOORSW_CH(channel, id);
BOOL is_sw_reverse = m_bIsSwReverse;
BOOL is_open = (sensor_raw_value == is_sw_reverse);
writeLog(CStringA().Format("[DoorCheck] Box: %d, CH: %d, ID: %d | "
"SensorRaw: %d, IsSwReverse: %d | Result(IsOpen): %d",
boxNumber, channel, id, sensor_raw_value,
is_sw_reverse, is_open));
return is_open;
}
BillAcceptor (지폐 투입기)
void EnableAcceptance(bool enable);
void StackBill();
void ReturnBill();
핵심 구현 (상태머신):
void BillAcceptor::Update()
{
if (!m_offlineNotified && isLinkDown(kLinkDownDetectMs))
{
if (m_errorCallback)
m_errorCallback(2, _T("장치 미응답/전원 꺼짐으로 추정"));
m_offlineNotified = true;
}
parseReceiveBuffer();
if (m_inflight.active)
{
if (isCommandCompleted(m_inflight.msg.Code))
{
if (m_inflight.msg.Code == Command::EnableAcceptance)
{
if (m_stackedCallback)
m_stackedCallback(m_currentState.nTotalAcceptedAmount);
}
m_inflight = InFlightState{};
}
else if (isTimeout(m_inflight.startTick, CMD_TIMEOUT_MS))
{
handleTimeout();
}
}
sendPeriodicPolling();
}
void BillAcceptor::parseStatusFrame(const std::vector<BYTE> &frame)
{
if (frame.size() < 3) return;
BYTE flag = frame[1];
BYTE data = frame[2];
switch (flag)
{
case 0x1B:
handleStatusFlag(data);
break;
case 0x1C:
handleErrorFlag(data);
break;
case 0x1A:
updateBillCount(data);
break;
}
}
void BillAcceptor::handleStatusFlag(BYTE flag)
{
if (flag & 0x01)
{
long escrowAmount = calcAmountFromBytes(
m_currentState.nBillCount1000,
m_currentState.nBillCount5000,
m_currentState.nBillCount10000);
if (m_escrowCallback)
m_escrowCallback(escrowAmount);
}
if (flag & 0x02)
{
if (m_stackedCallback)
m_stackedCallback(m_currentState.nTotalAcceptedAmount);
}
}
UI/세션 관리
SessionManager (세션 상태 관리)
BOOL Init();
void SetOperationMode(OperationMode mode);
BOOL RequestOpenLocker(int boxNumber, bool isPickup);
핵심 구현:
UINT SessionManager::ThreadOpenLocker(LPVOID lpParam)
{
OpenLockerParams* pParams = (OpenLockerParams*)lpParam;
BOOL bResult = SUB16_CONTROL->TryOpenLocker(pParams->boxNumber);
if (bResult)
{
::PostMessage(pParams->pManager->GetMainWnd(),
WM_USER_DOOR_OPEN_SUCCESS,
pParams->boxNumber, 0);
}
else
{
::PostMessage(pParams->pManager->GetMainWnd(),
WM_USER_DOOR_OPEN_FAIL,
pParams->boxNumber, 0);
}
delete pParams;
return 0;
}
CUI_Manager (UI 라우팅)
BOOL ChangeUI(int nPageID, LPARAM lParam = 0);
void SetAccessibilityMode(bool bEnable);
HWND GetCurrentPageHWND();
핵심 구현:
BOOL CUI_Manager::ChangeUI(int nPageID, LPARAM lParam)
{
if (nPageID < 0 || nPageID >= static_cast<int>(PageId::Count))
return FALSE;
PageId newPageId = static_cast<PageId>(nPageID);
if (m_pCurrentPage)
m_pCurrentPage->Hide();
m_pCurrentPage = (*m_pActivePages)[static_cast<size_t>(newPageId)];
m_currentPageId = newPageId;
if (m_pCurrentPage)
{
m_pCurrentPage->SetParam(lParam);
m_pCurrentPage->Show();
CStringA strLog;
strLog.Format("UI Changed to Page: %d", nPageID);
writeLog(strLog);
return TRUE;
}
return FALSE;
}
런타임 관리
TaskManager (주기 작업 스케줄러)
BOOL Start(HWND hMainWnd);
void Stop();
void CheckAndRunPayProgram();
핵심 구현:
UINT TaskManager::WorkerThread(LPVOID lpParam)
{
TaskManager *pThis = static_cast<TaskManager *>(lpParam);
pThis->CheckAndRunPayProgram();
pThis->PerformDailyTasks();
pThis->PerformRebootTasks();
::PostMessage(pThis->m_hMainWnd, WM_TASK_READY, 1, 0);
while (!pThis->m_bIsStop)
{
DWORD dwWaitResult = WaitForSingleObject(pThis->m_hStopEvent, 1000);
if (dwWaitResult != WAIT_TIMEOUT)
break;
ULONGLONG currentTick = GetTickCount64();
if (currentTick - pThis->m_lastProcessCheckTick > 5000)
{
pThis->CheckAndRunPayProgram();
pThis->m_lastProcessCheckTick = currentTick;
}
if (currentTick - pThis->m_lastDailyTaskTick > 60000)
{
pThis->PerformDailyTasks();
pThis->PerformRebootTasks();
pThis->m_lastDailyTaskTick = currentTick;
}
}
return 0;
}
void TaskManager::CheckAndRunPayProgram()
{
if (!TM_IsUbiAnyCardMutexAlive())
{
writeLog("TaskManager - UbiAnyCard_Ver2 프로세스가 실행되지 않음. 시작 시도...");
CString strExePath;
if (GetModuleFileName(NULL, strExePath.GetBuffer(MAX_PATH), MAX_PATH))
{
strExePath.ReleaseBuffer();
int nFind = strExePath.ReverseFind('\\');
if (nFind != -1)
{
strExePath = strExePath.Left(nFind + 1) + _T("UbiAnyCard_Ver2.exe");
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (CreateProcess(strExePath, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
writeLog("TaskManager - UbiAnyCard_Ver2 시작 성공");
}
else
{
writeLog("TaskManager - UbiAnyCard_Ver2 시작 실패");
}
}
}
}
}
void TaskManager::PerformRebootTasks()
{
SYSTEMTIME st;
GetLocalTime(&st);
if (st.wHour == 3 && st.wMinute < 5)
{
writeLog("TaskManager - Scheduled reboot at 3 AM");
::PostMessage(m_hMainWnd, WM_USER_WINDOW_REBOOT, 0, 0);
}
}
SessionManager (세션 상태 관리)
BOOL Init();
void SetOperationMode(OperationMode mode);
BOOL RequestOpenLocker(int boxNumber, bool isPickup);
핵심 구현:
BOOL SessionManager::RequestOpenLocker(int boxNumber, bool isPickup)
{
if (m_bIsProcessingOpen)
return FALSE;
m_bIsProcessingOpen = TRUE;
OpenLockerParams* pParams = new OpenLockerParams();
pParams->pManager = this;
pParams->boxNumber = boxNumber;
pParams->nSVC = GetCurrentServiceCode();
pParams->isPickup = isPickup;
AfxBeginThread(ThreadOpenLocker, pParams);
return TRUE;
}
이러한 클래스들은 RAII 원칙과 예외 최소화 전략을 따르며, UI 스레드 안전성을 위해 메시지 기반 비동기 패턴을 사용합니다.