<?php
/*
 * Mysql 디비 제어
 * 

ex)
require_once (__DIR__."/cpbLibrary/database/cpbDB.php");
$db 			= new cpbLibrary\cpbDB("localhost", "사용자 아이디", "암호", "디비");
*/
namespace platyFramework;

/**
 * mysqli_real_escape_string 을 실행
 *
 * @param $s
 * @return string
 */
function mre($s = "")
{
    if ($s == "")
        return "";

    global $platyFramework;
    if ($platyFramework == null || $platyFramework->db == null || $platyFramework->db->_connection == null) {
        // die ("db is not initialized");
        return;
    }

    return mysqli_real_escape_string($platyFramework->db->_connection, $s);
}

/**
 * mysqli_real_escape_string 을 실행 및 '' 를 앞뒤로 붙임
 *
 * @param $s
 * @return string
 */
function mrea($s)
{
    return "'" . mre($s) . "'";
}

function mreb($s)
{
    return "`" . mre($s) . "`";
}

/**
 * hello 또는 push_users.latsSendStatus 와 같은 문구를 `hello`, `push_users`.`latsSendStatus`로 변환하고 리턴합니다.
 * @param $s
 * @return mixed|string
 */
function mreap($s)
{
    if (strstr($s, ".")) {
        $n = explode(".", $s);
        return "`" . mre($n[0]) . "`.`" . mre($n[1]) . "`";
    }
    return $s;
}


class ptyMysql
{
    var $debug;
    var $_SQL_WHERE_OR_ARRAY;
    var $_SQL_WHERE_ARRAY;
    var $_SQL_WHERE;
    var $_SQL_SELECT;
    var $_SQL_COUNT_SELECT;
    /**
     * @var array
     */
    var $_SQL_ORDERBY;
    var $_SQL_SET_ARRAY;
    var $_SQL_LIMIT_ROWCOUNT;
    var $_SQL_JOIN;
    var $_connection;
    var $bDestroymSelect;
    var $activeCacheFileName = "";
    var $cacheEnabled = false;
    var $cacheEnabledInterval;
    private $primaryColumnName = "id";
    private $pageIndex = 1;

    /** @var bool 이 값이 활성화 되면 getItems() 에서 getTotalCount 시에 JOIN 도 같이 포함되어야 합니다 */
    public bool $needsJoinWhenTotalCount = false;

    /** @var bool 이 값이 활성화 되면 slow query 를 추적하여 telegram 에 전송합니다.
     * @see ptyMysql::setSlowQueryDetect()
     */
    private bool $willDetectSlowQuery = true;

    function __construct($dbhost, $dbuser, $dbpass, $dbname)
    {
        $this->debug = 0;
        $this->_SQL_COUNT_SELECT = "";
        $this->_SQL_WHERE_ARRAY = array();
        $this->_SQL_WHERE_OR_ARRAY = array();
        $this->_SQL_JOIN_ARRAY = array();
        $this->_SQL_SET_ARRAY = array();
        $this->_SQL_ORDERBY = array();
        $this->_SQL_SELECT_ARRAY = array();
        $this->_SQL_GROUP_BY_ARRAY = array();
        $this->_SQL_FROM_ARRAY = array();
        $this->_SQL_UNION_ARRAY = array();

        $this->pageName = "page";

        $this->bDestroymSelect = false;

        $this->initHaving();
        $this->initWhere();
        $this->InitOrderBy();
        $this->InitLimit(0);
        $this->InitGroupBy();

        if ($dbhost && $dbuser && $dbpass && $dbname)
            $this->connect($dbhost, $dbuser, $dbpass, $dbname);
        // else
        //	echo "Mysql 디비 연결 설정이 되어 있지 않습니다.<br>";
    }

    function initHaving()
    {
        $this->_SQL_HAVING = "";
        $this->_SQL_HAVING_ARRAY = array();
    }

    function initWhere()
    {
        $this->_SQL_WHERE = "";
        $this->_SQL_WHERE_ARRAY = array();
    }

    function InitOrderBy()
    {
        $this->_SQL_ORDERBY = array();
    }

    function initLimit($LimitCount = 20)
    {
        $this->_SQL_LIMIT_ROWCOUNT = $LimitCount;
        return $this;
    }

    function initGroupBy()
    {
        $this->_SQL_GROUP_BY_ARRAY = array();
        // $this->_SQL_GROUPBY = "";
    }

    private $_dbHost = "";
    private $_dbUserName = "";
    private $_dbPassword = "";
    private $_dbName = "";

    function connect($DBHost, $DBUserName, $DBPassword, $DBName, $showError = true)
    {
        if (!$DBHost && !$DBUserName && !$DBPassword && !$DBName) {
            ptyError("디비 정보가 올바르지 않습니다");
            return false;
        }

        $this->_dbHost = $DBHost;
        $this->_dbUserName = $DBUserName;
        $this->_dbPassword = $DBPassword;
        $this->_dbName = $DBName;

        ptyInitElapsedTime();
        $this->_connection = @mysqli_connect($DBHost, $DBUserName, $DBPassword);
        if (!$this->_connection) {
            session_write_close();

            if ($showError) {
                header("HTTP/1.0 500 Internal Server Error");

                $log = date("Ymd_His") . " " . getmygid() . " db connection fail. \n";
                file_put_contents(PLATYFRAMEWORK_SYSTEM_DIR . "/logs/db.log", $log, FILE_APPEND | LOCK_EX);

                $url = ptyGetCurrentUrl();
                $msg = urlencode("🔴 [platyFramework] mysql connection failed.\n\n[ url ]\n$url\n\n[ query ]\n$Query\n\n[ error ]\n" . mysqli_error($this->_connection));
                file_get_contents("https://api.telegram.org/bot1142526400:AAGjUWWxUk9dSKhCdGBgaxQqjfHF7HC9pIE/sendmessage?chat_id=589455612&text=$msg");

                ptyError("현재 일시적으로 접속이 불가능한 상태 입니다. 잠시 후 다시 시도해 주십시요. ");
            }

            return false;
        }
        mysqli_select_db($this->_connection, $DBName);
        mysqli_query($this->_connection, "set names " . PLATYFRAMEWORK_DB_CHARSET);

        ptyRecordElapsedTime("mysql connect");

        // ptyDebug(get_defined_vars());
    }

    function close()
    {
        if ($this->_connection != null)
            mysqli_close($this->_connection);
    }

    static function getWhere이번달($Key)
    {
        return "YEAR($Key) = YEAR(NOW()) AND MONTH($Key) = MONTH(NOW())";
    }

    static function getWhere지난달($Key)
    {
        return "YEAR($Key) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND 
				MONTH($Key) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))";
    }

    static function getWhere일년전이번달1일부터오늘까지($Key)
    {
        return "YEAR($Key) = YEAR(NOW()) - 1 AND MONTH($Key) = MONTH(NOW()) AND DAY($Key) > 1 AND DAY($Key) <= DAY(NOW())";
    }

    static function getWhere지난달1일부터오늘까지($Key)
    {
        return "YEAR($Key) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND 
				MONTH($Key) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))
				AND DAY($Key) > 1 AND DAY($Key) <= DAY(NOW())";
    }

    static function getWhereN개월전부터($n, $Key)
    {
        return "$Key > DATE_SUB(NOW(), INTERVAL $n MONTH)";
    }

    static function getWhere12개월전부터($Key)
    {
        return "$Key > DATE_SUB(NOW(), INTERVAL 12 MONTH)";
    }

    static function getWhere1개월전부터($Key)
    {
        return "$Key > DATE_SUB(NOW(), INTERVAL 1 MONTH)";
    }

    static function getCommaStringFromArray($s)
    {
        $n = array();
        foreach ($s as $k => $v)
            array_push($n, "$k = '$v'");

        return implode(", ", $n);
    }

    function __destruct()
    {
        if ($this->bDestroymSelect) {
            ?>
            <SCRIPT language='javascript'>
                <!--
                mSelect('SearchType', '<?=$_REQUEST[SearchType]?>');
                mSelect('SearchType2', '<?=$_REQUEST[SearchType2]?>');
                -->
            </SCRIPT>
            <?
        }
    }

    function getInsertId()
    {
        // if ($this->_connection == null) return null;
        return mysqli_insert_id($this->_connection);
    }

    function setPageName($name)
    {
        $this->pageName = $name;
        return $this;
    }

    function mtime()
    {
        $time = explode(" ", microtime());
        $usec = (double)$time[0];
        $sec = (double)$time[1];

        return $sec + $usec;
    }

    function mtime_diff($smsg, $ts, $te, $print_type = 4, $print = 1)
    {
        switch ($print_type) {
            case 0:
            case 1:

                // $msg = sprintf("<br><font size=2>경과시간 : "."End(%s) - Start(%s) = <b>%s</b>sec <br></font>"
                // , $te , $ts, $te - $ts);
                $msg = sprintf("<b>%s</b>sec <br></font>", $te - $ts);

                break;
            case 2:
                $msg = sprintf("<br><font size=2><b>%s</b>sec<br></font>", $te - $ts);
                break;
            case 3:
                $msg = sprintf("%s", $te - $ts);
                break;
            case 4:
            default:
                $msg = sprintf("%2.3f", $te - $ts); // 00.000 형태.
                break;
        }

        if ($print)
            echo $smsg . "=" . $msg;
        return $smsg . "=" . $msg;
    }

    function GetFirstDBRowCol($Query)
    {
        $Query = $Query;
        $Result = MysqlQuery($Query);
        $Row = mysqli_fetch_array($Result);
        return $Row[0];
    }

    function QueryAndMove($Query, $SuccessMessage, $SuccessURL, $FailMessage, $FailURL)
    {
        $Result = MysqlQuery($Query);

        if (!$Result) {
            if ($FailURL)
                AlertDirectGo($FailMessage, $FailURL);
            else
                AlertBackGo($FailMessage);
            return;
        }

        if ($SuccessURL)
            AlertDirectGo($SuccessMessage, $SuccessURL);
        else
            AlertBackGo($SuccessMessage);
    }

    function setLimit($limitCount = 20)
    {
        $this->initLimit($limitCount);
        return $this;
    }

    function setPageLimit($limitCount = 20)
    {
        if (is_null($limitCount)) $limitCount = 20;
        $this->initLimit($limitCount);
        return $this;
    }

    function addItemSet($k, $v = null)
    {
        if (is_array($k)) {
            foreach ($k as $key => $value) {
                $this->addItemSet($key, $value);
            }
            return $this;
        }

        /*
        foreach ($this->_SQL_SET_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $from)
                return $this;
        }
        */

        if (!get_magic_quotes_gpc()) {
            $v = addslashes($v);
        }

        /*
        $commands = array("NOW()");
        if (in_array($v, $commands))
            array_push($this->_SQL_SET_ARRAY, "`$k` = $v");
        */

        if ($k == "*password") {
            $k = substr($k, 1);
            array_push($this->_SQL_SET_ARRAY, "`$k` = CONCAT('*', UPPER(SHA1 (UNHEX(SHA1 ('$v')))))");
        } else if ($k[0] == "*") {
            $k = substr($k, 1);
            array_push($this->_SQL_SET_ARRAY, "`$k` = $v");
        } else {
            array_push($this->_SQL_SET_ARRAY, "`$k` = '$v'");
        }

        /*
        else if (!is_null($v))
            array_push($this->_SQL_SET_ARRAY, "`$k` = '" . $data . "'");
        else {
            if (strstr($k, "="))
                array_push($this->_SQL_SET_ARRAY, "$k");
            else
                array_push($this->_SQL_SET_ARRAY, "$k = NULL");
        }
        */

        return $this;


        // $this->add
        if (is_array($k)) {
            foreach ($k as $key => $value) {
                if (!is_null($value))
                    $this->addSet($key, $value);
            }
            return $this;
        }

        /*
        foreach ($this->_SQL_SET_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $from)
                return $this;
        }
        */

        // $data = htmlspecialchars($v, ENT_COMPAT, 'UTF-8');
        if (!get_magic_quotes_gpc()) {
            $data = addslashes($v);
        }

        $commands = array("NOW()");
        if (in_array($v, $commands))
            array_push($this->_SQL_SET_ARRAY, "`$k` = $v");
        else if (!is_null($v))
            array_push($this->_SQL_SET_ARRAY, "`$k` = '" . $data . "'");
        else
            array_push($this->_SQL_SET_ARRAY, "$k");


        return $this;
    }

    function addSet($k, $v = null)
    {
        if (is_array($k)) {
            foreach ($k as $key => $value) {
                $this->addSet($key, $value);
            }
            return $this;
        }

        /*
        if (!get_magic_quotes_gpc()) {
            if (isset($v))
                $v = addslashes($v);
        }
        */
        if (is_bool($v)) {
            $v = $v == true ? 1 : 0;
        }

        if (isset($v))
            $v = mre($v);

        // $gol = stripos($v, "@");

        // [2024-05-06 cpueblo] /*RAW*/ 와 * 로 시작되는 값은 이제 사용하지 않아야 함.
        /*
        if (stripos($v, "/ *RAW * /") !== false) {
            $vv = substr($v, 7);
            array_push($this->_SQL_SET_ARRAY, "`$k` = $vv");
        }
        */
        /* else if ($gol !== false && $gol == 0) {
            $vv = substr($v, 1);
            array_push($this->_SQL_SET_ARRAY, "`$k` = $vv");
        }
        */
        if ($k == "*password") {
            $k = substr($k, 1);
            array_push($this->_SQL_SET_ARRAY, "`$k` = CONCAT('*', UPPER(SHA1 (UNHEX(SHA1 ('$v')))))");
        } else if ($k[0] == "_") {
            return $this;
        } else if ($k[0] == "*" || $k[0] == "@") {
            $k = substr($k, 1);
            if (isset($v))
                array_push($this->_SQL_SET_ARRAY, "`$k` = $v");
            else
                array_push($this->_SQL_SET_ARRAY, "$k");
        } else {
            if (is_null($v))
                array_push($this->_SQL_SET_ARRAY, "`$k` = NULL");
            else
                array_push($this->_SQL_SET_ARRAY, "`$k` = '$v'");
        }

        return $this;
    }

    function addSetRaw($k, $v = null)
    {
        if (is_array($k)) {
            foreach ($k as $key => $value) {
                $this->addSetRaw($key, $value == "" ? "" : $value);
            }
            return $this;
        }

        foreach ($this->_SQL_SET_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $from)
                return $this;
        }

        if ($k && (isset($v)))
            array_push($this->_SQL_SET_ARRAY, "`$k` = $v");
        else
            array_push($this->_SQL_SET_ARRAY, "$k");
        // array_push($this->_SQL_SET_ARRAY, "`$k` = ''");

        return $this;
    }

    function addFrom($from)
    {
        foreach ($this->_SQL_FROM_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $from)
                return $this;
        }
        array_push($this->_SQL_FROM_ARRAY, $from);
        return $this;
    }

    function initFrom()
    {
        $this->_SQL_FROM_ARRAY = array();
        return $this;
    }

    function getPrimaryColumnName()
    {
        return $this->primaryColumnName;
    }

    function setPrimaryColumnName($id)
    {
        $this->primaryColumnName = $id;
        return $this;
    }

    function getTablePrimaryColumnName()
    {
        return $this->getTableName() . "." . $this->getPrimaryColumnName();
    }

    function getTableName()
    {
        return $this->_SQL_FROM_ARRAY[0];
    }

    function addSelect($select = "")
    {
        if ($select == "") {
            $this->addSelect($this->getPrimaryFrom() . ".*");
            return;
        }
        foreach ($this->_SQL_SELECT_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $select)
                return;
        }
        array_push($this->_SQL_SELECT_ARRAY, $select);
        return $this;
    }


    function addJoin($join)
    {
        foreach ($this->_SQL_JOIN_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $join)
                return;
        }
        array_push($this->_SQL_JOIN_ARRAY, $join);
        return $this;
    }

    function addWhereOr($Where)
    {
        array_push($this->_SQL_WHERE_OR_ARRAY, $Where);
        return $this;
    }

    function setSelect($s, $isArray = false)
    {
        if ($isArray)
            $this->_SQL_SELECT_ARRAY = $s;
        else
            $this->_SQL_SELECT = $s;
    }

    function setCountSelect($s)
    {
        $this->_SQL_COUNT_SELECT = $s;
    }

    function delete()
    {
        $this->MysqlQuery(sprintf("DELETE FROM %s %s", $this->getFrom(FALSE), $this->getWhere()));
    }

    function MysqlQuery($Query, $dispQuery = FALSE)
    {
        if (!$this->_connection) {
            pd("[PlatyFramework-mysql] mysql disconnected.");
            error_log("[PlatyFramework-mysql] mysql disconnected.");
            return false;
        }

        $requireCacheWrite = false;
        if ($Query == "")
            $Query = $this->getQuery();

        global $_lastSqlQuery;
        global $_lastSqlQuery2;
        global $_queryHistoryItems;
        if (!strstr($Query, "t_pty_logs") && !strstr($Query, "t_pty_inbounds") && !strstr($Query, "t_pty_allow_ips") && !strstr($Query, "t_timeline_items")) {
            $_lastSqlQuery2 = $_lastSqlQuery;
            $_lastSqlQuery = $Query;
            $_queryHistoryItems[] = $Query;
        }

        global $ShowQueryLog;

        if ($this->cacheEnabled) {
            $fileName = $this->getCacheFileName($Query);
            $dir = $this->getCacheDir();
            $cacheFullFileName = $dir . "/" . $fileName;
            $stat = stat($cacheFullFileName);
            $this->activeCacheFileName = $cacheFullFileName;
            pdv($cacheFullFileName, "sql cache fileName");
            if ($stat == null or time() - $stat['mtime'] > $this->cacheEnabledInterval) {
                $requireCacheWrite = true;
                // ptyDebug("need!");
            } else {
                // ptyDebug("cacheLoading. fileName = ".$this->cacheEnabledFileName);
                return 1;
            }
        }

        // 쿼리 로그 기록
        if (stristr($Query, "INSERT INTO ") || stristr($Query, "UPDATE ") || stristr($Query, "DELETE ")) {
            $this->_insertModifyLog($Query);
        }

        // $t_start = $this->mtime();
        $start_time = microtime(true);

        //if ($this->debug && !strstr($Query, "/* CACHE */"))
        //    $Query = str_replace("SELECT ", "SELECT SQL_NO_CACHE ", $Query);

        $Result = mysqli_query($this->_connection, $Query); // or die(mysqli_error());

        $elapsed_time = sprintf("%2.5f", (microtime(true) - $start_time));

        global $_queryHistoryItems;
        if (!strstr($Query, "t_pty_logs") && !strstr($Query, "t_pty_inbounds") && !strstr($Query, "t_pty_allow_ips") && !strstr($Query, "t_timeline_items")) {
            $_queryHistoryItems[] = "    elapsed = " . $elapsed_time;
        }


        if ($elapsed_time > 0.2 /* 200ms*/) {
            // echo "{$Query} {$elapsed_time} <br>";

            $slowLog = "\n❗️" . date("Y-m-d H:i:s") . " MYSQL SLOW QUERY\n";
            $slowLog .= "======================\n{$Query}\n====================== ❤️ {$elapsed_time} ms\n";

            $backtrace = debug_backtrace();
            for ($i = 0; $i < count($backtrace); $i++) {
                $file[$i] = debug_backtrace()[$i]['file'];
                $func[$i] = debug_backtrace()[$i]['function'];
                $args[$i] = debug_backtrace()[$i]['args'];

                $ar = "";
                foreach ($args[$i] as $k => $v) {
                    // pd(var_export($v), "v");
                    // $ar .= "'$v', ";
                }
                $args[$i] = $ar;
                // $args[$i] = print_r($args[$i], true);
                // $args[$i] = ptyCutStr($args[$i], 60);
                $class[$i] = debug_backtrace()[$i]['class'];
                $line[$i] = debug_backtrace()[$i]['line'];
            }

            // echo "<pre style='font-size: 10px; background-color: #ffffff; line-height: 10px;'>";
            for ($i = count($backtrace); $i >= 0; $i--) {
                if (isset($file[$i]))
                    $slowLog .= "$file[$i]:$line[$i] - $class[$i]::$func[$i]($args[$i]) \n";
            }

            $slowLogFileName = PLATYFRAMEWORK_SYSTEM_DIR . "/logs/mysql_slow_log.txt";
            file_put_contents($slowLogFileName, $slowLog, FILE_APPEND | LOCK_EX);
        }

        if (ispd()) $this->debug = true;

        if ($this->willDetectSlowQuery) {
            if ($elapsed_time > 3) {
                pd($Query, "🔴 WARNING TOO SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright);

                ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "TOO SLOW QUERY",
                    content: ["elapsedTime" => $elapsed_time, "query" => $Query],
                    serviceName: "database",
                    serviceAction: "tooSlowQuery",
                    sendTelegram: true,
                    sendTimeline: false,
                    sendUserTimeline: false,
                    sendStats: false,
                );
            } else if ($elapsed_time > 1) {
                pd($Query, "🟠 WARNING SLOW Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright, showAllCallStack: false);

                ptylogsItemsModel::build()->addLog(level: ptyLogLevel::WARNING, title: "SLOW QUERY",
                    content: ["elapsedTime" => $elapsed_time, "query" => $Query],
                    serviceName: "database",
                    serviceAction: "slowQuery",
                    sendTelegram: true,
                    sendTimeline: false,
                    sendUserTimeline: false,
                    sendStats: false,
                );

            } else {
                pd($Query, "🟢 Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright);
            }
        } else {
            pd($Query, "🟢 Query : elapsedTime: $elapsed_time", 5, \ptyTerminalColor::yellowBoldBright);
        }


        /*
        else {

            $backtrace = debug_backtrace();
            for ($i = 0; $i < count($backtrace); $i++) {
                $file[$i] = debug_backtrace()[$i]['file'];
                $func[$i] = debug_backtrace()[$i]['function'];
                $args[$i] = debug_backtrace()[$i]['args'];
                $args[$i] = print_r($args[$i], true);
                $args[$i] = ptyCutStr($args[$i], 60);
                $class[$i] = debug_backtrace()[$i]['class'];
                $line[$i] = debug_backtrace()[$i]['line'];
            }

            // echo "<pre style='font-size: 10px; background-color: #ffffff; line-height: 10px;'>";
            echo "<font color=#FF00FF>[MYSQL] =========================================</font><br>";
            for ($i = count($backtrace); $i >= 0; $i--) {
                if (isset($file[$i]))
                    echo "<font color=#FF00FF>[MYSQL] [$i]</font> <font color=blue>$file[$i]:$line[$i]</font> - $class[$i]::$func[$i](<font color=red>$args[$i]</font>)<br>";
            }

//			echo "<font color=#FF00FF>[MYSQL DEBUG] <font color=blue>$file[5]:$line[5]</font> - $class[5]::$func[5](<font color=red>$args[5]</font>)<br>";
//			echo "<font color=#FF00FF>[MYSQL DEBUG] <font color=blue>$file[4]:$line[4]</font> - $class[4]::$func[4](<font color=red>$args[4]</font>)<br>";
//			echo "<font color=#FF00FF>[MYSQL DEBUG] <font color=blue>$file[3]:$line[3]</font> - $class[3]::$func[3](<font color=red>$args[3]</font>)<br>";
//			echo "<font color=#FF00FF>[MYSQL DEBUG] <font color=blue>$file[2]:$line[2]</font> - $class[2]::$func[2](<font color=red>$args[2]</font>)<br>";
//			echo "<font color=#FF00FF>[MYSQL DEBUG] <font color=blue>$file[1]:$line[1]</font> - $class[1]::$func[1](<font color=red>$args[1]</font>)<br>";
            echo "<font color=#FF00FF>[MYSQL DEBUG]</font> [ <font color=green><xmp>$Query</xmp></font> ]<br>\n";
            echo "<font color=#FF00FF>[MYSQL DEBUG]</font> <font color=#9932cc>time = $elapsed_time </font><br>\n";
            echo "<font color=#FF00FF>[MYSQL DEBUG] =========================================<br></font>";
        }
        */

        // echo "</pre>";

        if (!$Result) {
            global $platyFramework;

            /*
                if ($_SERVER['HTTP_X_FORWARDED_HOST'] == "" && $_SERVER['HTTP_HOST'] == "") {
                    $message = "Query = {$Query}\nerror = ".mysqli_error($this->_connection);
                    $ptylogsItemsModel = ptylogsItemsModel::build();
                    $ptylogsItemsModel->addLogs("platyFramework", "mysql query error (cli)", 0, "", mysqli_error($this->_connection), $message);
                    echo "SQL ERROR = ".mysqli_error($this->_connection)." (elapsedTime: $elapsed_time)\n";
                    exit;
                }

                else {
            */
            {
                global $platyFramework;

                // 로그 메세지 생성
                $message = mysqli_error($this->_connection);

                if (strstr($message, "Deadlock found when trying to get lock; try restarting transaction")) {
                    sleep(1);
                } else {

                    if (!empty($_SERVER['HTTP_X_FORWARDED_HOST']) || !empty($_SERVER['HTTP_HOST'])) {
                        $sQuery = ptyCutStr($Query, 2000);

                        // 개발 모드에서만 500 에러시 에러값 노출
                        if ($platyFramework->system->isDevMode || ptyIsStringStart($platyFramework->request->hostName, "dev.")) {
                            $content = ptyGetArrayToString2(["QUERY" => $sQuery, "ERROR MESSAGE" => mysqli_error($this->_connection)]);
                            echo $platyFramework->loader->view("common.common.500", ["content" => "아래 메세지는 개발 환경에서만 출력됩니다.<br><br>" . nl2br($content)]);
                        } else {
                            $platyFramework->reportFatalError("MYSQL ERROR", ["QUERY" => $sQuery, "ERROR MESSAGE" => mysqli_error($this->_connection),]);
                            echo $platyFramework->loader->view("common.common.500");
                        }
                        exit;
                    }
                }

                // 무한 에러 방지
                global $mysql_error_count;
                if ($mysql_error_count > 10) die("mysql_error_count! cnt = $mysql_error_count");
                $mysql_error_count++;

                if (strstr($message, "server has gone away") ||
                    strstr($message, "Server shutdown in progress")) {
                    for ($tryCount = 0; $tryCount < 100; $tryCount++) {
                        $this->_connection = null;
                        sleep(5);
                        $this->reconnect();
                        if ($this->_connection) {
                            return $this->MysqlQuery($Query, $dispQuery);
                        }
                    }
                    return;
                }

                if (false) {
                    ob_start();
                    debug_print_backtrace();
                    $trace = ob_get_contents();
                    ob_end_clean();
                    $uri = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

                    $userData = "<pre>";
                    $userData .= "uri = {$uri}\n";
                    $userData .= "url = {$platyFramework->request->currentUrl}\n";
                    $userData .= "request = " . print_r($platyFramework->request->request->_item, true) . "\n";
                    $userData .= "message = <font color=red>" . $message . "</font>\n";
                    $userData .= "========== QUERY ===========\n";
                    $userData .= $Query;
                    $userData .= "\n========== CALL STACKS ===========\n";
                    $userData .= "\n" . $trace;
                    $userData .= "\n========== GET ===========\n";
                    $userData .= print_r($_GET, true);
                    $userData .= "\n========== POST ===========\n";
                    $userData .= print_r($_POST, true);
                    $userData .= "\n========== COOKIE ===========\n";
                    $userData .= print_r($_COOKIE, true);
                    $userData .= "\n========== SESSION ===========\n";
                    $userData .= print_r($_SESSION, true);
                    $userData .= "\n========== FILES ===========\n";
                    $userData .= print_r($_FILES, true);
//                    $userData .= "\n========== apache_request_headers ===========\n";
//                    $userData .= print_r(apache_request_headers(), true);
                    $userData .= "\n========== HTTP_RESPONSE_HEADER ===========\n";
                    $userData .= print_r($http_response_header, true);
                    $userData .= "</pre>";
                    $userData = nl2br($userData);


                    if (strstr($platyFramework->request->hostName, "dev.")) {
                        echo "</table></pre></xml><pre style='border:3px solid red;'>";
                        // echo "<font color=red>TDB :: MysqlQuery() error! <br>Query = $Query \n</FONT><FONT COLOR=#ff00ff>message = " . mysqli_error($this->_connection) . "</font><br>";
                        //			echo "error_no = ".mysqli_errno($this->_connection)."<BR>\n";
                        echo $userData;
                        echo "</font>";
                        echo "</pre>";;
                        exit;

                    } else {
                        // $this->_insertError($Query, mysqli_error($this->_connection));

                        /*
                        $email = ptyemailItemsModel::build();
                        $email->sendEmail(ptyemailSmtpItemsModel::getSuperAdminSmtpItem()->id, "", "admin", "cpueblo@platyhouse.com",
                            "[PLATYFRAMEWORK] QUERY ERROR", $userData, "");

                        $ptylogsItemsModel = ptylogsItemsModel::build();
                        $ptylogsItemsModel->addLogs("platyFramework", "DB Error", 0, "", "queryError", $userData);

                        exit;
                        */
                    }
                }
            }
        }

        if ($Result && $requireCacheWrite) {
//			cpbDebug($cacheFullFileName);
            $a = array();
            $a['query'] = $Query;
            $a['updateTime'] = date("Y-m-d H:i:s");

            $rows = array();
            while ($r = mysqli_fetch_assoc($Result)) {
                $rows[] = $r;
            }

            $a['data'] = $rows;

            $result = file_put_contents($this->activeCacheFileName, json_encode($a, JSON_UNESCAPED_UNICODE));
            if ($result === false) {
                global $platyFramework;
                $platyFramework->reportWarn(title: "파일 쓰기 오류", additional: ["파일명" => $this->activeCacheFileName]);
            }
        }

        return $Result;
    }

    function getQuery($DefaultQuery = "")
    {

        //echo $this->getSelect()." / ".$this->GetWhere()." / ".$this->GetGroupBy()." / ".$this->GetOrderBy()." / ".$this->GetLimit();
        if ($DefaultQuery)
            return $DefaultQuery . $this->getFrom() . $this->getJoin() . $this->GetWhere() . $this->getHaving() . $this->GetGroupBy() . $this->getOrderBy() . $this->GetLimit();
        else
            return $this->getSelect() . $this->getFrom() . $this->getJoin() . $this->GetWhere() . $this->getHaving() . $this->GetGroupBy() . $this->getOrderBy() . $this->GetLimit();
    }

    function getPrimaryFrom()
    {
        return $this->_SQL_FROM_ARRAY[0];
    }


    function getFrom($includeFromStr = 1)
    {
        if ($includeFromStr)
            return "\nFROM " . implode(",\n ", $this->_SQL_FROM_ARRAY) . "\n";
        else
            return "" . implode(",\n", $this->_SQL_FROM_ARRAY) . "";
    }

    function getJoin()
    {
        return implode(" \n ", $this->_SQL_JOIN_ARRAY) . " \n";
        /*
        $s = "";
        foreach ($this->_SQL_JOIN_ARRAY as $key => $value)
        {
            $s .= " $value ";
        }

        return $s;
        */
    }

    function getWhere($includeWhereStr = 1)
    {
        $s = "";
        foreach ($this->_SQL_WHERE_ARRAY as $key => $value) {
            if ($s)
                $s .= " AND \n";
            $s .= "\t(" . $value . ")";
        }

        foreach ($this->_SQL_WHERE_OR_ARRAY as $key => $value) {
            if ($s)
                $s .= " OR ";
            $s .= "( $value )";
        }

        if ($s) {
            return $includeWhereStr == 1 ? "\nWHERE $s " : $s . "\n";
        }
        return "";
    }

    function getHaving()
    {
        return $this->_SQL_HAVING;
    }

    function getGroupBy($includeGroupByStr = true)
    {
        if (!count($this->_SQL_GROUP_BY_ARRAY))
            return "";

        pd($this->_SQL_GROUP_BY_ARRAY);
        if ($includeGroupByStr)
            return "\nGROUP BY " . implode(",\n ", $this->_SQL_GROUP_BY_ARRAY) . "\n";
        else
            return "" . implode(",\n ", $this->_SQL_GROUP_BY_ARRAY) . "";

        // return $this->_SQL_GROUPBY;
    }

    function getOrderBy()
    {
        if (!count($this->_SQL_ORDERBY))
            return "";

        return "\nORDER BY " . implode(",\n ", $this->_SQL_ORDERBY) . "\n";


        /*
        if ($this->_SQL_ORDERBY)
            return $this->_SQL_ORDERBY . " " . $_SQL_ORDERBYSORT;
        else
            "";
        */
    }

    function getLimit()
    {
        if ($this->_SQL_LIMIT_ROWCOUNT == -1 || $this->_SQL_LIMIT_ROWCOUNT == 0)
            return "";

        /*
        if ($_REQUEST[$this->pageName]) {
            $Line = $this->_SQL_LIMIT_ROWCOUNT * ($_REQUEST[$this->pageName] - 1);
            return " LIMIT $Line, $this->_SQL_LIMIT_ROWCOUNT";
        } else
            return " LIMIT 0, $this->_SQL_LIMIT_ROWCOUNT";
        */
        $Line = $this->_SQL_LIMIT_ROWCOUNT * ($this->pageIndex - 1);
        return " LIMIT $Line, $this->_SQL_LIMIT_ROWCOUNT";

        return "";
    }

    function getSelect($str = true)
    {
        if ($this->_SQL_SELECT) {
            return $this->_SQL_SELECT;
        } else {
            if ($str)
                return "SELECT " . implode(", \n\t", $this->_SQL_SELECT_ARRAY);
            else
                return $this->_SQL_SELECT_ARRAY;
        }
    }

    function getSelectString()
    {
        return implode(", \n\t", $this->_SQL_SELECT_ARRAY);

    }

    function getCacheFileName($query)
    {
        $from = "";

        $pattern = '/\([^)]*\)/'; // 괄호와 그 안의 모든 문자를 찾음
        $replacement = ''; // 괄호 안의 내용을 삭제
        $testQuery = preg_replace($pattern, $replacement, $query);

        /*
        $pattern = '/select[^)]+\) from (\w+)/';
        if (preg_match($pattern, $query, $matches)) {
            $from = "s_".$matches[1];
        }
        */


        // php 에서 select subTitle from bill_keywords where mainTitle = '서울시법' 또는 select subTitle FROM bill_keywords where mainTitle = '서울시법' 와 같은 문장에서 bill_keywords 문자열을 추출
        if ($from == "") {
            $pattern = '/from\s+(\w+)\s/i';
            if (preg_match($pattern, $testQuery, $matches)) {
                $from = $matches[1];
            }
        }

        $ret = $from . "_" . sha1($query) . "_" . crc32($query) . ".sql_cache";
        return $ret;
    }

    function getCacheDir()
    {
        $path = PLATYFRAMEWORK_DATA_DIR . "/sql_cache";

        if (!file_exists($path))
            mkdir($path);

        return $path;
    }

    function _insertModifyLog($query, $message = "")
    {
        if (strstr($query, "`readCnt` = `readCnt` + 1"))
            return;

        if (strstr($query, "/* NO_LOG */"))
            return;
    }

    function sql_result($sql = "", $binds = FALSE)
    {
        $result = $this->sql_query($sql, $binds);

        if ($this->activeCacheFileName) {
            $a = json_decode(file_get_contents($this->activeCacheFileName));
            pdv($a, "sql cache readed #1");
            foreach ($a->data[0] as $value) {

                global $_queryHistoryItems;
                if (!strstr($Query, "t_pty_logs") && !strstr($Query, "t_pty_inbounds") && !strstr($Query, "t_pty_allow_ips") && !strstr($Query, "t_timeline_items")) {
                    $_queryHistoryItems[] = "    elapsed cache targeting!";
                }

                return $value;
            }
        }

        // pdv($result, "result");

        // mysqli_result 유형일 경우에만 mysqli_result 를 실행하여 첫번째 값을 가져옴
        if ($result instanceof \mysqli_result) {
            return $this->mysqli_result($result, 0, 0);
        }
        return null;

        /*
        if ($result)
            return $this->mysqli_result($result, 0, 0);
        else
            return null;
        */
    }

    function sql_query($sql = "", $binds = FALSE)
    {
        // Compile binds if needed
        if ($binds !== FALSE) {
            $sql = $this->compile_binds($sql, $binds);
        }

        return $this->MysqlQuery($sql);

        // updateDBLog($sql);

        $s = $this->_microtime() * 1;

        /*
         * if ($error)
         * $result = @mysqli_query($sql, $this->_connection) or die("<p>$sql<p>" . mysqli_errno() . " : " . mysqli_error() . "<p>error file : $_SERVER[PHP_SELF]".$this->getBacktrace());
         * else
         */
        $result = mysqli_query($sql, $this->_connection) or die("<p>$sql<p>" . mysqli_errno() . " : " . mysqli_error() . "<p>error file : $_SERVER[PHP_SELF]" . $this->getBacktrace());

        // echo "result = [$result, $this->_connection]<br>";
        return $result;
    }

    function _microtime()
    {
        return array_sum(explode(' ', microtime()));
    }

    function getBacktrace($ignore = 2)
    {
        $trace = '';
        foreach (debug_backtrace() as $k => $v) {
            if ($k < $ignore) {
                continue;
            }
            /*
             * array_walk($v['args'], function (&$item, $key) {
             * $item = var_export($item, true);
             * });
             */

            $trace .= '#' . ($k - $ignore) . ' ' . $v['file'] . '(' . $v['line'] . '): ' . (isset($v['class']) ? $v['class'] . '->' : '') . $v['function'] . '(' . implode(', ', $v['args']) . ')' . " <br>\n";
        }

        $trace = "<br>\n [[[ debug trace ]]] <br>\n" . $trace;

        return $trace;
    }

    function mysqli_result($res, $row = 0, $col = 0)
    {
        $numrows = mysqli_num_rows($res);
        if ($numrows && $row <= ($numrows - 1) && $row >= 0) {
            mysqli_data_seek($res, $row);
            $resrow = (is_numeric($col)) ? mysqli_fetch_row($res) : mysqli_fetch_assoc($res);
            if (isset($resrow[$col])) {
                return $resrow[$col];
            }
        }
        return false;
    }

    function getKeyValues($array)
    {
        foreach ($array as $key => $value) {
            $value = mysql_real_escape_string($value);
            if ($out)
                $out .= ", ";
            $out .= "`$key` = '$value'";
        }

        return $out;
    }

    function update()
    {
        $q = sprintf("UPDATE %s SET %s %s", $this->getFrom(FALSE), $this->getSet(), $this->getWhere());
        $this->MysqlQuery($q);

        /*
        ptylogsItemsModel::build()->addLog(level: ptyLogLevel::INFO, title: "SQL UPDATE",
            serviceName: "mysql",
            serviceAction: "update",
            content: $q,
        );
        */
    }

    function getSet()
    {
        return implode(",\n", $this->_SQL_SET_ARRAY) . "\n";
    }

    function insert()
    {
        $n = $this->MysqlQuery(sprintf("INSERT INTO %s SET %s %s", $this->getFrom(FALSE), $this->getSet(), $this->getWhere()));
    }

    function insertOnDuplicateKeyUpdate()
    {
        $n = $this->MysqlQuery(sprintf("INSERT INTO %s SET %s %s ON DUPLICATE KEY UPDATE %s", $this->getFrom(FALSE), $this->getSet(), $this->getWhere(), $this->getSet()));
    }

    function getQueryExceptJoin($DefaultQuery = null)
    {

        // echo $this->getSelect()." / ".$this->GetWhere()." / ".$this->GetGroupBy()." / ".$this->GetOrderBy()." / ".$this->GetLimit();
        if ($DefaultQuery)
            return $DefaultQuery . $this->getFrom() . $this->GetWhere() . $this->getHaving() . $this->GetGroupBy() . $this->getOrderBy() . $this->GetLimit();
        else
            return $this->getSelect() . $this->getFrom() . $this->GetWhere() . $this->getHaving() . $this->GetGroupBy() . $this->getOrderBy() . $this->GetLimit();
    }

    function getShortQuery($DefaultQuery)
    {
        if ($DefaultQuery)
            return $DefaultQuery . $this->GetWhere() . $this->GetGroupBy();
        else
            return $this->getSelect() . $this->GetWhere() . $this->GetGroupBy();
    }

    function addGroupBy($groupBy)
    {
        foreach ($this->_SQL_GROUP_BY_ARRAY as $key => $value) {
            // 이미 등록되어 있으면 추가 금지
            if ($value == $groupBy)
                return $this;
        }
        array_push($this->_SQL_GROUP_BY_ARRAY, $groupBy);
        pd($this->_SQL_GROUP_BY_ARRAY, "this->_SQL_GROUP_BY_ARRAY");
        return $this;


        if ($this->_SQL_GROUPBY)
            $this->_SQL_GROUPBY .= " AND " . $GroupBy;
        else
            $this->_SQL_GROUPBY = " GROUP BY " . $GroupBy;

        return $this;
    }

    function addHaving($having)
    {
        if ($this->_SQL_HAVING)
            $this->_SQL_HAVING .= " AND " . $having;
        else
            $this->_SQL_HAVING = " HAVING " . $having;

        return $this;
    }

    function addUnion($bottom)
    {
        array_push($this->_SQL_UNION_ARRAY, $bottom);
    }

    // CountQuery 로 부터 첫번째 리턴된 인자를 얻어옴

    function SetDefaultQuery($Query)
    {
        global $_SQL_DEFAULTQUERY;
        $_SQL_DEFAULTQUERY = $Query;
    }

    function SetDefaultCountQuery($Query)
    {
        global $_SQL_DEFAULTCOUNTQUERY;
        $_SQL_DEFAULTCOUNTQUERY = $Query;
    }

    function DefaultQuery($Debug = FALSE)
    {
        global $_SQL_RESULT;
        global $_SQL_DEFAULTQUERY;
        $_SQL_RESULT = $this->MysqlQuery($this->GetQuery($_SQL_DEFAULTQUERY), $Debug);
        return $_SQL_RESULT;
    }

    function DefaultCountQuery($Debug = FALSE)
    {
        global $_SQL_RESULT;
        global $_SQL_DEFAULTCOUNTQUERY;
        $_SQL_RESULT = $this->MysqlQuery($this->GetQuery($_SQL_DEFAULTCOUNTQUERY), $Debug);
        return $_SQL_RESULT;
    }

    function Query($Query, $binds = FALSE)
    {
        global $_SQL_RESULT;

        // Compile binds if needed
        if ($binds !== FALSE) {
            $Query = $this->compile_binds($Query, $binds);
        }


        if ($Query != "") {
            $_SQL_RESULT = $this->MysqlQuery($Query, false);
            return $_SQL_RESULT;
        } else {
            return $this->MysqlQuery($this->getQuery(), false);
        }
    }

    /**
     * code ignitor 3
     *
     * "Smart" Escape String
     *
     * Escapes data based on type
     * Sets boolean and null types
     *
     * @param string
     * @return    mixed
     */
    public function escape($str)
    {
        if (is_array($str)) {
            $str = array_map(array(&$this, 'escape'), $str);
            return $str;
        } elseif (is_string($str) or (is_object($str) && method_exists($str, '__toString'))) {
            return "'" . $this->escape_str($str) . "'";
        } elseif (is_bool($str)) {
            return ($str === FALSE) ? 0 : 1;
        } elseif ($str === NULL) {
            return 'NULL';
        }

        return $str;
    }


    /**
     * for code ignitor 3
     *
     * Compile Bindings
     *
     * @param string    the sql statement
     * @param array    an array of bind data
     * @return    string
     */
    public $bind_marker = '?';

    public function compile_binds($sql, $binds)
    {
        pd("sql = $sql, binds = " . print_r($binds, true), "compile_binds");
        // {a}, {b} 형태를 처리
        if (true) {
            foreach ($binds as $key => $value) {
                $placeholder = '{' . $key . '}';
                // $escapedValue = is_numeric($value) ? $value : mrea($value);
                // $escapedValue = $value;
                $escapedValue = mrea($value);
                $sql = str_replace($placeholder, $escapedValue, $sql);
            }

            // pdfe($sql);
        }

        if (empty($this->bind_marker) or strpos($sql, $this->bind_marker) === FALSE) {
            return $sql;
        } elseif (!is_array($binds)) {
            $binds = array($binds);
            $bind_count = 1;
        } else {
            // Make sure we're using numeric keys
            $binds = array_values($binds);
            $bind_count = count($binds);
        }

        // We'll need the marker length later
        $ml = strlen($this->bind_marker);

        // Make sure not to replace a chunk inside a string that happens to match the bind marker
        if ($c = preg_match_all("/'[^']*'|\"[^\"]*\"/i", $sql, $matches)) {
            $c = preg_match_all('/' . preg_quote($this->bind_marker, '/') . '/i',
                str_replace($matches[0],
                    str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]),
                    $sql, $c),
                $matches, PREG_OFFSET_CAPTURE);

            // Bind values' count must match the count of markers in the query
            if ($bind_count !== $c) {
                return $sql;
            }
        } elseif (($c = preg_match_all('/' . preg_quote($this->bind_marker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count) {
            return $sql;
        }

        do {
            $c--;

            // 2025-05-04 escape 가 되지 않는 듯 하다. 원래 사용하던 mrea 로 대체해야 하는가?
            $escaped_value = $this->escape($binds[$c]);
            $escaped_value = mrea($binds[$c]);
//            pdf($binds);
//            pdf($escaped_value, "escaped_value");
            if (is_array($escaped_value)) {
                $escaped_value = '(' . implode(',', $escaped_value) . ')';
            }
            $sql = substr_replace($sql, $escaped_value, $matches[0][$c][1], $ml);
        } while ($c !== 0);

        return $sql;
    }

    // --------------------------------------------------------------------

    /**
     * Escape String
     *
     * @param string|string[] $str Input string
     * @param bool $like Whether or not the string will be used in a LIKE condition
     * @return    string
     */
    public function escape_str($str, $like = FALSE)
    {
        if (is_array($str)) {
            foreach ($str as $key => $val) {
                $str[$key] = $this->escape_str($val, $like);
            }

            return $str;
        }

        $str = $this->_escape_str($str);

        // escape LIKE condition wildcards
        if ($like === TRUE) {
            return str_replace(
                array($this->_like_escape_chr, '%', '_'),
                array($this->_like_escape_chr . $this->_like_escape_chr, $this->_like_escape_chr . '%', $this->_like_escape_chr . '_'),
                $str
            );
        }

        return $str;
    }

    // --------------------------------------------------------------------

    /**
     * Escape LIKE String
     *
     * Calls the individual driver for platform
     * specific escaping for LIKE conditions
     *
     * @param string|string[]
     * @return    mixed
     */
    public function escape_like_str($str)
    {
        return $this->escape_str($str, TRUE);
    }

    // --------------------------------------------------------------------

    /**
     * Platform-dependent string escape
     *
     * @param string
     * @return    string
     */
    protected function _escape_str($str)
    {
        return str_replace("'", "''", $this->remove_invisible_characters($str, FALSE));
    }


    function remove_invisible_characters($str, $url_encoded = TRUE)
    {
        $non_displayables = array();

        // every control character except newline (dec 10),
        // carriage return (dec 13) and horizontal tab (dec 09)
        if ($url_encoded) {
            $non_displayables[] = '/%0[0-8bcef]/i';    // url encoded 00-08, 11, 12, 14, 15
            $non_displayables[] = '/%1[0-9a-f]/i';    // url encoded 16-31
            $non_displayables[] = '/%7f/i';    // url encoded 127
        }

        $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S';    // 00-08, 11, 12, 14-31, 127

        do {
            $str = preg_replace($non_displayables, '', $str, -1, $count);
        } while ($count);

        return $str;
    }


    function getCount()
    {
        $ret = $this->sql_result($this->getCountQuery());

        if ($this->debug)
            pd($ret, "getCount()");

        return $ret;
    }

    function getCountWithJoin()
    {
        $ret = $this->sql_result($this->getCountQueryWithJoin());

        if ($this->debug)
            pd("ptyMysql::getCount() ret = $ret");

        return $ret;
    }


    // 쿼리를 실행한 후 결과값에서 한행을 얻는다.

    function getCountQuery($DefaultQuery = "")
    {
        /*
        if ($DefaultQuery)
            $ret = $DefaultQuery . $this->GetWhere() . $this->GetGroupBy();
        else
            $ret = $this->getCountSelect() . $this->GetWhere() . $this->GetGroupBy();

        if ($this->debug)
            echo "TDB :: getCountQuery() ret = $ret <br>";
        */
        if ($DefaultQuery)
            $ret = $DefaultQuery . $this->GetWhere();
        else
            $ret = $this->getCountSelect() . $this->GetWhere();

        return $ret;
    }

    // 쿼리를 실행한 후 결과값에서 한행을 얻는다.
    function getCountQueryWithJoin($DefaultQuery = "")
    {
        /*
        if ($DefaultQuery)
            $ret = $DefaultQuery . $this->GetWhere() . $this->GetGroupBy();
        else
            $ret = $this->getCountSelect() . $this->GetWhere() . $this->GetGroupBy();

        if ($this->debug)
            echo "TDB :: getCountQuery() ret = $ret <br>";
        */
        if ($DefaultQuery)
            $ret = $DefaultQuery . $this->GetWhere();
        else
            $ret = $this->getCountSelect() . $this->getJoin() . $this->GetWhere();

        return $ret;
    }

    // 쿼리를 실행한 후 결과값에서 한행을 얻는다.

    function getCountSelect()
    {
        if ($this->_SQL_COUNT_SELECT)
            return $this->_SQL_COUNT_SELECT;
        else
            return "SELECT COUNT(*) " . $this->getFrom(); //  . $this->getJoin();
        // return "SELECT COUNT(`".$this->getFrom(false)."`.`id`) " . $this->getFrom() . " ".$this->getJoin();
    }


    // 결과값에서 한행 연관배열(이름으로)로 얻는다.

    function queryCount($Query, $Debug = FALSE)
    {
        global $_SQL_RESULT;

        if ($Query != "") {
            $_SQL_RESULT = $this->MysqlQuery($Query, $Debug);
            return $_SQL_RESULT;
        } else {
            return $this->MysqlQuery($this->getCountQuery(), $Debug);
        }
    }

    function GetDefaultCountQuery()
    {
        global $_SQL_DEFAULTCOUNTQUERY;
        return $_SQL_DEFAULTCOUNTQUERY;
    }

    function IsRowsZero()
    {
        global $_SQL_RESULT;
        if (mysqli_num_rows($_SQL_RESULT) == 0)
            return TRUE;
        else
            return FALSE;
    }

    function fetch($sql, $error = TRUE)
    {
        return $this->sql_fetch($sql, $error);
    }

    //
    // 날짜 검색 쿼리
    //
    // ex)
    // $db->addWhere($db->getSearchDateRange ("개통일", "2013-05-01")); // 특정일만
    // $db->addWhere($db->getSearchDateRange ("개통일", "2013-05-01~")); // 특정일 부터
    // $db->addWhere($db->getSearchDateRange ("개통일", "~2013-05-01")); // 특정일 까지
    // $db->addWhere($db->getSearchDateRange ("개통일", "2013-01-01~2013-05-01")); // 특정일 사이
    //

    function sql_fetch($sql = "", $binds = FALSE)
    {
        $result = $this->sql_query($sql, $binds);
        $row = $this->sql_fetch_array($result);
        return $row;
    }

    public function sqlFetchItem($sql = "", $binds = FALSE)
    {
        $fetch = $this->sql_fetch($sql, $binds);
        return ptyArrayItemModel::buildByMap($fetch);
    }

    function sql_fetch_array($result)
    {
        $row = @mysqli_fetch_assoc($result);
        return $row;
    }

    /**
     * @deprecated
     *
     * =================================================================
     * //
     * // addWhereFromSearch()
     * //
     * // $Array[SearchRangeColumn]            : 범위를 검색. 형태 : "10~20"
     * // $Array[SearchRangeMaxColumn]            : 범위를 검색. 형태 : "10~20" 예외 : 20 만 입력시 0~20을 검색
     * // $Array[SearchRangeMinColumn]            : 범위를 검색. 형태 : "10~20" 예외 : 20 만 입력시 20~ 을 검색
     * // $Array[SearchLikeColumn]                : LIKE 를 콤마 와 - 를 사용하여 OR 검색. 형태 : "문장1,문장2,-문장3"
     * // $Array[SearchEqualColumn]            : EQUAL 을 비교
     * // $Array[SearchStartLikeColumn]        : LIKE '%문장' 을 검색
     * // $Array[SearchEndLikeColumn]            : LIKE '문장%' 을 검색
     * // $Array[SearchExceptYearMonthColumn]    : 콤마를 사용하여 지정 월, 달을 제외한 중복 검색. 2013-05, 2013-06 => 2013년 5월과 6월을 제외 처리
     * //
     * // ex)
     * // 데이터 지정 예 : $_REQUEST[SearchLikeColumn][컬럼명] = 검색값
     * =================================================================
     */
    function addWhereFromSearch(&$params, $tablename = "", $orderstyle = 0)
    {
        // 키 sort
        if ($params['sc']) {
            if ($params['sc'] == "function") return;
            $sa = strtolower($params($params['sa']));
            if ($sa == "asc" || $sa == "desc")
                $this->addOrderBy(sprintf("%s %s", $params['sc'], $params['sa']));
        }

        if ($params['page'] != "")
            $this->setPageIndex((int)$params['page']);

        /*
         * 기본 정렬 기능 추가
         * // getSortLink 함수를 통해 연동됨
         * // 다른 addOrderBy 를 하기 전에 호출해야 정상 동작함
         */
        if ($params[sort_asc] == "asc")
            $params[new_sort_asc] = "desc";
        else
            $params[new_sort_asc] = "asc";

        if ($params[sort]) {
            if ($orderstyle == 0) {
                if (strstr($params[sort], ".")) {
                    if ($params[sort][0] == '.')
                        $this->addOrderBy(substr($params[sort], 1) . " $params[sort_asc]");
                    else
                        $this->addOrderBy("$params[sort] $params[sort_asc]");
                } else
                    if ($tablename && !strstr($params[sort], $tablename))
                        $this->addOrderBy("$tablename.$params[sort] $params[sort_asc]");
                    else
                        $this->addOrderBy("$params[sort] $params[sort_asc]");
            } else
                if ($orderstyle == 1) {
                    $this->addOrderBy("$params[sort] $params[sort_asc]");
                }
        }
        // 기본 검색 처리
        if ($params[SearchText] && $tablename && $params[SearchType]) {
            $this->bDestroymSelect = true;
            switch ($params[SearchType]) {
                case "All":
                {
                    $lst = $this->sql_list("desc $tablename");
                    $s = "";
                    foreach ($lst as $row) {
                        $c = $row[Field];
                        if ($s)
                            $s .= " OR ";
                        $s .= "$tablename.`$c` LIKE '%$params[SearchText]%'";
                    }
                    $this->addWhere("$s");
                    break;
                }

                default:
                    $this->addWhere("`$params[SearchType]` LIKE '%" . $params[SearchText] . "%'");
                    break;
            }
        }

        // 기본 검색 처리
        if ($params[SearchText2] && $tablename) {
            switch ($params[SearchType2]) {
                case "All":
                {
                    $lst = $this->sql_list("desc $tablename");
                    $s = "";
                    foreach ($lst as $row) {
                        $c = $row[Field];
                        if ($s)
                            $s .= " OR ";
                        $s .= "`$c` LIKE '%$params[SearchText2]%'";
                    }
                    $this->addWhere("$s");
                    break;
                }

                default:
                    $this->addWhere("`$params[SearchType2]` LIKE '%" . $params[SearchText2] . "%'");
                    break;
            }
        }

        // 20 입력시 20 이상을 검색, 0~20 입력시 사이를 검색
        foreach ($params[SearchRangeColumn] as $Key => $Value) {
            if ($Value != "") {
                $s = explode("~", $Value);
                $Start = $s[0];
                $Max = $s[1];

                // echo "Start = $Start, Max = $Max";

                if ($Start && $Max)
                    $this->addWhere("$Key >= '$Start' AND $Key <= '$Max'");
                else
                    if ($Start && strstr($Value, $Start . "~"))
                        $this->addWhere("$Key >= '$Start'");
                    else
                        if ($Max && strstr($Value, "~" . $Max))
                            $this->addWhere("$Key <= '$Max'");
                        else
                            $this->addWhere("$Key = '$Start'");
            }
        }

        // 20 입력시 0~20 이상을 검색, 10~20 입력시 사이를 검색
        foreach ($params[SearchRangeMaxColumn] as $Key => $Value) {
            if ($Value != "") {
                $s = explode("~", $Value);
                $Start = $s[0];
                $Max = $s[1];

                if ($Max)
                    $this->addWhere("$Key >= '$Start' AND $Key < '$Max'");
                else
                    $this->addWhere("$Key >= 0 AND $Key <= '$Start'");
            }
        }

        // 해당 월의 첫날부터 값을 포함하는 일자까지 검색
        // ex) 2013-07-05 --> 2013-07-01 <= date <= 2013-07-05 까지 검색
        foreach ($params[SearchMonthDateMaxColumn] as $Key => $Value) {
            if ($Value != "") {
                $s = explode("~", $Value);
                $Start = $s[0];
                $Max = $s[1];

                if ($Max)
                    $this->addWhere("$Key >= '$Start' AND $Key <= '$Max'");
                else
                    $this->addWhere("Year($Key) = Year('$Start') AND Month($Key) = Month('$Start') AND Day($Key) >= 1 AND Day($Key) <= Day('$Start')");
            }
        }

        foreach ($params[SearchLikeColumn] as $Key => $Value) {
            if ($Value != "") {
                // 검색 내용에 , 가 들어갈 경우 해당 키워드를 모두 검색해준다.
                // 예 : 거래처에 PS,MS 를 입력시 쿼리 = 거래처 LIKE '%PS%' OR 거래처 LIKE '%MS%'
                if (strstr($Value, ",")) {
                    $w = "";
                    $s = explode(",", $Value);
                    foreach ($s as $k => $v) {
                        // echo ord($v). "--";
                        if (ord($v) == ord("-")) {
                            if ($w)
                                $w .= " AND ";
                            $w .= "$Key NOT LIKE '%" . substr($v, 1) . "%'";
                        } else {
                            if ($w)
                                $w .= " OR ";
                            $w .= "$Key LIKE '%$v%'";
                        }
                    }

                    $this->addWhere("$w");
                } else {
                    $v = $Value;
                    $w = "";
                    if (ord($v) == ord("-")) {
                        if ($w)
                            $w .= " AND ";
                        $w .= "$Key NOT LIKE '%" . substr($v, 1) . "%'";
                    } else {
                        if ($w)
                            $w .= " OR ";
                        $w .= "$Key LIKE '%$v%'";
                    }
                    $this->addWhere("$w");
                    // $this->addWhere("$Key LIKE '%$Value%'");
                }
            }
        }


        // search like column
        foreach ($params['slc'] as $Key => $Value) {
            if ($Value != "") {
                $this->addWhere("$Key LIKE '%$Value%'");
            }
        }

        // search equal column
        foreach ($params as $param => $v) {
            if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0) {
                $this->addWhere(substr($param, 4) . " = '$v'");
            }
        }
        // ptyDebug($params);
        foreach ($params['sec'] as $Key => $Value) {
            if ($Value != "") {
                $this->addWhere("$Key = '$Value'");
            }
        }

        // search end like column
        foreach ($params['selc'] as $Key => $Value) {
            if ($Value != "") {
                $this->addWhere("$Key LIKE '$Value%'");
            }
        }

        // Search start like column
        foreach ($params['sslc'] as $Key => $Value) {
            if ($Value != "") {
                $this->addWhere("$Key LIKE '$%Value'");
            }
        }

        // SearchExceptYearMonthColumn : 2013-05, 2013-06 을 제외 또는 2013-05~06 을 제외 처리
        foreach ($_REQUEST[SearchExceptYearMonthColumn] as $Key => $Value) {
            if (!$Value)
                continue;

            $s = explode(",", $Value);
            foreach ($s as $v) {
                if (strstr($v, "~")) {
                    $s = sscanf($v, "%d-%d~%d", $Year, $Mon1, $Mon2);

                    for ($i = $Mon1; $i <= $Mon2; $i++) {
                        $this->addWhere("NOT (Year($Key) = '$Year' AND Month($Key) = '$i')");
                    }
                } else {
                    $ym = explode("-", $v);
                    $this->addWhere("NOT (Year($Key) = '$ym[0]' AND Month($Key) = '$ym[1]')");
                }
            }
        }

        // Search Range Column : 기간 검색
        foreach ($params[src] as $Key => $Value) {
            $db = new ptyMysql();

            $s = explode("~", $Value);
            $Start = $s[0];
            $Max = $s[1];

            if ($Max) {
                if ($Value[0] == "~")
                    $db->addWhere("$Key <= '$Max'");
                else
                    $db->addWhere("$Key >= '$Start' AND $Key <= '$Max'");
            } else {
                if ($Value[strlen($Start) + 0] == "~")
                    $db->addWhere("$Key >= '$Start'");
                else
                    $db->addWhere("$Key LIKE '%$Start%'");
            }

            $this->addWhere($db->getWhere(FALSE));
        }

    }

    function addOrderBy($Order)
    {
        if (!$Order || $Order == " ") return $this;

        $this->_SQL_ORDERBY[] = $Order;
        return $this;
    }

    /**
     * ORDER BY FIELD(columnName, 1, 2, 3, 4) 형으로 쿼리를 입력합니다
     * @param string $col
     * @param array $ids
     * @return $this
     */
    function addOrderByIn(string $col, array $ids)
    {
        pd($ids, "addOrderByIn.ids");
        if (is_array($ids))
            $this->addOrderBy("/* addOrderByIn */ FIELD(" . $col . ", " . ptyImplodeApostrophe($ids) . ")");
        return $this;
    }

    /*
     * static function getWhere지난달1일부터오늘까지($Key)
     * {
     * return "YEAR($Key) = YEAR(NOW()) - 1 AND MONTH($Key) = MONTH(NOW()) AND DAY($Key) > 1 AND DAY($Key) <= DAY(NOW())";
     * }
     */

    function setPageIndex($pageIndex)
    {
        if (is_null($pageIndex))
            $pageIndex = 1;
        $this->pageIndex = $pageIndex;

        return $this;
    }

    function sql_items($sql = "", $debug = 0, array $bools = [], $itemModel = "platyFramework\\ptyArrayItemModel")
    {
        $lst = $this->sql_list($sql, $debug, $bools);
        $ret = array();
        foreach ($lst as $k => $v) {
            // $item = ptyArrayItemModel::buildByMap($v);
            $item = $itemModel::buildByMap($v);
            $ret[] = $item;
        }

        return $ret;

        // return $this->sql_list ($sql, $debug, $bools);
    }

    /**
     * @param $sql
     * @param $debug
     * @return array|mixed
     */
    function sql_list($sql = "", $debug = 0, array $bools = [], $binds = FALSE)
    {
        $sql_list = array();

        if ($debug)
            echo "<TEXTAREA ROWS=5 COLS=100>sql_list debug. \n $sql</TEXTAREA>";


        $sql_q = $this->sql_query($sql, $binds);
        pdv($sql_q, "sql_query result");

        if ($this->activeCacheFileName) {
            $a = json_decode(file_get_contents($this->activeCacheFileName), true);
            pdv($a, "sql cache readed #2");
            return $a['data'];
        }

        $i = 0;
        while ($sql_r = $this->sql_fetch_array($sql_q)) {
            foreach ($sql_r as $k => $v) {
                if (in_array($k, $bools))
                    $v = boolval($v);
//                \t  에 의한 tttt 노출 오류
//                if (!is_null($v) && !get_magic_quotes_gpc()) {
//                    $v = stripslashes($v);
//                }
                $sql_r[$k] = $v;
            }
            $sql_list[$i] = $sql_r;
            $i++;
        }

        return $sql_list;
    }

    public function getPagingItems(string $sql)
    {


    }

    public function getBindsQuery($sql, $binds = FALSE)
    {
        if ($binds !== FALSE) {
            return $this->compile_binds($sql, $binds);
        }
        return $sql;
    }

    /**
     * @param $Where
     * @param $binds
     * @return $this
     * @example
     * $bookingItemsModel->db->addWhere("DATE(dateTime) = DATE(NOW())");
     * $bookingItemsModel->db->addWhere("DATE(dateTime) = ?", [date("Y-m-d")]);
     * $bookingItemsModel->db->addWhere("DATE(dateTime) = {0}", [date("Y-m-d")]);
     * $bookingItemsModel->db->addWhere("DATE(dateTime) = {a}", ["a" => date("Y-m-d")]);
     *
     */
    function addWhere($Where, $binds = FALSE)
    {
        // GLOBAL $_SQL_WHERE_ARRAY;
        // GLOBAL $_SQL_WHERE;
        if (!$Where)
            return $this;

        if ($binds !== FALSE) {
            $Where = $this->compile_binds($Where, $binds);
        }

        if ($this->_SQL_WHERE)
            $this->_SQL_WHERE .= " AND " . $Where;
        else
            $this->_SQL_WHERE = " WHERE " . $Where;

        array_push($this->_SQL_WHERE_ARRAY, $Where);
        return $this;
    }

    /**
     * @param string $tableName
     * @param string $indexName
     * @param array $matchKeys
     * @param bool $isRebulid
     * @return void
     */
    function migrateFullTextIndex($tableName, $indexName, $matchKeys, $isRebulid = false)
    {
        $cq = "show index from $tableName where Key_name like '$indexName'";
        if ($this->sql_result($cq)) {
            if ($isRebulid == true) {
                $this->sql_query("alter table $tableName drop index $indexName");

                $q = "ALTER TABLE `$tableName` ADD FULLTEXT INDEX `$indexName` (`" . implode("`, `", $matchKeys) . "`) WITH PARSER ngram;";
                $this->sql_query($q);
            }
        } else {
            $q = "ALTER TABLE `$tableName` ADD FULLTEXT INDEX `$indexName` (`" . implode("`, `", $matchKeys) . "`) WITH PARSER ngram;";
            $this->sql_query($q);
        }

    }

    /**
     * 풀텍스트 검색을 위함
     * 모든 문자열을 1자로 분리하여 검색합니다.
     *
     * my.cnf 에 아래를 포함
     *
     * [mysqld]
     * # FullText 최소 검색 글자 길이
     * innodb_ft_min_token_size=1
     * ft_min_word_len=1
     * ngram_token_size=1
     *
     *
     * migrateFullTextIndex("ft_search", ["law_name_kr", ...]);
     *
     * @param string|array $matchKey
     * @param $keyword
     */
    function getWhereFullText($matchKey, $keyword, $score = "")
    {
        $newKeyword = $keyword;

        if (true) {
            $ifScore = $score != "" ? "> $score" : "";

            $keyword = str_replace(" ", "", $keyword);
            $length = mb_strlen($keyword, 'utf-8');
            $parts = array();

            for ($i = 0; $i < $length; $i++) {
                $substring = mb_substr($keyword, $i, 2, 'utf-8');

                if (mb_strlen($substring) == 2)
                    $parts[] = $substring;
            }

            if (count($parts) == 0 && $keyword != "")
                $parts[] = $keyword;

            $partsAll = "+" . implode("* +", $parts) . "*";

            if (is_array($matchKey))
                $w[] = "# getWhereFullText()\nMATCH (" . implode(",", $matchKey) . ") AGAINST ('$partsAll' in boolean mode) $ifScore";
            else
                $w[] = "# getWhereFullText()\nMATCH ($matchKey) AGAINST ('$partsAll' in boolean mode) $ifScore";
        }

        if (false) {
            // 한글자씩 잘라서 삽입
            $text_array = array();
            for ($i = 0; $i < mb_strlen($newKeyword, "UTF-8"); $i++) {

                $char = mb_substr($newKeyword, $i, 1, 'UTF-8');

                if (trim($char))
                    array_push($text_array, $char);
            }

            $w = array();
            // $kv = explode(" ", $text_array);
            $kv = $text_array;

            $s = "";
            if (count($kv)) {
                foreach ($kv as $k) {
                    $s .= "+" . mre($k) . "* ";
                }
            } else {
                $s .= "+" . mre($text_array) . "* ";
            }

            $s = trim($s);

            if (is_array($matchKey))
                $w[] = "MATCH (" . implode(",", $matchKey) . ") AGAINST ('$s' in boolean mode)";
            else
                $w[] = "MATCH ($matchKey) AGAINST ('$s' in boolean mode)";
        }


        // 통
        if (false) {
            $s = "+$keyword*";
            if (is_array($matchKey))
                $w[] = "/* T1 */ MATCH (" . implode(",", $matchKey) . ") AGAINST ('$s' in boolean mode)";
            else
                $w[] = "/* T1 */ MATCH ($matchKey) AGAINST ('$s' in boolean mode)";
        }


        if (false) {
            // 통
            $keywords = explode(" ", $keyword);
            $words = array();
            foreach ($keywords as $_keyword) {
                $words[] = "+$_keyword*";

                if (false) {
                    if (is_array($matchKey))
                        $w[] = "/* T2 */ MATCH (" . implode(",", $matchKey) . ") AGAINST ('$s' in boolean mode)";
                    else
                        $w[] = "/* T2 */ MATCH ($matchKey) AGAINST ('$s' in boolean mode)";
                }
            }

            $wordsAll = "+" . implode("* +", $keywords) . "*";

            if (is_array($matchKey))
                $w[] = "MATCH (" . implode(",", $matchKey) . ") AGAINST ('$wordsAll' in boolean mode)";
            else
                $w[] = "MATCH ($matchKey) AGAINST ('$wordsAll' in boolean mode)";

        }

        return implode(" OR ", $w);
    }

    function addWhereFullText($matchKey, $keyword)
    {
        $this->addWhere($this->getWhereFullText($matchKey, $keyword));
        return $this;
    }

    function addSelectFullText($matchKey, $keyword)
    {
        $this->addSelect($this->getSelectFullText($matchKey, $keyword));
        return $this;
    }

    function getSelectFullText($matchKey, $keyword, $searchScoreName = "searchScore")
    {
        return $this->getWhereFullText($matchKey, $keyword) . " as $searchScoreName";
    }


    /**
     * 특정 파라미터중에 검색을 허용할 파라미터를 지정하여 검색하게 합니다
     * page, sec_, slc_, sq_ 를 지원합니다
     *
     * ex)
     * $this->db->addWhereParam($this->request->request, ["page", "sec_petName"]);
     *
     * sq 는 다음의 형식을 지원합니다
     * 1. = NULL
     * 2. != NULL
     * 3. > ABCD
     * 4. < ABCD
     * 4. >= ABCD
     * 4. <= ABCD
     * 4. "ABCD"
     * 4. ABCD OR hello
     *
     * @param $params
     * @param $allowParams
     */
    function addWhereParam($params, $allowParams = null)
    {
        // pdf($params, "params");
        foreach ($params as $param => $v) {
            if (isset($allowParams) && is_array($allowParams)) {
                foreach ($allowParams as $allowParam) {
                    if ($param == $allowParam) {
                        if ($params['page'] != "")
                            $this->setPageIndex((int)$params['page']);

                        if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v == "{EMPTY}") {
                            $keyName = substr($param, 4);
                            $this->addWhere("`" . mre($keyName) . "` = '' OR `" . mre($keyName) . "` is NULL");
                        } else if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v == "{!EMPTY}") {
                            $keyName = substr($param, 4);
                            $this->addWhere("`" . mre($keyName) . "` != ''");
                        } else if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v != "") {
                            $this->addWhereEqual(substr($param, 4), $v);
                        } else if (stripos($param, "slc_") !== false && stripos($param, "slc_") == 0 && $v != "") {
                            $this->addWhereLike(substr($param, 4), $v);
                        }
                    }
                }
            } else {

                // $colName 은 내부에서 mrea 로 감싸므로 앞뒤 ` 를 제거합니다.
                if (true) {
                    // $tableName = $this->getPrimaryFrom();

                    // pdf($param, "param");
                    if (strstr($param, ".")) {
                        $tableName = substr($param, 0, strrpos($param, "."));
                        $_colName = substr($param, strrpos($param, ".") + 1);
                        $colName = "`$tableName`.`$_colName`";
                        // $value = str_replace($_colName, "", $param);
                    } // sq_booking_isActive 형태일 경우 sq_ 를 제거한 값을 $colName 으로 남김
                    else if (strstr($param, "_")) {
                        // sq_ 등의 첫번째 항목을 제거
                        $pos = strpos($param, '_'); // 마지막 언더스코어 위치
                        $_colName = substr($param, $pos + 1);
                        $colName = $_colName;

                        // pdf($colName, "colName");

                        // tableName_id 처럼 되어 있지 않다면 `yourTable`.`id` 로 변경함
                        if (!strstr($colName, "_")) {
                            $pos = strrpos($param, '_'); // 마지막 언더스코어 위치
                            $_colName = substr($param, $pos + 1);
                            $_colName = mreb($_colName);
                            $colName = "`" . $this->getPrimaryFrom() . "`.$_colName";
                        }
                    }
                    /*
                    else {
                        $pos = strrpos($param, '_'); // 마지막 언더스코어 위치
                        $_colName = substr($param, $pos + 1);
                        $colName = "`" . $this->getPrimaryFrom() . "`.`$_colName`";
                        // $value = str_replace($_colName, "", $param);
                    }
                    */
                }

                $value = $v;

//                pdf($param, "param");
//                pdf($colName, "colName");
//                pdf($value, "value");
                // exit;

                if ($params['page'] != "")
                    $this->setPageIndex((int)$params['page']);

                if (stripos($param, "sd_") !== false && stripos($param, "sd_") == 0 && $v != "") {
                    // $colName = substr($param, 3);
                    // $query = $this->getWhereDateTime(strstr($colName, ".") ? $colName : $this->getPrimaryFrom(), $colName, $v);
                    $query = $this->getWhereDateTime($colName, $v);
                    pd($query, "addWhereParam() sd.query");
                    $this->addWhere($query);
                } else if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v == "{EMPTY}") {
                    $keyName = substr($param, 4);
                    $this->addWhere("`" . mre($keyName) . "` = '' OR `" . mre($keyName) . "` is NULL");
                } else if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v == "{!EMPTY}") {
                    $keyName = substr($param, 4);
                    $this->addWhere("`" . mre($keyName) . "` != ''");
                } else if (stripos($param, "sec_") !== false && stripos($param, "sec_") == 0 && $v != "") {
                    $col = substr($param, 4);
                    if (!strstr($col, ".")) {
                        $col = "/* added getPrimaryFrom() #1 */ `" . $this->getPrimaryFrom() . "`." . $col;
                    }
                    $this->addWhereEqual($col, $v);
                } else if (stripos($param, "slc_") !== false && stripos($param, "slc_") == 0 && $v != "") {

                    $k = substr($param, 4);

                    // 키 네임에 t_pty_user_`fullName` 형식이 들어오면 t_pty_users.`fullName` 으로 치환합니다
                    $k = str_replace("_`", ".`", $k);

                    $this->addWhereLike($k, $v);
                } else if (stripos($param, "sq_") !== false && stripos($param, "sq_") == 0 && $v != "") {

                    // setpd(true);
                    pd($v, "v");
                    $k = substr($param, 3);

                    // 키 네임에 테이블명@컬럼명 형식이 들어올 경우
                    if (stripos($k, "@") == true) {
                        $k = "`" . str_replace("@", "`.`", mre($k)) . "`";
                    } else if (stripos($k, ".") == true) {
                        $k = "/* addWhereParam() sq with dot Join */`" . str_replace(".", "`.`", mre($k)) . "`";
                        $this->needsJoinWhenTotalCount = true;
                    } else {
                        pd($colName, "colName");
                        $k = "/* addWhereParam().sq */ $colName";
                    }

                    $v = trim($v);
                    $v = html_entity_decode($v);

                    // https://stackoverflow.com/questions/2202435/php-explode-the-string-but-treat-words-in-quotes-as-a-single-word
                    // 단어와 큰 따움표를 분기함
                    preg_match_all('/"(?:\\\\.|[^\\\\"])*"|\S+/', $v, $values);
                    $values = $values[0];

                    $q = "";
                    $largerThan = "";
                    pd($values, "sq.values");
                    foreach ($values as $value) {

                        $value = trim($value);
                        $first = $value[0];
                        $second = $value[1];
                        $end = $value[strlen($value) - 1];

                        pd("first = $first, value = $value", "sq.values.value");

                        // 부등호 처리
                        if ($value == ">=" || $value == "<=" || $value == ">" || $value == "<" || $value == "=" || $value == "!=") {
                            $largerThan = $value;
                            continue;
                        }

                        debug($first, "first");
                        debug($end, "end");

                        if ($value == "or" || $value == "OR") {
                            if ($q)
                                $q .= "OR ";
                            continue;
                        } else if ($value == "and" || $value == "AND") {
                            if ($q)
                                $q .= "AND ";
                            continue;
                        } else {
                            if ($q)
                                $q .= "AND ";
                        }

                        // 부등호 처리
                        if ($largerThan != "") {
                            $k = mre($k);
                            $value = mre($value);

                            if ($largerThan == "<" || $largerThan == "<=")
                                $q .= "{$k} $largerThan '$value' OR {$k} IS NULL";
                            else if ($largerThan == "=") {
                                if ($value == "NULL") {
                                    $q .= "{$k} = '' OR {$k} IS NULL";
                                }
                            } else if ($largerThan == "!=") {
                                if ($value == "NULL") {
                                    $q .= "{$k} != '' OR {$k} IS NOT NULL";
                                }
                            } else
                                $q .= "{$k} $largerThan '$value'";

                            $largerThan = "";
                            continue;
                        }

                        // "HELLO" => 'HELLO'
                        $value2 = ptyGetMiddleStr($value, '"', '"');
                        if ($value == "\"\"") {
                            $k = mre($k);
                            $q .= "({$k} = '' OR {$k} IS NULL) ";
                            // if ($value2 !== FALSE) {
                        } else if ($value2 != "") {
                            $k = mre($k);
                            $value2 = mre($value2);

                            $value2 = htmlentities($value2);
                            $q .= "{$k} = '$value2'";
                        } // ~HELLO~ => %HELLO%
                        else if ($first == '*' && $end == '*') {
                            $v = substr($value, 1, strlen($value) - 2);

                            $k = mre($k);
                            $v = mre($v);

                            $q .= "{$k} LIKE '%$v%' ";
                        } // ~HELLO => %HELLO
                        else if ($first == '*' && $end != '*') {
                            $v = substr($value, 1, strlen($value) - 1);

                            $k = mre($k);
                            $v = mre($v);

                            $q .= "{$k} LIKE '%$v' ";
                        } // HELLO~ => HELLO%
                        else if ($first != '*' && $end == '*') {
                            $v = substr($value, 0, strlen($value) - 1);

                            $k = mre($k);
                            $v = mre($v);

                            $q .= "{$k} LIKE '$v%' ";
                        } // =HELLO
                        else if ($first == '=') {
                            $v = trim(substr($value, 1, strlen($value) - 1));

                            $k = mre($k);
                            $v = mre($v);

                            $q .= "{$k} = '$v' ";
                        } // HELLO => 'HELLO'
                        else {
                            $k = mre($k);
                            $v = mre($value);
                            $q .= "{$k} = '$v' ";
                        }
                    }

                    debug($q, "q");
                    $q = str_replace("AND AND", "AND", $q);
                    $q = str_replace("OR AND", "OR", $q);
                    $this->addWhere($q);
                }
            }
        }

        // 키 sort
        if ($params['sc'] != "") {
            $sc = mreap($params['sc']);
            $sa = mre($params['sa']);

            if ($sc == "function" || $sc == "기능") return;
            $sa = strtolower($sa);
            if ($sa == "asc" || $sa == "desc") {
                $this->addOrderBy(sprintf("%s %s", $sc, $sa));
            }
        }
//        if ($params['page'] != "")
//            $this->setPageIndex((int)$params['page']);
    }

    /**
     * 로그인된 회원 정보를 WHERE 절에 포합합니다.
     * 비로그인 시에는 throw 가 발생합니다.
     *
     * @param model $model
     * @return ptyMysql
     * @throws ptyException
     * @author KwangHee Yoo <cpueblo@platyhouse.com>
     * @created 2024-04-09
     */
    public function addWhereSignUser(model $model = null): ptyMysql
    {
        if ($model) {
            $model->checkSignedInItem();
            $this->addWhereEqual("/* addWhereSignUser */ `{$model->_tableName}`.serviceUserId", $model->signedInItem->serviceUserId);
            $this->addWhereEqual("/* addWhereSignUser */ `{$model->_tableName}`.userId", $model->signedInItem->userId);
        } else {
            /** @var ptyuserItemModel $model */
            $model = $GLOBALS['platyFramework']->userItemsModel;
            pd($model, "model");
            $model->checkSignedInItem();
            $this->addWhereEqual("/* addWhereSignUser */ `" . $this->getTableName() . "`.serviceUserId", $model->signedInItem->serviceUserId);
            $this->addWhereEqual("/* addWhereSignUser */ `" . $this->getTableName() . "`.userId", $model->signedInItem->userId);
        }

        return $this;
    }

    public function getWhereSignUser(model $model): string
    {
        $model->checkSignedInItem();

        $sz = "(/* getWhereSignUser */ `{$model->_tableName}`.serviceUserId  = " . mrea($model->signedInItem->serviceUserId) . " AND `{$model->_tableName}`.userId = " . mrea($model->signedInItem->userId) . ")";
        return $sz;
    }

    // 결과값에서 한행 연관배열(이름으로)로 얻는다.

    /**
     *
     * @param $k
     * @param $v
     * @param $enabled
     * @param string|null $tableName
     * @return $this
     * @example
     * ```
     * addWhereEqual(["status" => BookingRecordStatus::CONFIRMED, "enabled" => true, "@dateTime > NOW()"]);
     * ```
     *
     */
    function addWhereEqual($k, $v, bool $enabled = true, string $tableName = null)
    {
        if (isset($tableName)) $tableName = "`$tableName`.";
        if (!isset($k) || $k == "")
            return $this;

        if (!$enabled)
            return $this;

        // @dateTime > NOW() 와 같은 형태
        if (is_int($k) && $v[0] == "@") {
            $where = substr($v, 1);
        } else if ($k[0] == "*" || $k[0] == "@") {
            if (is_null($v))
                $where = substr($k, 1);
            else
                $where = substr($k, 1) . " = $v";
        } else {
            /*
            if (is_null($v)) {
                $where = "{$tableName}`$k` IS NULL";
            } else {
            */
            $magicV = mre($v);
            if (strstr($k, "."))
                $where = "{$tableName}$k = '$magicV'";
            else
                $where = "{$tableName}`$k` = '$magicV'";
            // }
        }

        if ($this->_SQL_WHERE)
            $this->_SQL_WHERE .= " AND " . $where;
        else
            $this->_SQL_WHERE = " WHERE " . $where;

        array_push($this->_SQL_WHERE_ARRAY, $where);
        return $this;
    }

    public function addWhereEquals($keyValues = [])
    {
        foreach ($keyValues as $k => $v)
            $this->addWhereEqual($k, $v);

        return $this;
    }


    function addWhereLikes($its)
    {
        $wheres = [];
        foreach ($its as $k => $v) {
            $wheres[] = mreb($this->getTableName()) . "." . mreb($k) . " LIKE '%" . mre($v) . "%'";
        }

        $where = implode(" OR ", $wheres);
        array_push($this->_SQL_WHERE_ARRAY, $where);
        return $this;
    }


    function addWhereLike($k, $v)
    {
        // $this->getTableName() . "`.`" . mre($k) . "`";
        $where = mreb($this->getTableName()) . "." . mreb($k) . " LIKE '%" . mre($v) . "%'";

        if ($this->_SQL_WHERE)
            $this->_SQL_WHERE .= " AND " . $where;
        else
            $this->_SQL_WHERE = " WHERE " . $where;

        array_push($this->_SQL_WHERE_ARRAY, $where);
        return $this;
    }

    function setDebug($debug)
    {
        pdv($debug, "setDebug");
        if ($_REQUEST['debug'] == "db" && $debug == 0)
            return;

        $this->debug = $debug;
    }

    // for code ignitor 3
    function result($sql_q)
    {
        // return $this->sql_list($sql_q);

        $i = 0;
        $sql_list = [];
        while ($sql_r = $this->sql_fetch_array($sql_q)) {
            foreach ($sql_r as $k => $v) {
//                \t  에 의한 tttt 노출 오류
//                if (!is_null($v) && !get_magic_quotes_gpc()) {
//                    $v = stripslashes($v);
//                }
                $sql_r[$k] = $v;
            }
            $sql_list[$i] = $sql_r;
            $i++;
        }

        return $sql_list;

    }

    function xxsql_fetch_array($result)
    {
        $row = @mysqli_fetch_assoc($result);
        return $row;
    }

    function lst($query = "")
    {
        if ($query)
            return $this->sql_list($query);
        else
            return $this->sql_list($this->getQuery());
    }

    /*
     * function query($sql, $debug)
     * {
     * $this->sql_query($query, $debug);
     * }
     */

    function sql_list2($sql = "")
    {
        $sql_list = array();

        $sql_q = $this->sql_query($sql);
        $i = 0;
        while ($sql_r = mysqli_fetch_row($sql_q)) {
            // $sql_list[$i]= $sql_r;
            array_push($sql_list, $sql_r[0]);
            $i++;
        }

        return $sql_list;
    }

    function microtime_float()
    {
        list ($usec, $sec) = explode(" ", microtime());
        return ((float)$usec + (float)$sec) * 1000;
    }

    function clear()
    {
        $this->setCacheEnabled(0);
        $this->initSelect();
        $this->initHaving();
        $this->initWhere();
        $this->InitOrderBy();
        $this->InitLimit(0);
        $this->initSet();
        $this->InitGroupBy();
        $this->initJoin();
        $this->_SQL_FROM_ARRAY = array();
        return $this;
    }

    function setCacheEnabled($interval = 600 /* 60 sec * 10 min*/)
    {
        if ($interval) {
            pdv($interval, "sql cache enabled = $interval");
            $this->cacheEnabled = true;
            $this->cacheEnabledInterval = $interval;
            $this->activeCacheFileName = "";
        } else {
            pdv($interval, "sql cache disabled");
            $this->cacheEnabled = false;
            $this->cacheEnabledInterval = 600;
            $this->activeCacheFileName = "";
        }
    }

    function initSelect()
    {
        $this->_SQL_SELECT = "";
        $this->_SQL_SELECT_ARRAY = array();
    }

    // 한개의 데이터만을 얻을 경우 0=>값1, 1=>값2 식으로 데이터를 리턴

    function initSet()
    {
        $this->_SQL_SET = "";
        $this->_SQL_SET_ARRAY = array();
    }

    // 한개의 데이터만을 얻을 경우 0=>값1, 1=>값2 식으로 데이터를 리턴

    function initJoin()
    {
        $this->_SQL_JOIN_ARRAY = array();
    }

    // 첫번째 col 을 배열의 값으로 잡고 나머지를 계산

    function sql_list3($sql = "")
    {
        $sql_list = array();

        $sql_q = $this->sql_query($sql);
        $i = 0;
        while ($sql_r = mysqli_fetch_row($sql_q)) {
            // $sql_list[$i]= $sql_r;
            // array_push($sql_list, $sql_r[0]);
            $sql_list[$sql_r[0]] = $sql_r[0];
            $i++;
        }

        return $sql_list;
    }

    /*
    lst_cols
    : 여러개의 컬럼을 배열 형태로 리턴해줌

    in)
        $sql = "
        SELECT
            구매경로,
            (WEEK(date_add(일자, interval +5 day), 0) - WEEK(DATE_FORMAT(일자, '%Y-%m-1')) + 1) as weekid,
            매장명,
            SUM(Count) as cnt,
            DAYNAME(일자) as WeekDay
        FROM t_visitcount
        WHERE 	(Year(일자) = 2015 AND Month(일자) = 04)
        GROUP BY 매장명, DAYOFMONTH(일자)
        ORDER BY 구매경로, weekid, 매장명
        ";
        $db->lst_cols ($sql, 3);		// 3 은 cnt, WeekDay 를 분리하도록 요청
    result)
        Array
        (
            [A-초객] => Array
                (
                    [2] => Array
                        (
                            [레몬점] => Array
                                (
                                    [cnt] => 1
                                    [WeekDay] => Friday
                                )

                            [신방점] => Array
                                (
                                    [cnt] => 11
                                    [WeekDay] => Thursday
                                )

                            [쌍용점] => Array
                                (
                                    [cnt] => 1
                                    [WeekDay] => Wednesday
                                )
                        )
                )

            )
    */

    function sql_list4($sql = "", $debug = false)
    {
        if ($debug)
            echo "<TEXTAREA ROWS=5 COLS=100>sql_list debug. \n $sql</TEXTAREA>";

        $result = $this->sql_query($sql);
        $ret = array();

        while ($row = mysqli_fetch_array($result)) {
            $s = "$" . "ret";
            for ($col = 0; $col < (count($row) - 2) / 2; $col++) {
                $s .= sprintf("[\"%s\"]", $row[$col]);
            }

            $s .= sprintf("=\"" . str_replace("%", "%%", $row[$col]) . "\";");
            eval($s);
        }
        return $ret;
    }

    function sql_list5($sql)
    {
        if (!$sql)
            $sql = $this->getQuery();

        $result = $this->sql_query($sql);
        $ret = array();

        while ($row = mysqli_fetch_assoc($result)) {
            $colIndex = 0;
            $a = array();

            foreach ($row as $k => $v) {
                $a[$k] = $v;
                if ($colIndex == 0) {
                    $key = $v;
                }
                $colIndex++;
            }
            $ret[$key] = $a;
        }
        return $ret;

    }

    /**
     * select date(regDateTime), count(*) as cnt, 5 as id 형태를 아래 형태로 변환합니다.
     * $input = [
     * "2025-06-02" => ["web" => 39, "Android" => 3, "iOS" => 3],
     * "2025-06-03" => ["web" => 38, "Android" => 4, "iOS" => 2],
     * // ...
     * ];
     * @param $sql
     * @return array
     */
    function sql_list_dates($sql)
    {
        if (!$sql)
            $sql = $this->getQuery();

        $result = $this->sql_query($sql);
        $ret = array();

        while ($row = mysqli_fetch_assoc($result)) {
            $colIndex = 0;
            $a = array();

            foreach ($row as $k => $v) {
                if ($colIndex == 0) {
                    $key = $v;
                    $colIndex++;
                    continue;
                }
                $a[$k] = $v;
                $colIndex++;
            }
            $ret[$key] = $a;
        }
        return $ret;

    }

    function lst_cols($sql, $depth = 999)
    {
        $result = $this->sql_query($sql);
        $ret = array();

        $depthCount = 0;
        while ($row = mysqli_fetch_assoc($result)) {
            $s = "";
            $k = '$ret';

            $cols = array_keys($row);
            $values = array_values($row);
            if ($depth > count($cols)) {
                $depth = count($cols) - 1;
            }

            for ($col = 0; $col < $depth; $col++) {
                $k .= sprintf("['%s']", addslashes($values[$col]));
            }

            for ($col = $depth; $col < count($cols); $col++) {
                $s .= $k . sprintf("['%s'] = stripslashes(\"%s\");\n", $cols[$col], addslashes($values[$col]));
            }


            // echo $s; exit;
            eval($s);
        }
        return $ret;
    }

    public function reconnect()
    {
        $this->connect($this->_dbHost, $this->_dbUserName, $this->_dbPassword, $this->_dbName, false);
    }

    public function addWhereIn(string $col, array $ids)
    {
        pd($ids, "addWhereIn.ids");
        if (is_array($ids))
            $this->addWhere("/* addWhereIn */" . $col . " " . ptyInImplodeApostrophe($ids));
        return $this;
    }

    public function getWhereIn(string $col, array $ids)
    {
        $out = "";
        if (is_array($ids))
            $out = "/* getWhereIn */" . $col . " " . ptyInImplodeApostrophe($ids);
        return $out;
    }


    /**
     * MySQL의 WHERE 절에 사용될 날짜 및 시간 조건을 생성합니다.
     *
     * @param string $colName 컬럼 이름
     * @param string $q 날짜 또는 시간 값
     * @return string WHERE 절에 사용될 조건 문자열
     *
     * @author KwangHee Yoo <cpueblo@platyhouse.com>
     * @created 2024-04-12
     */
    function getWhereDateTime(string $colName, string $q)
    {
        // pdf($colName, "getWhereDateTime.colName");
        // pdf($q, "getWhereDateTime.q");

        // $colName = "$tableName`.`$colName";
        // $colName = $colName;
        // 입력값을 SQL 인젝션으로부터 보호
        $q = mre(trim($q));

        // 큰따옴표를 제거하여 " 가 있거나 없거나 동일하게 처리됨
        $q = trim($q, '"');

        // 범위 검색 형태인지 확인 ("2024-01~2024-02", "2024-01~", "~2024-01")
        if (strpos($q, '~') !== false) {
            $range = explode('~', $q);
            $start = trim($range[0]);
            $end = isset($range[1]) ? trim($range[1]) : '';

            // 시작 날짜만 있는 경우 (ex. "2024-01~")
            if (!empty($start) && empty($end)) {
                if (preg_match('/^\d{4}-\d{2}$/', $start)) {
                    // 연-월만 주어진 경우, 해당 월의 첫날부터 검색
                    return "/* getWhereDateTime A */ $colName >= '$start-01 00:00:00'";
                }
                if (preg_match('/^\d{4}$/', $start)) {
                    // 연도만 주어진 경우, 해당 연도의 첫날부터 검색
                    return "/* getWhereDateTime B */ $colName >= '$start-01-01 00:00:00'";
                }
                if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start)) {
                    // 시작 날짜가 연-월-일 형식인 경우
                    return "/* getWhereDateTime C */ $colName >= '$start 00:00:00'";
                }
            }

            // 종료 날짜만 있는 경우 (ex. "~2024-01")
            if (empty($start) && !empty($end)) {
                if (preg_match('/^\d{4}-\d{2}$/', $end)) {
                    // 연-월만 주어진 경우, 해당 월의 마지막 날까지 검색
                    $lastDay = (new \DateTime("$end-01"))->format('Y-m-t');
                    return "/* getWhereDateTime D */ $colName <= '$lastDay 23:59:59'";
                }
                if (preg_match('/^\d{4}$/', $end)) {
                    // 연도만 주어진 경우, 해당 연도의 마지막 날까지 검색
                    return "/* getWhereDateTime E */ $colName <= '$end-12-31 23:59:59'";
                }
                if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) {
                    // 종료 날짜가 연-월-일 형식인 경우
                    return "/* getWhereDateTime F */ $colName <= '$end 23:59:59'";
                }
            }

            // 시작과 종료 날짜가 모두 있는 경우 (ex. "2024-01~2024-02")
            if (!empty($start) && !empty($end)) {

                // 1900-01~1900-01 형태인 경우 => 1900-01-01 ~ 1900-01-31 일을 검색
                // if (preg_match('/^\d{4}-\d{2}$/', $start) && preg_match('/^\d{4}-\d{2}$/', $end)) {
                if (ptyCheckPattern($start, "nnnn-nn") && ptyCheckPattern($end, "nnnn-nn")) {
                    // 시작과 종료가 연-월인 경우
                    $lastDay = (new \DateTime("$end-01"))->format('Y-m-t');
                    return "/* getWhereDateTime G */ $colName >= '$start-01 00:00:00' AND $colName <= '$lastDay 23:59:59'";
                }

                // 1900-01-01~1900-01-01 형태인 경우
                // else if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) {
                else if (ptyCheckPattern($start, "nnnn-nn-nn") && ptyCheckPattern($end, "nnnn-nn-nn")) {
                    // 시작과 종료가 연-월-일인 경우
                    return "/* getWhereDateTime H */ $colName >= '$start 00:00:00' AND $colName <= '$end 23:59:59'";
                } else if (ptyCheckPattern($start, "nnnn-nn-nn nn:nn:nn") && ptyCheckPattern($end, "nnnn-nn-nn nn:nn:nn")) {
                    return "/* getWhereDateTime H */ $colName >= '$start' AND $colName <= '$end'";
                } else {
                    return "/* getWhereDateTime H */ $colName >= '$start 00:00:00' AND $colName <= '$end 23:59:59'";
                }

            }
        }

        // 단순 숫자일 경우 (15 등)
        if (is_numeric($q)) {
            return "/* getWhereDateTime I */ YEAR($colName) = $q OR MONTH($colName) = $q OR DAY($colName) = $q OR HOUR($colName) = $q OR MINUTE($colName) = $q OR SECOND($colName) = $q";
        }

        // 시간(HH:MM:SS) 형태인지 확인
        if (preg_match('/^\d{2}(:\d{2}(:\d{2})?)?$/', $q)) {
            $parts = explode(':', $q);
            if (count($parts) === 3) {
                return "/* getWhereDateTime J */ HOUR($colName) = {$parts[0]} AND MINUTE($colName) = {$parts[1]} AND SECOND($colName) = {$parts[2]}";
            } elseif (count($parts) === 2) {
                return "/* getWhereDateTime K */ HOUR($colName) = {$parts[0]} AND MINUTE($colName) = {$parts[1]}";
            }
        }

        // 날짜와 시간 형태인지 확인하는 정규식 (연-월 또는 연-월-일 또는 연-월-일 시:분:초까지 지원)
        // if (preg_match('/^\d{4}(-\d{2}(-\d{2}(\s\d{2}(:\d{2}(:\d{2})?)?)?)?)?$/', $q)) {
        if (true) {
            // 정확한 DATETIME까지 입력된 경우
            if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/', $q)) {
                return "/* getWhereDateTime L */ $colName = '$q'";
            }
            // 날짜 + 시간(시:분)까지 입력된 경우
            if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}$/', $q)) {
                return "/* getWhereDateTime M */ $colName >= '$q:00' AND $colName <= '$q:59'";
            }
            // 날짜 + 시간(시)까지 입력된 경우
            if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}$/', $q)) {
                return "/* getWhereDateTime N */ $colName >= '$q:00:00' AND $colName <= '$q:59:59'";
            }
            // 날짜까지 입력된 경우
            if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $q)) {
                return "/* getWhereDateTime O */ $colName >= '$q 00:00:00' AND $colName <= '$q 23:59:59'";
            }
            // 년-월이나 불완전한 년-월-까지 입력된 경우
            if (preg_match('/^\d{4}-\d{2}(-)?$/', $q)) {
                // 불완전한 형식 `2024-11-`을 처리하여 `2024-11`로 간주
                $q = rtrim($q, '-');
                $lastDay = (new \DateTime("$q-01"))->format('Y-m-t'); // 'Y-m-t'는 해당 월의 마지막 날짜
                return "/* getWhereDateTime P */ $colName >= '$q-01 00:00:00' AND $colName <= '$lastDay 23:59:59'";
            }
            // 연도만 입력된 경우
            if (preg_match('/^\d{4}$/', $q)) {
                return "/* getWhereDateTime Q */ $colName >= '$q-01-01 00:00:00' AND $colName <= '$q-12-31 23:59:59'";
            }
        }

        ptynotifitjsApiModel::addError("날짜 검색 기준이 올바르지 않아 검색 결과에 포함되지 않았습니다.");
        // ptynotifitjsApiModel::build()->

        // 기본 반환값 (매칭되지 않을 경우)
        return "/* getWhereDateTime */ 1 = 1"; // 항상 true인 조건문
    }

    /**
     * 통계에서 사용됨
     *
     * @param $q
     * @param $dateColName
     * @param $startDate
     * @param $endDate
     * @return array
     */
    function getAreaChartQueryWithDateRange($q, $dateColName = "date", $startDate = "", $endDate = "")
    {
        global $platyFramework;

        if ($startDate == "") {
            $startDate = date("Y-m-d", strtotime("-14 day"));
        }
        if ($endDate == "") {
            $endDate = date("Y-m-d");
        }

        if (strlen($startDate) == "10") $startDate .= " 00:00:00";
        if (strlen($endDate) == "10") $endDate .= " 23:59:59";

        $dateRangeWhere = " `$dateColName` BETWEEN " . mrea($startDate) . " AND " . mrea($endDate);
        $q = str_replace("{dateRangeWhere}", $dateRangeWhere, $q);
        $lst = $platyFramework->db->sql_list4($q);

        // fill
        $output = [];
        $current = strtotime($startDate);
        $end = strtotime($endDate);
        while ($current <= $end) {
            $date = date("Y-m-d", $current);
            $output["$date"] = [];
            $current = strtotime("+1 day", $current);
        }

        foreach ($lst as $k => $v) {
            $date = $k;
            $output[$date] = $v;
        }

        // 가장 많은 순으로 정렬
        foreach ($output as $date => &$platforms) {
            // pdf($platforms);

            if (is_array($platforms)) {
                arsort($platforms); // 값 기준 내림차순 정렬
            }
        }
        unset($platforms); // 참조 해제

        return $output;
    }

    function getPieChartQueryWithDateRange($q, $dateColName = "date", $startDate = "", $endDate = "")
    {
        global $platyFramework;

        if ($startDate == "") {
            $startDate = date("Y-m-d 00:00:00", strtotime("-14 day"));
        }
        if ($endDate == "") {
            $endDate = date("Y-m-d 23:59:00");
        }

        if (strlen($startDate) == "10") $startDate .= " 00:00:00";
        if (strlen($endDate) == "10") $endDate .= " 23:59:59";

        $dateRangeWhere = " `$dateColName` BETWEEN " . mrea($startDate) . " AND " . mrea($endDate);
        $q = str_replace("{dateRangeWhere}", $dateRangeWhere, $q);
        $lst = $platyFramework->db->sql_list4($q);

        arsort($lst);
        return $lst;
    }

    /**
     * 초성 및 알파벳 시작을 검색
     *
     * @param string $colName
     * @param string $abc
     * @return void
     */
    public function addWhereAbc(string $colName, string $abc)
    {
        switch ($abc) {
            case "ㄱ":
                $where = "(`{$colName}` RLIKE '^(ㄱ|ㄲ)' OR (`{$colName}` >= '가' AND `{$colName}` < '나'))";
                break;
            case "ㄴ":
                $where = "(`{$colName}` RLIKE '^ㄴ' OR (`{$colName}` >= '나' AND `{$colName}` < '다'))";
                break;
            case "ㄷ":
                $where = "(`{$colName}` RLIKE '^(ㄷ|ㄸ)' OR (`{$colName}` >= '다' AND `{$colName}` < '라'))";
                break;
            case "ㄹ":
                $where = "(`{$colName}` RLIKE '^ㄹ' OR (`{$colName}` >= '라' AND `{$colName}` < '마'))";
                break;
            case "ㅁ":
                $where = "(`{$colName}` RLIKE '^ㅁ' OR (`{$colName}` >= '마' AND `{$colName}` < '바'))";
                break;
            case "ㅂ":
                $where = "(`{$colName}` RLIKE '^ㅂ' OR (`{$colName}` >= '바' AND `{$colName}` < '사'))";
                break;
            case "ㅅ":
                $where = "(`{$colName}` RLIKE '^(ㅅ|ㅆ)' OR (`{$colName}` >= '사' AND `{$colName}` < '아'))";
                break;
            case "ㅇ":
                $where = "(`{$colName}` RLIKE '^ㅇ' OR (`{$colName}` >= '아' AND `{$colName}` < '자'))";
                break;
            case "ㅈ":
                $where = "(`{$colName}` RLIKE '^(ㅈ|ㅉ)' OR (`{$colName}` >= '자' AND `{$colName}` < '차'))";
                break;
            case "ㅊ":
                $where = "(`{$colName}` RLIKE '^ㅊ' OR (`{$colName}` >= '차' AND `{$colName}` < '카'))";
                break;
            case "ㅋ":
                $where = "(`{$colName}` RLIKE '^ㅋ' OR (`{$colName}` >= '카' AND `{$colName}` < '타'))";
                break;
            case "ㅌ":
                $where = "(`{$colName}` RLIKE '^ㅌ' OR (`{$colName}` >= '타' AND `{$colName}` < '파'))";
                break;
            case "ㅍ":
                $where = "(`{$colName}` RLIKE '^ㅍ' OR (`{$colName}` >= '파' AND `{$colName}` < '하'))";
                break;
            case "ㅎ":
                $where = "(`{$colName}` RLIKE '^ㅎ' OR (`{$colName}` >= '하'))";
                break;

            // A ~ Z 영어 알파벳
            case "A":
            case "B":
            case "C":
            case "D":
            case "E":
            case "F":
            case "G":
            case "H":
            case "I":
            case "J":
            case "K":
            case "L":
            case "M":
            case "N":
            case "O":
            case "P":
            case "Q":
            case "R":
            case "S":
            case "T":
            case "U":
            case "V":
            case "W":
            case "X":
            case "Y":
            case "Z":
                $where = "`{$colName}` RLIKE '^{$abc}'";
                break;

            default:
                $where = ""; // 또는 전체 검색
                break;
        }

        $this->addWhere($where);

        return $this;
        /*
                WHERE (`".$colName."` RLIKE '^(ㄱ|ㄲ)' OR ( `".$colName."` >= '가' AND `".$colName."` < '나' ))
        WHERE (`".$colName."` RLIKE '^ㄴ' OR ( `".$colName."` >= '나' AND `".$colName."` < '다' ))
        WHERE (`".$colName."` RLIKE '^(ㄷ|ㄸ)' OR ( `".$colName."` >= '다' AND `".$colName."` < '라' ))
        WHERE (`".$colName."` RLIKE '^ㄹ' OR ( `".$colName."` >= '라' AND `".$colName."` < '마' ))
        WHERE (`".$colName."` RLIKE '^ㅁ' OR ( `".$colName."` >= '마' AND `".$colName."` < '바' ))
        WHERE (`".$colName."` RLIKE '^ㅂ' OR ( `".$colName."` >= '바' AND `".$colName."` < '사' ))
        WHERE (`".$colName."` RLIKE '^(ㅅ|ㅆ)' OR ( `".$colName."` >= '사' AND `".$colName."` < '아' ))
        WHERE (`".$colName."` RLIKE '^ㅇ' OR ( `".$colName."` >= '아' AND `".$colName."` < '자' ))
        WHERE (`".$colName."` RLIKE '^(ㅈ|ㅉ)' OR ( `".$colName."` >= '자' AND `".$colName."` < '차' ))
        WHERE (`".$colName."` RLIKE '^ㅊ' OR ( `".$colName."` >= '차' AND `".$colName."` < '카' ))
        WHERE (`".$colName."` RLIKE '^ㅋ' OR ( `".$colName."` >= '카' AND `".$colName."` < '타' ))
        WHERE (`".$colName."` RLIKE '^ㅌ' OR ( `".$colName."` >= '타' AND `".$colName."` < '파' ))
        WHERE (`".$colName."` RLIKE '^ㅍ' OR ( `".$colName."` >= '파' AND `".$colName."` < '하' ))
        WHERE (`".$colName."` RLIKE '^ㅎ' OR ( `".$colName."` >= '하'))
        */

    }

    public function debug()
    {
        pdf($this->_SQL_SELECT_ARRAY, "_SQL_SELECT_ARRAY");
        pdf($this->_SQL_WHERE_ARRAY, "_SQL_WHERE_ARRAY");
        pdf($this->_SQL_WHERE_OR_ARRAY, "_SQL_WHERE_OR_ARRAY");
    }

    public function setSlowQueryDetect(bool $value = true)
    {
        $this->willDetectSlowQuery = $value;
    }

    public function getWhereDateRange(string $date, string &$startDate = "", string &$endDate = "")
    {
        if ($startDate == "") {
            $startDate = date("Y-m-d", strtotime("-14 day"));
        }
        if ($endDate == "") {
            $endDate = date("Y-m-d");
        }

        if (strlen($startDate) == "10") $startDate .= " 00:00:00";
        if (strlen($endDate) == "10") $endDate .= " 23:59:59";

        return "`$date` >= " . mrea($startDate) . " AND `$date` <= " . mrea($endDate);
    }
}

function cpbDBErrorHandler($errno, $errstr, $errfile, $errline)
{
    if (E_RECOVERABLE_ERROR === $errno) {
        echo "'catched' catchable fatal error\n";
        return true;
    }
    return false;
}

?>
