Move git root from Client/ to src/ to track all source code: - Client: Game client source (moved to Client/Client/) - Server: Game server source - GameTools: Development tools - CryptoSource: Encryption utilities - database: Database scripts - Script: Game scripts - rylCoder_16.02.2008_src: Legacy coder tools - GMFont, Game: Additional resources 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
540 lines
17 KiB
C++
540 lines
17 KiB
C++
// MediaWebBilling.cpp : 응용 프로그램에 대한 진입점을 정의합니다.
|
|
//
|
|
|
|
#include "stdafx.h"
|
|
#include "MediaWebBilling.h"
|
|
#include <myOLEDB.h>
|
|
#include <Config.h>
|
|
#include <Log.h>
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <ctime>
|
|
#include <vector>
|
|
#include <oledb.h>
|
|
#include <srv.h>
|
|
|
|
using namespace std;
|
|
|
|
template<class _Elem, class _Traits>
|
|
inline basic_ostream<_Elem, _Traits>& __cdecl writetime(basic_ostream<_Elem, _Traits>& _Ostr)
|
|
{
|
|
SYSTEMTIME systime;
|
|
GetLocalTime(&systime);
|
|
|
|
_Elem fill = _Ostr.fill();
|
|
|
|
_Ostr << setfill('0')
|
|
<< "["
|
|
<< setw(4) << systime.wYear << "-"
|
|
<< setw(2) << systime.wMonth << "-"
|
|
<< setw(2) << systime.wDay << " "
|
|
<< setw(2) << systime.wHour << ":"
|
|
<< setw(2) << systime.wMinute << ":"
|
|
<< setw(2) << systime.wSecond << "] " << setfill(fill);
|
|
|
|
return (_Ostr);
|
|
}
|
|
|
|
inline std::string& addNumber(std::string& str, const char* szData)
|
|
{
|
|
const char* szWriteData = (0 == strlen(szData)) ? "NULL" : szData;
|
|
|
|
str += szWriteData;
|
|
str += ", ";
|
|
|
|
return str;
|
|
}
|
|
|
|
inline std::string& addString(std::string& str, const char* szData)
|
|
{
|
|
if(0 == strlen(szData))
|
|
{
|
|
str += "NULL, ";
|
|
}
|
|
else
|
|
{
|
|
str += "'";
|
|
str += szData;
|
|
str += "', ";
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
|
|
#pragma pack(1)
|
|
struct MWBillingData
|
|
{
|
|
DBCHAR m_CRMCode[16]; // PC방 CRMCode
|
|
DBCHAR m_Command[16]; // 처리 command
|
|
|
|
DBCHAR m_CRMIP1[13]; // 서비스ip대역1
|
|
DBCHAR m_CRMIP2[13]; // 서비스ip대역2
|
|
DBCHAR m_CRMIP3[13]; // 서비스ip대역3
|
|
|
|
DBCHAR m_Index[11]; // 자동증가(SEQID)
|
|
DBCHAR m_ServiceTime[11]; // 정량제시간
|
|
DBCHAR m_EndTime[11]; // 선승일, 보상 요청시간
|
|
DBCHAR m_ServiceIPNum[11]; // 정액제구매ip개수
|
|
|
|
DBCHAR m_SysDay[20]; // 결재일 YYYY-MM-DD HH24:MI:SS
|
|
DBCHAR m_ServiceDay[11]; // 정액제만료일 YYYY-MM-DD
|
|
DBCHAR m_EndDay[11]; // 선승일, 보상 만료일 YYYY-MM-DD
|
|
|
|
DBCHAR m_CRMStartIP1[4]; // 서비스ip대역시작1
|
|
DBCHAR m_CRMStopIP1[4]; // 서비스ip대역끝1
|
|
DBCHAR m_CRMStartIP2[4]; // 서비스ip대역시작2
|
|
DBCHAR m_CRMStopIP2[4]; // 서비스ip대역끝2
|
|
DBCHAR m_CRMStartIP3[4]; // 서비스ip대역시작3
|
|
DBCHAR m_CRMStopIP3[4]; // 서비스ip대역끝3
|
|
|
|
DBCHAR m_TimeProcess[2]; // 잔여일처리 방법 (m = -처리, z = 끊음)
|
|
DBCHAR m_PriceType[2]; // 정액제,정량제 여부 (D 정액제, T 정량제)
|
|
};
|
|
|
|
struct RemainTimeLog
|
|
{
|
|
DBCHAR m_CRMCode[20];
|
|
DBCHAR m_IntServiceTime[11];
|
|
DBCHAR m_DelColumn[2];
|
|
};
|
|
|
|
#pragma pack()
|
|
|
|
|
|
int APIENTRY _tWinMain(HINSTANCE hInstance,
|
|
HINSTANCE hPrevInstance,
|
|
LPTSTR lpCmdLine,
|
|
int nCmdShow)
|
|
{
|
|
CLog log;
|
|
CConfigurator config;
|
|
|
|
OleDB mwDB;
|
|
OleDB billingDB;
|
|
|
|
char szQuery[OleDB::MaxQueryTextLen];
|
|
int nQueryLen = 0;
|
|
|
|
if(!log.RedirectStdOut("MediaWebBilling"))
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
cout << endl << writetime << "쿼리를 실행합니다." << endl;
|
|
|
|
TCHAR* tszConfigFileName = TEXT("MWBillingInfo.cfg");
|
|
|
|
if(!config.Load(tszConfigFileName))
|
|
{
|
|
cout << writetime << tszConfigFileName << "DB설정 파일을 읽을 수 없습니다." << endl;
|
|
return -1;
|
|
}
|
|
|
|
const char* szMWServerName = config.Get("MWServerName");
|
|
const char* szMWDBName = config.Get("MWDBName");
|
|
const char* szMWUserName = config.Get("MWUserName");
|
|
const char* szMWPassword = config.Get("MWPassword");
|
|
|
|
if(!mwDB.ConnectSQLServer(szMWServerName, szMWDBName,
|
|
szMWUserName, szMWPassword, OleDB::ConnType_ORACLE))
|
|
{
|
|
cout << writetime << "미디어웹DB : 접속할 수 없습니다. : " << mwDB.GetErrorString()
|
|
<< " ServerName : " << szMWServerName
|
|
<< " DBName : " << szMWDBName
|
|
<< " UserName : " << szMWUserName
|
|
<< " Password : " << szMWPassword
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
const char* szBillingServerName = config.Get("BillingServerName");
|
|
const char* szBillingDBName = config.Get("BillingDBName");
|
|
const char* szBillingUserName = config.Get("BillingUserName");
|
|
const char* szBillingPassword = config.Get("BillingPassword");
|
|
|
|
if(!billingDB.ConnectSQLServer(szBillingServerName, szBillingDBName,
|
|
szBillingUserName, szBillingPassword, OleDB::ConnType_MSSQL))
|
|
{
|
|
cout << writetime << "빌링DB : 접속할 수 없습니다. : " << billingDB.GetErrorString()
|
|
<< " ServerName : " << szBillingServerName
|
|
<< " DBName : " << szBillingDBName
|
|
<< " UserName : " << szBillingUserName
|
|
<< " Password : " << szBillingPassword
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
// 1. 테이블 클리어
|
|
if(!billingDB.ExcuteQuery("EXEC agt_CRM_RYLLOG_Delete"))
|
|
{
|
|
cout << writetime << "빌링DB : 임시 테이블 삭제에 실패했습니다 : "
|
|
<< billingDB.GetErrorString() << endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
// 2. 미디어웹(Oracle) 에서 데이터 가져오기
|
|
int nMinNum = 0;
|
|
|
|
if(!billingDB.ExcuteQueryGetData(
|
|
"SELECT intCount FROM TblImportedNum WHERE strCompType = 'M'", &nMinNum))
|
|
{
|
|
cout << writetime << "빌링DB : 마지막으로 처리된 데이터 건수 번호를 얻어올 수 없습니다 : "
|
|
<< billingDB.GetErrorString() << endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
nQueryLen = _snprintf(szQuery, OleDB::MaxQueryTextLen,
|
|
"SELECT "
|
|
" CAST(crmcode AS VARCHAR(16)), "
|
|
" CAST(command AS VARCHAR(16)), "
|
|
|
|
" CAST(crmip1 AS VARCHAR(13)), "
|
|
" CAST(crmip2 AS VARCHAR(13)), "
|
|
" CAST(crmip3 AS VARCHAR(13)), "
|
|
|
|
" CAST(seqid AS VARCHAR(11)), "
|
|
" CAST(servicetime AS VARCHAR(11)), "
|
|
" CAST(endtime AS VARCHAR(11)), "
|
|
" CAST(serviceipnum AS VARCHAR(11)), "
|
|
|
|
" TO_CHAR(regdate, 'YYYY-MM-DD HH24:MI:SS '), "
|
|
" TO_CHAR(serviceday, 'YYYY-MM-DD '), "
|
|
" TO_CHAR(endday, 'YYYY-MM-DD '), "
|
|
|
|
" CAST(startcrmip1 AS VARCHAR(4)), "
|
|
" CAST(endcrmip1 AS VARCHAR(4)), "
|
|
" CAST(startcrmip2 AS VARCHAR(4)), "
|
|
" CAST(endcrmip2 AS VARCHAR(4)), "
|
|
" CAST(startcrmip3 AS VARCHAR(4)), "
|
|
" CAST(endcrmip3 AS VARCHAR(4)), "
|
|
|
|
" CAST(timeprocess AS VARCHAR(2)), "
|
|
" CAST(pricetype AS VARCHAR(2)) "
|
|
|
|
" FROM CRM.CRM_RYLLOG WHERE SEQID > %d AND RYLCHK = 'N' ORDER BY SEQID ASC", nMinNum);
|
|
|
|
if(nQueryLen < 0)
|
|
{
|
|
cout << writetime << "미디어웹DB : 빌링 데이터를 얻어오는 쿼리를 만들 수 없습니다." << endl;
|
|
return -1;
|
|
}
|
|
|
|
if(!mwDB.ExcuteQuery(szQuery))
|
|
{
|
|
cout << writetime << "미디어웹DB : 빌링 정보를 얻어올 수 없습니다 : "
|
|
<< mwDB.GetErrorString()
|
|
<< " Query : " << szQuery << endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
typedef std::vector<MWBillingData> BillingDataArray;
|
|
|
|
BillingDataArray mwBillingArray;
|
|
mwBillingArray.reserve(5000);
|
|
|
|
const int MAX_DATA = 1000;
|
|
MWBillingData mwBillingData[MAX_DATA];
|
|
memset(mwBillingData, 0, sizeof(MWBillingData) * MAX_DATA);
|
|
|
|
int nReturnRow = 0;
|
|
while(mwDB.GetData((void**)&mwBillingData,
|
|
sizeof(MWBillingData), MAX_DATA, &nReturnRow))
|
|
{
|
|
if(0 == nReturnRow)
|
|
{
|
|
break;
|
|
}
|
|
|
|
mwBillingArray.insert(mwBillingArray.end(),
|
|
mwBillingData, mwBillingData + nReturnRow);
|
|
|
|
memset(mwBillingData, 0, sizeof(MWBillingData) * MAX_DATA);
|
|
}
|
|
|
|
// BillNum, MemberID, RylUID, BillingType, EndTime, GameMin
|
|
const char* szInsertQuery = "INSERT INTO TblCRM_RYLLOG "
|
|
" (intIndex, strCRMCode, strPriceType, dateSysDay, strCommand, "
|
|
" strCRMIP1,strStartCRMIP1,strEndCRMIP1, "
|
|
" strCRMIP2,strStartCRMIP2,strEndCRMIP2, "
|
|
" strCRMIP3,strStartCRMIP3,strEndCRMIP3, "
|
|
" strTimeprocess, intServiceTime, dateServiceDay, "
|
|
" TinyServiceIPNum, dateEndday, intEndTime, strRylChk) values ( ";
|
|
|
|
BillingDataArray::iterator pos = mwBillingArray.begin();
|
|
BillingDataArray::iterator end = mwBillingArray.end();
|
|
|
|
std::string strQuery;
|
|
strQuery.reserve(OleDB::MaxQueryTextLen);
|
|
|
|
for(; pos != end; ++pos)
|
|
{
|
|
MWBillingData& data = *pos;
|
|
|
|
strQuery.assign(szInsertQuery);
|
|
|
|
addNumber(strQuery, data.m_Index);
|
|
addString(strQuery, data.m_CRMCode);
|
|
addString(strQuery, data.m_PriceType);
|
|
addString(strQuery, data.m_SysDay);
|
|
addString(strQuery, data.m_Command);
|
|
|
|
addString(strQuery, data.m_CRMIP1);
|
|
addNumber(strQuery, data.m_CRMStartIP1);
|
|
addNumber(strQuery, data.m_CRMStopIP1);
|
|
|
|
addString(strQuery, data.m_CRMIP2);
|
|
addNumber(strQuery, data.m_CRMStartIP2);
|
|
addNumber(strQuery, data.m_CRMStopIP2);
|
|
|
|
addString(strQuery, data.m_CRMIP3);
|
|
addNumber(strQuery, data.m_CRMStartIP3);
|
|
addNumber(strQuery, data.m_CRMStopIP3);
|
|
|
|
addString(strQuery, data.m_TimeProcess);
|
|
addNumber(strQuery, data.m_ServiceTime);
|
|
addString(strQuery, data.m_ServiceDay);
|
|
addNumber(strQuery, data.m_ServiceIPNum);
|
|
addString(strQuery, data.m_EndDay);
|
|
addNumber(strQuery, data.m_EndTime);
|
|
|
|
strQuery += " 'N')";
|
|
|
|
if(!billingDB.ExcuteQuery(strQuery.c_str(), OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "빌링DB : 미디어웹에서 가져온 데이터를 기록할 수 없습니다. : " << billingDB.GetErrorString()
|
|
<< " 현재 SEQIndex : " << data.m_Index
|
|
<< " Query : " << strQuery << endl;
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int nMaxNum = 0;
|
|
|
|
if(!billingDB.ExcuteQueryGetData(
|
|
"SELECT MAX(intIndex) FROM TblCRM_RYLLOG WHERE strRYLCHK = 'N' AND strConvertCHK ='N'", &nMaxNum))
|
|
{
|
|
cout << writetime << "빌링DB : 미디어웹에서 가져온 데이터 건수 최대 번호를 얻어올 수 없습니다 : "
|
|
<< billingDB.GetErrorString() << endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
if(0 < nMaxNum)
|
|
{
|
|
nQueryLen = _snprintf(szQuery, OleDB::MaxQueryTextLen,
|
|
"UPDATE CRM.CRM_RYLLOG SET RYLCHK = 'Y'"
|
|
"WHERE SEQID > %d AND SEQID <= %d AND RYLCHK = 'N'", nMinNum, nMaxNum);
|
|
|
|
if(nQueryLen < 0)
|
|
{
|
|
cout << writetime << "미디어웹DB : 과금 처리 업데이트 실패 - 쿼리 생성 실패" << endl;
|
|
return -1;
|
|
}
|
|
|
|
if(!mwDB.ExcuteQuery(szQuery, OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "미디어웹DB : 과금 처리 업데이트 실패 - 쿼리 실패 : "
|
|
<< mwDB.GetErrorString() << " Query : " << szQuery << endl;
|
|
return -1;
|
|
}
|
|
|
|
nQueryLen = _snprintf(szQuery, OleDB::MaxQueryTextLen,
|
|
"UPDATE TblImportedNum SET intCount = %d WHERE strCompType = 'M'", nMaxNum);
|
|
|
|
if(nQueryLen < 0)
|
|
{
|
|
cout << writetime << "빌링DB : 과금 처리 업데이트 실패 - 쿼리 생성 실패 "
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< " Query : " << szQuery << endl;
|
|
return -1;
|
|
}
|
|
|
|
if(!billingDB.ExcuteQuery(szQuery, OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "빌링DB : 과금 처리 업데이트 실패 - 쿼리 실패 : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< " Query : " << szQuery << endl;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
// 3. 데이터 처리
|
|
if(!billingDB.ExcuteQuery("EXEC agt_CRM_RYLLOG_EXECUTE"))
|
|
{
|
|
cout << writetime << "빌링DB : agt_CRM_RYLLOG_EXECUTE을 실패했습니다. : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< endl;
|
|
|
|
// 빠져나가지 않는다. 데이터 오류가 있으면 에러가 날 수 있다.
|
|
// return -1;
|
|
}
|
|
|
|
// 4. 남은 서비스 타임 계산
|
|
if(!billingDB.ExcuteQuery("EXEC agt_CRM_RemainServiceTime"))
|
|
{
|
|
cout << writetime << "빌링DB : agt_CRM_RemainServiceTime을 실패했습니다. : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
// 5. 잔여시간 넘겨주기 agt_CRM_RemainServiceTime_Transfer
|
|
if(!billingDB.ExcuteQuery(
|
|
"SELECT strCRMCode, "
|
|
" CAST(MIN(intServiceTime) AS VARCHAR(11)), "
|
|
" CAST(delColumn AS VARCHAR(2)) "
|
|
" FROM TblCRM_SERVICETIME WHERE WebCHK = 'N' "
|
|
" GROUP BY strCRMCode,delColumn"))
|
|
{
|
|
cout << writetime << "빌링DB : 잔여시간을 얻어오는 데 실패했습니다. : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
typedef std::vector<RemainTimeLog> RemainTimeLogArray;
|
|
|
|
RemainTimeLogArray remainTimeLogArray;
|
|
remainTimeLogArray.reserve(10000);
|
|
|
|
const int MAX_REMAIN_TIME_DATA = 1000;
|
|
RemainTimeLog remainTimes[MAX_REMAIN_TIME_DATA];
|
|
|
|
memset(remainTimes, 0, sizeof(RemainTimeLog) * MAX_REMAIN_TIME_DATA);
|
|
|
|
int nGetRemainTimesNum = 0;
|
|
|
|
while(billingDB.GetData((void**)&remainTimes, sizeof(RemainTimeLog),
|
|
MAX_REMAIN_TIME_DATA, &nGetRemainTimesNum))
|
|
{
|
|
if(0 == nGetRemainTimesNum)
|
|
{
|
|
break;
|
|
}
|
|
|
|
remainTimeLogArray.insert(remainTimeLogArray.end(),
|
|
remainTimes, remainTimes + nGetRemainTimesNum);
|
|
|
|
memset(remainTimes, 0, sizeof(RemainTimeLog) * MAX_REMAIN_TIME_DATA);
|
|
}
|
|
|
|
if(!billingDB.ExcuteQuery("UPDATE TblCRM_SERVICETIME SET WebCHK = 'Y' WHERE WebCHK ='N'",
|
|
OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "빌링DB : WebCHK를 Y로 설정하는 데 실패했습니다. : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
RemainTimeLogArray::iterator rtpos = remainTimeLogArray.begin();
|
|
RemainTimeLogArray::iterator rtend = remainTimeLogArray.end();
|
|
|
|
for(; rtpos != rtend; ++rtpos)
|
|
{
|
|
RemainTimeLog& data = *rtpos;
|
|
|
|
char szCRMCode[20];
|
|
memset(szCRMCode, 0, sizeof(char) * 20);
|
|
|
|
strQuery = "SELECT CRMCODE FROM GAME.CRM_SERVICETIME WHERE CRMCODE = '";
|
|
strQuery += data.m_CRMCode;
|
|
strQuery += "' AND GAMECODE ='002' ";
|
|
|
|
if(!mwDB.ExcuteQuery(strQuery.c_str()))
|
|
{
|
|
cout << writetime << "미디어웹DB : CRMCODE검색에 실패했습니다. : "
|
|
<< mwDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< " Query : " << strQuery << endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
mwDB.GetData(szCRMCode);
|
|
|
|
if(0 != strlen(szCRMCode))
|
|
{
|
|
strQuery = "UPDATE GAME.CRM_SERVICETIME SET SERVICETIME = ";
|
|
strQuery += data.m_IntServiceTime;
|
|
strQuery += ",DELCHK = '";
|
|
strQuery += data.m_DelColumn;
|
|
strQuery += "' WHERE CRMCODE = '";
|
|
strQuery += data.m_CRMCode;
|
|
strQuery += "' AND GAMECODE ='002'";
|
|
|
|
if(!mwDB.ExcuteQuery(strQuery.c_str(), OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "미디어웹DB : ServiceTime Update에 실패했습니다. : "
|
|
<< mwDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< " Query : " << strQuery << endl;
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
strQuery = "INSERT INTO GAME.CRM_SERVICETIME (CRMCODE,GAMECODE,SERVICETIME,DELCHK) VALUES ('";
|
|
strQuery += data.m_CRMCode;
|
|
strQuery += "','002',";
|
|
strQuery += data.m_IntServiceTime;
|
|
strQuery += ", '";
|
|
strQuery += data.m_DelColumn;
|
|
strQuery += "') ";
|
|
|
|
if(!mwDB.ExcuteQuery(strQuery.c_str(), OleDB::Rowset_Update))
|
|
{
|
|
cout << writetime << "미디어웹DB : ServiceTime Insert에 실패했습니다. : "
|
|
<< mwDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< " Query : " << strQuery << endl;
|
|
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. 잔여시간 로그남기기
|
|
if(!billingDB.ExcuteQuery("EXEC agt_CRM_RemainServiceTime_LOG"))
|
|
{
|
|
cout << writetime << "빌링DB : agt_CRM_RemainServiceTime_LOG를 실패했습니다. : "
|
|
<< billingDB.GetErrorString()
|
|
<< " MinBillingNum : " << nMinNum
|
|
<< " MaxBillingNum : " << nMaxNum
|
|
<< endl;
|
|
|
|
return -1;
|
|
}
|
|
|
|
cout << writetime << "쿼리 실행 완료." << endl;
|
|
return 0;
|
|
}
|
|
|
|
|