Locker Kiosk Korail Ver2 1.0
한국철도 락커 키오스크 시스템
로딩중...
검색중...
일치하는것 없음
Locker Kiosk Korail Ver2

프로그램 개요

본 프로젝트는 역사 내 무인 보관함의 보관, 찾기, 결제, 원격 제어를 수행하는 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)

접근성 기능은 하단 바를 통해 제공되며, 모든 페이지에서 일관되게 동작합니다.


고대비 모드

저자세 모드

돋보기 모드

음성 안내

전체 아키텍처 개요

시스템 구성 요소

dot_inline_dotgraph_1.png

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_LAYEREDWS_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\nPONG\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_* 메시지로 세밀한 진행(에스크로/스택/진행률/거스름 실패)을 전달

카드 결제 데이터 흐름 시퀀스

dot_inline_dotgraph_2.png

스레드 모델

  • CCommRun
    • 송신 루프: DB 큐에서 전송, IsSend(대기) 처리, 실패 로그 적재
    • 연결 감시: 주기 keepalive/끊김 감지 → 재연결 스레드 기동
    • 초기 연결: 앱 레벨 핸드셰이크(0101) 대기/재시도
    • 재연결 스레드: 안전한 Stop→Start 시퀀스 + 최신 Topic 반영
  • SerialPort 워커: 포트별 1개 스레드, 데이터 수신→파서(Bill/Coin) 전달→콜백
  • TaskManager: 주기성 작업(5s/60s), Stop 즉시 반응
dot_inline_dotgraph_3.png

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 연결 정보 로드, PaymentManagerAdmin.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 (서버 통신 관리)

// CCommRun.h 주요 멤버 함수
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); // MQTT 콜백에서 호출
void Reconnect();

핵심 구현 (CCommRun.cpp):

// 수신 데이터 처리 (MQTT 콜백 → 프로토콜 매니저)
void CCommRun::OnRecvData(LPSTR& szData)
{
::CoInitialize(0);
CString strRecvData(szData);
// 1. DB 로그 기록 (뮤텍스 보호)
{
std::lock_guard<std::mutex> lock(m_dbMutex);
m_db.InsertLog(strTime.GetBuffer(), strTime.GetBuffer(),
_T("1"), strRecvData.GetBuffer(), _T(""), _T(""), _T(""), _T("받은 데이터"));
}
// 2. 데이터 디코딩
char szDecodeData[512] = { 0, };
if (MT_Decoder(szData, szDecodeData, 512) == TRUE)
{
// 3. 프로토콜 매니저로 처리 위임
char* pDataCopy = new char[strlen(szDecodeData) + 1];
strcpy_s(pDataCopy, strlen(szDecodeData) + 1, szDecodeData);
PROTOCOLMANAGER->ProcessRecvData(pDataCopy);
}
::CoUninitialize();
}

CProtocolManager (프로토콜 파싱)

// 주요 기능: 수신 데이터 파싱 → UI/비즈니스 로직 분배
void ProcessRecvData(char* pData);
BOOL Recv010x(char* pData); // 키오스크 정보 동기화
BOOL Recv020x(char* pData); // 원격 제어 (문열기/재부팅 등)
BOOL Recv0501(char* pData); // 결제 승인 처리

결제 모듈

CPaymentManager (결제 통합 관리)

// PaymentManager.h 주요 API
bool RequestPayment(const PaymentInfo &paymentInfo); // 비동기 결제 요청
void HandleUbiPaymentSuccess(std::unique_ptr<PaymentTerminalResponse> pResponse);
bool RequestCancel(); // 결제 취소

핵심 구현:

// UbiAnyCard 결제 처리 플로우
bool CPaymentManager::ProcessUbiPayment()
{
// 1. 콜백 설정
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);
// 2. 결제 요청 (비동기)
PaymentTerminalRequest req;
req.nAmount = m_pendingInfo.Amount;
req.nPayType = GetPayTypeFromMethod(m_pendingInfo.Method);
req.nUILang = GetCurrentUILanguage();
return UBIHANDLER->RequestPayment(req);
}

CUbiAnyCardHandler (카드/전자화폐 결제)

// CUbiAnyCardHandler.h 주요 API
bool RequestPayment(const PaymentTerminalRequest &req); // 비동기 승인 요청
bool RequestCreditCancel(int amount, const std::string &approvalNo, const std::string &approvalDate);
bool TestConnection(); // 핑 테스트

핵심 구현:

// 거래당 TCP 연결 생성 (Transaction-per-Connection)
bool CUbiAnyCardHandler::ExecuteTransaction(const std::string &command,
std::string &response, uint32_t timeoutMs)
{
SOCKET sock = INVALID_SOCKET;
// 1. 연결 생성
if (!ConnectToServer(sock))
return false;
// 2. 명령 전송
if (!SendCommand(sock, command))
{
CloseSocket(sock);
return false;
}
// 3. ACK 대기 (3초)
std::string ack;
if (!ReceiveResponse(sock, ack, 3000))
{
CloseSocket(sock);
return false;
}
// 4. 응답 대기 (60초)
if (!ReceiveResponse(sock, response, timeoutMs))
{
CloseSocket(sock);
return false;
}
CloseSocket(sock);
return true;
}
// K=V 형식 응답 파싱
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 (시리얼 통신 래퍼)

// SerialPort.h 주요 API
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 (보드 제어)

// CSUB16_BD_Control.h 주요 API
BOOL TryOpenLocker(int nBoxNumber); // 문 열기 시도
void ResetBoard(); // 보드 리셋
BOOL GetBoardStatus(); // 상태 조회

핵심 구현:

// 보드 초기화 및 설정
BOOL CSUB16_BD_Control::Initialize(const BoardConfig &config)
{
// 1. DLL 초기화
SDLL_SET_INITIAL(config.nBoxCount + 1, config.nMaxCHN);
SDLL_Set_Polling(TRUE);
// 2. 메인 제어 포트 열기
if (SDLL_OpenComPort(config.nPort, (char *)config.szBPS) == FALSE)
return FALSE;
// 3. 재연결용 포트 설정
m_reconnecter.Open(config.nReconnecterPort, dwError);
// 4. 함-채널/ID 매핑
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));
// 1. 솔레노이드 작동 명령
BOOL open_command_result = OpenLocker(boxNumber);
Sleep(retryIntervalMs);
// 2. 문 열림 센서 확인 (신호 반전 고려)
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 (지폐 투입기)

// BillAcceptor.h 주요 API
void EnableAcceptance(bool enable); // 지폐 받기 활성화
void StackBill(); // 지폐 입금 완료
void ReturnBill(); // 지폐 반환

핵심 구현 (상태머신):

// 지폐기 상태 업데이트 및 프레임 처리
void BillAcceptor::Update()
{
// 1. 링크 다운 감지 (전원/케이블 문제)
if (!m_offlineNotified && isLinkDown(kLinkDownDetectMs))
{
if (m_errorCallback)
m_errorCallback(2, _T("장치 미응답/전원 꺼짐으로 추정"));
m_offlineNotified = true;
}
// 2. 수신 버퍼 파싱 (상태 프레임 0x1B/0x1C 처리)
parseReceiveBuffer();
// 3. 진행 중 명령 타임아웃/완료 처리
if (m_inflight.active)
{
if (isCommandCompleted(m_inflight.msg.Code))
{
// Enable 완료 처리
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();
}
}
// 4. 주기적 상태 폴링 (0x59/0x5A)
sendPeriodicPolling();
}
// 상태 프레임 파싱 (0x1B: 상태 플래그, 0x1C: 에러 플래그)
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) // Escrow Position
{
// 금액 계산 (1000/5000/10000 권종별)
long escrowAmount = calcAmountFromBytes(
m_currentState.nBillCount1000,
m_currentState.nBillCount5000,
m_currentState.nBillCount10000);
if (m_escrowCallback)
m_escrowCallback(escrowAmount);
}
// 스택 완료 확인
if (flag & 0x02) // Stacked
{
if (m_stackedCallback)
m_stackedCallback(m_currentState.nTotalAcceptedAmount);
}
}

UI/세션 관리

SessionManager (세션 상태 관리)

// SessionManager.h 주요 API
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);
// 결과에 따라 UI 메시지 전송
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 라우팅)

// CUI_Manager.h 주요 API
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 (주기 작업 스케줄러)

// TaskManager.h 주요 API
BOOL Start(HWND hMainWnd); // 스케줄러 시작
void Stop(); // 스케줄러 종료
void CheckAndRunPayProgram(); // 결제 프로그램 감시

핵심 구현:

// 주기 작업 스레드 (1초 틱 기반)
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();
// 5초 주기: 결제 프로그램 감시
if (currentTick - pThis->m_lastProcessCheckTick > 5000)
{
pThis->CheckAndRunPayProgram();
pThis->m_lastProcessCheckTick = currentTick;
}
// 60초 주기: 일일 작업 및 재부팅 확인
if (currentTick - pThis->m_lastDailyTaskTick > 60000)
{
pThis->PerformDailyTasks();
pThis->PerformRebootTasks();
pThis->m_lastDailyTaskTick = currentTick;
}
}
return 0;
}
// UbiAnyCard 결제 프로그램 감시 및 자동 기동
void TaskManager::CheckAndRunPayProgram()
{
// 1. 프로세스 실행 여부 확인 (뮤텍스 기반)
if (!TM_IsUbiAnyCardMutexAlive())
{
writeLog("TaskManager - UbiAnyCard_Ver2 프로세스가 실행되지 않음. 시작 시도...");
// 2. 프로세스 시작
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()
{
// 1. 시스템 시간 확인 (시간 동기화 실패 시 재부팅)
SYSTEMTIME st;
GetLocalTime(&st);
// 2. 로그 파일 크기 확인 (과도하게 큰 경우 정리)
// 3. 메모리 사용량 확인 (임계치 초과 시 재부팅)
// 4. 특정 시간대 자동 재부팅 (새벽 3시)
if (st.wHour == 3 && st.wMinute < 5)
{
writeLog("TaskManager - Scheduled reboot at 3 AM");
::PostMessage(m_hMainWnd, WM_USER_WINDOW_REBOOT, 0, 0);
}
}

SessionManager (세션 상태 관리)

// SessionManager.h 주요 API
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 스레드 안전성을 위해 메시지 기반 비동기 패턴을 사용합니다.