<file_sep><?php

    /**
     * TF 용 커스텀 컨트롤러
     *
     */

    namespace controllers\internal;

    use Slim\Http\Request;
    use Slim\Http\Response;
    use Psr\Container\ContainerInterface;

    use libraries\{
        log\LogMessage,
        constant\CommonConst,
        constant\ErrorConst,
        constant\TFConst,
        util\CommonUtil,
        util\RedisUtil,
        util\AwsUtil,
        http\Http
    };

    class TravelForest extends SynctreeInternal
    {

        private $guzzleOpt = [
            'verify'          => false,
            'timeout'         => CommonConst::HTTP_RESPONSE_TIMEOUT,
            'connect_timeout' => CommonConst::HTTP_CONNECT_TIMEOUT,
            'headers'         => [
                'User-Agent' => 'Synctree/2.1 - ' . APP_ENV
            ]
        ];

        private $noWait = true;
        private $maxTryCount = 10;
        private $redisSession = TFConst::REDIS_TF_SESSION;
        private $recipients = [];

        public function __construct(ContainerInterface $ci)
        {
            parent::__construct($ci);

            $this->recipients[] = '<EMAIL>';
            if (APP_ENV === APP_ENV_STAGING || APP_ENV === APP_ENV_PRODUCTION) {
                $this->recipients[] = '<EMAIL>';
            }
        }

        public function template(Request $request, Response $response)
        {
            $results = $this->jsonResult;

            try {


            } catch (\Exception $ex) {

                $results = [
                    'result' => false,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * 모상품 목록 가지고 오기
         *
         * @param Request  $request
         * @param Response $response
         *
         * @return Response
         */
        public function getProducts(Request $request, Response $response)
        {
            $products = null;

            try {

                if (false === ($products = RedisUtil::getData($this->redis, TFConst::REDIS_TF_PRODUCTS_REDIS_KEY, $this->redisSession))) {
                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                }

                $results = $products;

            } catch (\Exception $ex) {
                $results = [
                    'result' => ErrorConst::FAIL_STRING,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);
        }


        /**
         * 자상품의 번호로 모상품의 IDX를 리턴
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         */
        public function getProductIdx(Request $request, Response $response, $args)
        {
            $productIdx = null;

            try {

                $params = $args;

                if (false === CommonUtil::validateParams($params, ['productTypeKey'])) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $redisKey = TFConst::REDIS_TF_SUB_PRODUCT_REDIS_KEY . '_' . $params['productTypeKey'];
                if (false === ($product = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {
                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                }

                $productIdx = $product['product_idx'];

                $results = [
                    'productKey' => $productIdx
                ];

            } catch (\Exception $ex) {
                $results = [
                    'result' => ErrorConst::FAIL_STRING,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);


        }

        /**
         * 모상품의 상세정보 리턴
         *
         * @param Request  $request
         * @param Response $response
         *
         * @return Response
         */
        public function getProduct(Request $request, Response $response)
        {
            $productDetail = [];

            try {

                $params = $request->getAttribute('params');

                if (false === CommonUtil::validateParams($params, ['product'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $product = $params['product'];
                $dateSearchFlag = false;

                if ( ! is_array($product) || empty($product) ) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $tourOptions = [];
                $priceArr = [];
                $priceAdultArr = [];
                $priceChildArr = [];
                $paxArr = [];
                $saleAvailableTour = [];
                $pDate = [];

                if (isset($params['fromDate']) && isset($params['toDate']) && ! empty($params['fromDate']) && ! empty($params['toDate'])) {

                    $dateSearchFlag = true;

                    $fromDate = date('Y-m-d', strtotime($params['fromDate']));
                    $toDate = date('Y-m-d', strtotime($params['toDate']));

                    $fromDateTime = new \DateTime($fromDate);
                    $toDateTime = new \DateTime($toDate);

                    $dateDiff = date_diff($fromDateTime, $toDateTime);
                    $dateDiff = $dateDiff->days + 1;

                    if ($dateDiff > 366) {
                        throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                    }

                    $period = new \DatePeriod($fromDateTime, new \DateInterval('P1D'), $toDateTime->modify('+1 day'));

                    foreach ($period as $key => $value) {
                        $pDate[] = $value->format('Y-m-d');
                    }

                }

                switch (true) {

                    // 자상품 상세
                    case (isset($params['productTypeKey']) && ! empty($params['productTypeKey']) && $params['productTypeKey'] !== 'null') :

                        if ( ! isset($params['productTypes']) || empty($params['productTypes'])) {
                            break;
                        }

                        $voucherUse = '';
                        $refundPolicy = null;
                        $isNoneRefund = false; // 환불불가
                        $tourItemFlag = false;
                        $transferItemFlag = false;

                        // 투어 상품과 픽업 상품이 나뉨
                        switch (true) {
                            case (isset($product['tour_item']) && is_array($product['tour_item'])) :
                                $tourItemFlag = true;
                                break;
                            case (isset($product['tour_items']) && is_array($product['tour_items'])) :
                                if (false === ($tourIdx = array_search($params['productTypeKey'],
                                        array_column($product['tour_items'], 'tour_item_idx')))
                                ) {
                                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                                }
                                $tourItemFlag = true;
                                $product['tour_item'] = $product['tour_items'][$tourIdx];
                                break;
                            case (isset($product['transfer_item']) && is_array($product['transfer_item'])) :
                                $transferItemFlag = true;
                                break;
                            case (isset($product['transfer_items']) && is_array($product['transfer_items'])) :
                                if (false === ($tourIdx = array_search($params['productTypeKey'],
                                        array_column($product['transfer_items'], 'transfer_item_idx')))
                                ) {
                                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                                }
                                $transferItemFlag = true;
                                $product['transfer_item'] = $product['transfer_items'][$tourIdx];
                                break;
                        }

                        if (true === $tourItemFlag) { // 투어 아이템일 경우

                            $tourItem = $product['tour_item'];

                            $productDetail['productTypeKey'] = $tourItem['tour_item_idx'];

                            $productDetail['title'] = $tourItem['tour_ko'];
                            $productDetail['title'] = $this->_replaceSpecialChar($productDetail['title']);

                            $productDetail['durationDays'] = (int)$tourItem['duration_day'] ?? 0;
                            $productDetail['durationHours'] = (int)$tourItem['duration_hour'] ?? 0;
                            $productDetail['durationMins'] = (int)$tourItem['duration_minute'] ?? 0;
                            $productDetail['minAdultAge'] = 0;
                            $productDetail['maxAdultAge'] = 99;
                            $productDetail['hasChildPrice'] = false;
                            $productDetail['minChildAge'] = 0;

                            if (isset($tourItem['tour_suppliers'])) {
                                foreach ($tourItem['tour_suppliers'] as $tsp) {

                                    // 바우처 정보
                                    //$voucherUse .= ($tsp['note_content'] ?? '');
                                    $voucherUse .= ($tsp['voucher_content'] ?? '');

                                    // 환불규정
                                    $refundPolicy = $tsp['cancel_content'] ?? '';

                                    // 환불 불가여부
                                    $isNoneRefund = (isset($tsp['cancel_type_cd']) && $tsp['cancel_type_cd'] == 'refund') ? false : true;

                                    // 최대/최소 가격 , 최소/최대 여행자수
                                    foreach ($tsp['tour_seasons'] as $ts) {

                                        // 날짜 검색이면
                                        if (true === $dateSearchFlag) {

                                            if ( ! empty($pDate)) {

                                                $weeks = explode( ',', $ts['season_week']);
                                                $tourDateFrom = date('Y-m-d', strtotime($ts['season_date_from']));
                                                $tourDateTo = date('Y-m-d', strtotime($ts['season_date_to']));
                                                $tourSaleDateTo = date('Y-m-d',
                                                    strtotime($ts['season_date_to'] . ' -' . $ts['sale_day_to'] . ' days'));

                                                foreach ($pDate as $sDate) {

                                                    $tmpDate = date('Y-m-d', strtotime($sDate));
                                                    $tmpWeek = date('w', strtotime($sDate));
                                                    if ($tmpDate >= $tourDateFrom && $tmpDate <= $tourDateTo && in_array($tmpWeek, $weeks)) {
                                                        $saleAvailableTour[] = [
                                                            'tour_season_idx' => $ts['tour_season_idx'],
                                                            'from_to'         => $tmpDate,
                                                        ];
                                                    }
                                                }
                                            }
                                        }

                                        // 가격/여행객 수
                                        foreach ($ts['tour_prices'] as $tps) {

                                            switch ($tps['sale_unit_ko']) {
                                                case '성인' :
                                                    $productDetail['minAdultAge'] = (int)$tps['customer_age_from'];
                                                    $productDetail['maxAdultAge'] = (int)(($tps['customer_age_to'] > 0) ? $tps['customer_age_to'] : 99) ;
                                                    $priceArr[] = $tps['sale_price'] ?? 0;
                                                    $priceAdultArr[] = $tps['sale_price'] ?? 0;
                                                    break;
                                                case '아동' :
                                                    $productDetail['hasChildPrice'] = true;
                                                    $productDetail['minChildAge'] = (int)$tps['customer_age_from'];
                                                    $priceChildArr[] = $tps['sale_price'] ?? 0;
                                                    break;
                                                case '유아' :
                                                    $productDetail['allowInfants'] = true;
                                                    $productDetail['maxInfantAge'] = (int)$tps['customer_age_to'];
                                                    $priceChildArr[] = $tps['sale_price'] ?? 0;
                                                    break;
                                            }

                                            if (isset($tps['customer_count_from']) && !empty($tps['customer_count_from'])) {
                                                $paxArr[] = (int)$tps['customer_count_from'];
                                            }

                                            if (isset($tps['customer_count_to'])) {
                                                $paxArr[] = (int)$tps['customer_count_to'];
                                            }
                                        }

                                    }

                                    // 자상품 옵션
                                    if (isset($tsp['tour_options']) && ! empty($tsp['tour_options'])) {
                                        foreach ($tsp['tour_options'] as $to) {

                                            if ( ! isset($to['tour_fields']) || empty($to['tour_fields'])) {
                                                continue;
                                            }

                                            foreach ($to['tour_fields'] as $tf) {

                                                $tmpOpt = [];
                                                $tmpOpt['id'] = $tf['field_id'];
                                                $tmpOpt['target'] = 1;
                                                $tmpOpt['name'] = $tf['field_label'];
                                                $tmpOpt['type'] = (isset($tf['field_items']) && ! empty($tf['field_items'])) ? 1 : 4;
                                                $tmpOpt['required'] = (isset($tf['field_items']) && ! empty($tf['field_items'])) ? true : false;
                                                $tmpOpt['price'] = 0;

                                                if (isset($tf['field_items']) && ! empty($tf['field_items']) && is_array($tf['field_items'])) {

                                                    $tmpOpt['item'] = [];
                                                    foreach ($tf['field_items'] as $fl) {
                                                        $tmpOpt['item'][] = [
                                                            'label' => $fl['item_text'],
                                                            'value' => $fl['item_value'],
                                                        ];
                                                    }
                                                }

                                                $tourOptions[] = $tmpOpt;
                                            }
                                        }
                                    }
                                }
                            }

                        } elseif (true === $transferItemFlag) {

                            $tourItem = $product['transfer_item'];

                            $productDetail['productTypeKey'] = $tourItem['transfer_item_idx'];

                            $productDetail['title'] = $tourItem['transfer_ko'];
                            $productDetail['title'] = $this->_replaceSpecialChar($productDetail['title']);

                            $productDetail['durationDays'] = 0;
                            $productDetail['durationHours'] = 0;
                            $productDetail['durationMins'] = 0;

                            $productDetail['minAdultAge'] = 0;
                            $productDetail['maxAdultAge'] = 99;
                            $productDetail['hasChildPrice'] = false;
                            $productDetail['minChildAge'] = 0;

                            $priceChildArr = [0];

                            if (isset($tourItem['transfer_suppliers'])) {
                                foreach ($tourItem['transfer_suppliers'] as $tsp) {

                                    // 바우처 정보
                                    $voucherUse .= ($tsp['voucher_content'] ?? '');


                                    // 환불규정
                                    $refundPolicy = $tsp['cancel_content'] ?? '';

                                    // 환불 불가여부
                                    $isNoneRefund = (isset($tsp['cancel_type_cd']) && $tsp['cancel_type_cd'] == 'refund') ? false : true;

                                    // 최대/최소 가격
                                    foreach ($tsp['transfer_seasons'] as $ts) {

                                        // 날짜 검색이면
                                        if (true === $dateSearchFlag) {
                                            if ( ! empty($pDate)) {
                                                $tourDateFrom = date('Y-m-d', strtotime($ts['season_date_from']));
                                                $tourDateTo = date('Y-m-d', strtotime($ts['season_date_to']));
                                                $tourSaleDateTo = date('Y-m-d',
                                                    strtotime($ts['season_date_to'] . ' -' . $ts['sale_day_to'] . ' days'));

                                                foreach ($pDate as $sDate) {
                                                    $tmpDate = date('Y-m-d', strtotime($sDate));
                                                    if ($tmpDate >= $tourDateFrom && $tmpDate <= $tourDateTo) {
                                                        $saleAvailableTour[] = [
                                                            'from_to' => $tmpDate,
                                                        ];

                                                        foreach ($ts['transfer_prices'] as $tps) {
                                                            $priceArr[] = $priceAdultArr[] = ($tps['sale_price'] ?? 0);

                                                            // 픽업 서비스인 경우 옵션에 상품정보를 넣는다.
                                                            $tourOptions[] = [
                                                                'id'       => $tps['transfer_price_idx'],
                                                                'target'   => 1,
                                                                'name'     => $tps['vehicle_type_ko'] . '-' . $tps['customer_count_to'] . '인',
                                                                'type'     => 3,
                                                                'required' => true,
                                                                'price'    => (int)($tps['sale_price'] ?? 0)
                                                            ];
                                                        }
                                                    }
                                                }
                                            }

                                        } else {

                                            $paxArr = [];
                                            $paxArr[] = 1;

                                            foreach ($ts['transfer_prices'] as $tps) {

                                                $priceArr[] = $priceAdultArr[] = $tps['sale_price'] ?? 0;

                                                if (isset($tps['customer_count_to'])) {
                                                    $paxArr[] = (int)$tps['customer_count_to'];
                                                }

                                                // 픽업 서비스인 경우 옵션에 상품정보를 넣는다.
                                                $tourOptions[] = [
                                                    'id'       => $tps['transfer_price_idx'],
                                                    'target'   => 1,
                                                    'name'     => $tps['vehicle_type_ko'] . '-' . $tps['customer_count_to'] . '인',
                                                    'type'     => 3,
                                                    'required' => true,
                                                    'price'    => (int)($tps['sale_price'] ?? 0)
                                                ];
                                            }
                                        }

                                    }

                                    // 자상품 옵션
                                    if (isset($tsp['transfer_option']['transfer_fields']) && ! empty($tsp['transfer_option']['transfer_fields'])) {

                                        $tFields = $tsp['transfer_option']['transfer_fields'];

                                        foreach ($tFields as $tf) {

                                            $tmpOpt = [];
                                            $tmpOpt['id'] = $tf['field_id'];
                                            $tmpOpt['target'] = 1;
                                            $tmpOpt['name'] = $tf['field_label'];
                                            $tmpOpt['type'] = (isset($tf['field_items']) && ! empty($tf['field_items'])) ? 1 : 4;
                                            $tmpOpt['required'] = true;
                                            $tmpOpt['price'] = 0;

                                            if (isset($tf['field_items']) && ! empty($tf['field_items']) && is_array($tf['field_items'])) {
                                                $tmpOpt['item'] = [];
                                                foreach ($tf['field_items'] as $fl) {
                                                    $tmpOpt['item'][] = [
                                                        'label' => $fl['item_text'] ?? '',
                                                        'value' => $fl['item_value'] ?? '',
                                                    ];
                                                }
                                            }

                                            $tourOptions[] = $tmpOpt;
                                        }
                                    }
                                }
                            }
                        }

                        $productDetail['minPax'] = ( ! empty($paxArr)) ? min($paxArr) : 0;
                        $productDetail['maxPax'] = ( ! empty($paxArr)) ? max($paxArr) : 0;

                        $tourOptions = array_merge($tourOptions, [
                            [
                                'id' => 'arrival_date',
                                'target' => 1,
                                'name' => '현지 도착일',
                                'type' => 6,
                                'required' => true,
                                'price' => 0
                            ],
                            [
                                'id' => 'arrival_time',
                                'target' => 1,
                                'name' => '현지 도착시간',
                                'type' => 10,
                                'required' => true,
                                'price' => 0
                            ],
                            [
                                'id' => 'duration_date',
                                'target' => 1,
                                'name' => '여행일수',
                                'type' => 3,
                                'required' => true,
                                'price' => 0
                            ],
                            [
                                'id' => 'participant_sex',
                                'target' => 2,
                                'name' => '성별',
                                'type' => 1,
                                'required' => true,
                                'price' => 0,
                                'item' => [
                                    [
                                        'label' => 'Male',
                                        'value' => 'Male'
                                    ],
                                    [
                                        'label' => 'Female',
                                        'value' => 'Female'
                                    ]
                                ]
                            ],
                            [
                                'id' => 'participant_name',
                                'target' => 2,
                                'name' => '이름',
                                'type' => 4,
                                'required' => true,
                                'price' => 0
                            ],
                            [
                                'id' => 'participant_birth',
                                'target' => 2,
                                'name' => '생년월일',
                                'type' => 6,
                                'required' => true,
                                'price' => 0
                            ],
                            [
                                'id' => 'participant_nation',
                                'target' => 2,
                                'name' => '국가',
                                'type' => 4,
                                'required' => false,
                                'price' => 0
                            ],
                        ]);

                        $productDetail['hasSeniorPrice'] = false;
                        $productDetail['hasTeenagerPrice'] = false;
                        $productDetail['minTeenagerAge'] = 0;
                        $productDetail['allowInfants'] = $productDetail['allowInfants'] ?? false;
                        $productDetail['maxInfantAge'] = $productDetail['maxInfantAge'] ?? 0;
                        $productDetail['instantConfirmation'] = false;
                        $productDetail['voucherType'] = 'E_VOUCHER';
                        $productDetail['voucherUse'] = $this->_replaceSpecialChar($voucherUse);
                        //$productDetail['meetingTime'] = $tourItem[''];
                        //$productDetail['meetingLocation'] = $tourItem[''];
                        $productDetail['isNonRefundable'] = $isNoneRefund;
                        $productDetail['refundPolicy'] = $refundPolicy;
                        //$productDetail['validityType'] = $tourItem[''];
                        //$productDetail['validityDate'] = $tourItem[''];
                        $productDetail['minPrice'] = ( ! empty($priceArr)) ? (int)min($priceArr) : 0;
                        $productDetail['options'] = $tourOptions;

                        // 날짜검색이라면 데이터 초기화
                        if (true === $dateSearchFlag) {
                            $productDetail = [];
                        }

                        if (empty($priceAdultArr)) {
                            $priceAdultArr = [0];
                        }
                        if (empty($priceChildArr)) {
                            $priceChildArr = [0];
                        }

                        // 검색 날짜내에 가능한 상품이 있으면
                        if ( ! empty($saleAvailableTour)) {
                            foreach ($saleAvailableTour as $st) {
                                $productDetail[] = [
                                    'date'      => $st['from_to'],
                                    'available' => true,
                                    'price'     => [
                                        'adult'    => (int)max($priceAdultArr),
                                        'child'    => (int)max($priceChildArr),
                                        'senior'   => 0,
                                        'teenager' => 0,
                                    ]
                                ];
                            }
                        }

                        break;


                    // 자상품 리스트 반환
                    case (isset($params['productTypes']) && ! empty($params['productTypes']) && $params['productTypes'] !== 'null') :


                        if ( ! is_array($params['product'])) {
                            throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                        }

                        if (isset($product['tour_items']) && is_array($product['tour_items'])) {
                            foreach ($product['tour_items'] as $row) {
                                $productDetail[] = [
                                    'productTypeKey' => $row['tour_item_idx'],
                                    'title'          => $this->_replaceSpecialChar($row['tour_ko']),
                                ];
                            }
                        } elseif (isset($product['transfer_items']) && is_array($product['transfer_items'])) {
                            foreach ($product['transfer_items'] as $row) {
                                $productDetail[] = [
                                    'productTypeKey' => $row['transfer_item_idx'],
                                    'title'          => $this->_replaceSpecialChar($row['transfer_ko']),
                                ];
                            }
                        }

                        break;

                    // 모상품 상세
                    default :

                        if ( ! is_array($params['product']) || empty($product) ) {
                            throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                        }

                        $desc = ( ! empty($product['comment_content'])) ? $product['comment_content'] : ($product['description_content'] ?? null);
                        $desc = $this->_replaceSpecialChar(strip_tags($desc));

                        $productTitle = $product['product_ko'] ?? '';
                        $productDetail = [
                            'productKey'     => $product['product_idx'],
                            'updatedAt'      => $product['modify_datetime'] ?? null,
                            'title'          => $this->_replaceSpecialChar($productTitle),
                            'description'    => $desc ?? null,
                            'highlights'     => $product['highlight_content'] ?? null,
                            'additionalInfo' => strip_tags($product['comment_content'] ?? null) ?? null,
                        ];

                        // 여정
                        $productDetail['itinerary'] = '';
                        if ( ! empty($product['tour_itineraries']) && is_array($product['tour_itineraries'])) {
                            foreach ($product['tour_itineraries'] as $ti) {
                                $productDetail['itinerary'] .= ($ti['itinerary_daytime'] ?? null) . PHP_EOL;
                                $productDetail['itinerary'] .= ($ti['itinerary_route'] ?? null) . PHP_EOL;
                                $productDetail['itinerary'] .= ($ti['itinerary_content'] ?? null) . PHP_EOL . PHP_EOL;
                            }

                            $productDetail['itinerary'] = strip_tags($productDetail['itinerary']);
                        }

                        $productDetail['latitude'] = 0;
                        $productDetail['longitude'] = 0;
                        if (isset($product['tour_maps'][0]) && ! empty($product['tour_maps'][0])) {
                            $productDetail['latitude'] = (float)$product['tour_maps'][0]['point_latitude'];
                            $productDetail['longitude'] = (float)$product['tour_maps'][0]['point_longitude'];
                        }


                        $productDetail['currency'] = 'KRW';

                        // 여행상품 최소가
                        $priceArr = [];
                        if ( ! empty($product['tour_items']) && is_array($product['tour_items'])) {
                            foreach ($product['tour_items'] as $ti) {

                                if ( ! isset($ti['tour_suppliers']) || empty($ti['tour_suppliers'])) {
                                    continue;
                                }

                                foreach ($ti['tour_suppliers'] as $tsp) {
                                    foreach ($tsp['tour_seasons'] as $ts) {
                                        foreach ($ts['tour_prices'] as $tps) {
                                            $priceArr[] = $tps['sale_price'] ?? 0;
                                        }
                                    }
                                }
                            }
                        }

                        // 픽업상품 최소가
                        if ( ! empty($product['transfer_items']) && is_array($product['transfer_items'])) {
                            foreach ($product['transfer_items'] as $ti) {

                                if ( ! isset($ti['transfer_suppliers']) || empty($ti['transfer_suppliers'])) {
                                    continue;
                                }

                                foreach ($ti['transfer_suppliers'] as $tsp) {
                                    foreach ($tsp['transfer_seasons'] as $ts) {
                                        foreach ($ts['transfer_prices'] as $tps) {
                                            $priceArr[] = $tps['sale_price'] ?? 0;
                                        }
                                    }
                                }
                            }
                        }

                        $productDetail['minPrice'] = ( ! empty($priceArr)) ? ((int)min($priceArr)) : 0;

                        $redisKey = TFConst::REDIS_TF_PRODUCT_REDIS_KEY . '_' . $product['product_idx'];
                        if (false !== ($productRedis = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {
                            $productDetail['location'] = TFConst::TS_AREA_CODE[$productRedis['area_idx']][TFConst::TS_AREA_LOC_TXT] ?? null;
                            $productDetail['city'] = TFConst::TS_AREA_CODE[$productRedis['area_idx']][TFConst::TS_AREA_CITY_TXT] ?? null;
                        }

                        $productDetail['photos'] = [];
                        if (isset($product['image_urls']) && ! empty($product['image_urls'])) {
                            foreach ($product['image_urls'] as $iu) {
                                $productDetail['photos'][] = ['path' => $iu];
                            }
                        }

                }

                $results = $productDetail;


            } catch (\Exception $ex) {
                $results = [
                    'result' => ErrorConst::FAIL_STRING,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * TF 상품 배치
         * Redis에 무조건 덮어쓴다.
         *
         * @param Request  $request
         * @param Response $response
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function setProductsBatch(Request $request, Response $response)
        {
            $products = null;
            $batchProducts = [];
            $productDetail = [];

            $startDate = CommonUtil::getDateTime();
            $startTime = CommonUtil::getMicroTime();
            $startTimeStamp = $startDate . ' ' . $startTime;
            $updateWebHook = 'http://tnaapi.tourvis.com/apiary/common/webhook/v1/product';
            $updatedParentProduct = [];
            $updatedChildProduct = [];

            try {

                $params = $request->getAttribute('params');

                if (!isset($params['execute']) || empty($params['execute'])) {
                    throw new \Exception('Batch Non-excute');
                }

                // 지역 정보
                $redisKey = TFConst::REDIS_TF_AREA_REDIS_KEY;
                $targetUrl = TFConst::TF_URL . TFConst::TF_GET_AREA_URI;
                $eventKey = $this->_getRedisEventKey(['action'=>'batch_get_areas', 'datetime'   => $startTimeStamp]);
                $this->guzzleOpt['query'] = ['event_key' => $eventKey];
                $areaResult = Http::httpRequest($targetUrl, $this->guzzleOpt);

                if (!empty($areaResult['errors'])) {
                    LogMessage::error(json_encode($areaResult['errors'], JSON_UNESCAPED_UNICODE));
                    throw new \Exception('Batch Area Error!!');
                }

                $areas = $areaResult['areas'];
                $this->_setRedis($redisKey, $areas);

                foreach ($areas as $row) {

                    // 지역별 모상품 리스트
                    $areaIdx = $row['area_idx'];
                    $redisKey = TFConst::REDIS_TF_PRODUCTS_REDIS_KEY . '_' . $areaIdx;

                    if ( ! array_key_exists($areaIdx, TFConst::TS_AREA_CODE)) {
                        LogMessage::error('NOT EXIST TF area_idx :: ' . $areaIdx);
                        continue;
                    }

                    LogMessage::info('TF area_idx :: ' . $areaIdx);

                    $eventKey = $this->_getRedisEventKey(['action' => 'batch_get_products', 'area_idx' => $areaIdx, 'datetime' => $startTimeStamp]);
                    $this->guzzleOpt['query'] = ['event_key' => $eventKey];
                    $targetUrl = TFConst::TF_URL . TFConst::TF_GET_AREA_URI . '/' . $areaIdx . '/products';
                    $areaProducts = Http::httpRequest($targetUrl, $this->guzzleOpt);

                    if (!empty($areaProducts['errors'])) {
                        LogMessage::error($targetUrl);
                        LogMessage::error(json_encode($areaProducts['errors'], JSON_UNESCAPED_UNICODE));
                        continue;
                    }

                    $areaProducts = $areaProducts['products'];
                    $this->_setRedis($redisKey, $areaProducts);

                    try {

                        // 지역별 모상품 리스트 루프
                        foreach ($areaProducts as $aps) {

                            $productIdx = $aps['product_idx'];
                            $updateFlag = false;

                            // 모상품 상세정보
                            LogMessage::info('TF product_idx :: ' . $productIdx);

                            $redisKey = TFConst::REDIS_TF_PRODUCT_REDIS_KEY . '_' . $productIdx;
                            $targetUrl = TFConst::TF_URL . TFConst::TF_GET_PRODUCTS_URI . '/' . $productIdx;

                            $eventKey = $this->_getRedisEventKey(['action'=>'batch_get_product', 'product_idx' => $productIdx, 'datetime' => $startTimeStamp]);
                            $this->guzzleOpt['query'] = ['event_key' => $eventKey];
                            $productDetail = Http::httpRequest($targetUrl, $this->guzzleOpt);

                            if (!empty($productDetail['errors'])) {
                                LogMessage::error($targetUrl);
                                LogMessage::error(json_encode($productDetail['errors'], JSON_UNESCAPED_UNICODE));
                                continue;
                            }

                            $productDetail = $productDetail['product'];
                            $productDetail['area_idx'] = $areaIdx;

                            // 배치때 날짜 비교
                            try {

                                if (false !== ($productDetailOld = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {
                                    LogMessage::info('product_old :: ' . $productIdx . ' - date :: ' . ($productDetailOld['modify_datetime'] ?? null));
                                    if ($productDetailOld['modify_datetime'] <> $productDetail['modify_datetime']) {
                                        $updateFlag = true;
                                        $updatedParentProduct[] = [
                                            'productKey' => $productIdx,
                                            'updateAt'   => $productDetail['modify_datetime'],
                                        ];
                                    }
                                }

                            } catch (\Exception $eee) {
                                LogMessage::error('modify_datetime diff error');
                                LogMessage::info('product :: ' . $productIdx . ' - date :: ' . ($productDetail['modify_datetime'] ?? null));
                            }

                            $this->_setRedis($redisKey, $productDetail);

                            try {

                                $tourItemIdx = 0;
                                $productSubDetail = null;

                                // 투어상품 상세정보
                                if (isset($productDetail['tour_items']) && is_array($productDetail['tour_items'])) {
                                    foreach ($productDetail['tour_items'] as $ti) {

                                        $tourItemIdx = $ti['tour_item_idx'];
                                        LogMessage::info('TF tour_item_idx :: ' . $tourItemIdx);

                                        $productSubDetail = [
                                            'area_idx'    => $areaIdx,
                                            'product_idx' => $productIdx,
                                            'tour_item'   => $ti
                                        ];

                                        $redisKey = TFConst::REDIS_TF_SUB_PRODUCT_REDIS_KEY . '_' . $tourItemIdx;
                                        $this->_setRedis($redisKey, $productSubDetail);
                                    }
                                }

                                // 픽업상품 상세정보
                                if (isset($productDetail['transfer_items']) && is_array($productDetail['transfer_items'])) {
                                    foreach ($productDetail['transfer_items'] as $ti) {

                                        $tourItemIdx = $ti['transfer_item_idx'];
                                        LogMessage::info('TF transfer_item_idx :: ' . $tourItemIdx);

                                        $productSubDetail = [
                                            'area_idx'      => $areaIdx,
                                            'product_idx'   => $productIdx,
                                            'transfer_item' => $ti
                                        ];

                                        $redisKey = TFConst::REDIS_TF_SUB_PRODUCT_REDIS_KEY . '_' . $tourItemIdx;
                                        $this->_setRedis($redisKey, $productSubDetail);
                                    }
                                }

                                if ($updateFlag === true) {
                                    $updatedChildProduct[$productDetail['product_idx']][] = $tourItemIdx;
                                }

                            } catch (\Exception $ee) {
                                LogMessage::error('SubProduct Detail Error :: ' . $areaIdx . '-' . $productIdx . '-' . $tourItemIdx);
                                LogMessage::error($ee->getMessage());
                                LogMessage::info(json_encode($tourItemIdx, JSON_UNESCAPED_UNICODE));
                            }

                            if (!array_key_exists($productIdx, $batchProducts)) {
                                $batchProducts[$productIdx] = $productIdx;
                                $products[] = [
                                    'productKey' => $productIdx,
                                    'updatedAt'  => $productDetail['modify_datetime'] ?? null,
                                    'title'      => $this->_replaceSpecialChar($aps['product_ko'])
                                ];
                            }

                            sleep(1);

                        }

                    } catch (\Exception $e) {

                        LogMessage::error('Product Detail Error :: ' . $productIdx . '_' . $areaIdx);
                        LogMessage::error($e->getMessage());
                        LogMessage::info(json_encode($productDetail, JSON_UNESCAPED_UNICODE));

                    }

                    sleep(1);

                }

                $this->_setRedis(TFConst::REDIS_TF_PRODUCTS_REDIS_KEY, $products);
                $results = $products;

            } catch (\Exception $ex) {
                $results = [
                    'result' => ErrorConst::FAIL_STRING,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            if (!empty($updatedParentProduct)) {
                try {

                    foreach ($updatedParentProduct as $key => $pidx) {
                        try {

                            $data = [
                                'productKey' => $pidx['productKey'],
                                'updateType' => 'UPDATE',
                                'updateAt' => $pidx['updateAt'],
                            ];

                            $command = 'curl -X ';
                            $command .= '-d "' . http_build_query($data) . '" ';

                            $command .= $updateWebHook . ' -s > /dev/null 2>&1 &';
                            passthru($command);

                            foreach ($updatedChildProduct[$pidx['productKey']] as $cidx) {
                                try {
                                    $data = [
                                        'productKey'     => $pidx['productKey'],
                                        'productTypeKey' => $cidx,
                                        'updateType'     => 'UPDATE',
                                        'updateAt'       => $pidx['updateAt']
                                    ];

                                    $command = 'curl -X ';
                                    $command .= '-d "' . http_build_query($data) . '" ';

                                    $command .= $updateWebHook . ' -s > /dev/null 2>&1 &';
                                    LogMessage::debug('curl command :: ' . $command);
                                    passthru($command);
                                } catch (\Exception $e) {
                                    LogMessage::error('Child Product Webhook Error :: ' . $cidx);
                                }
                            }

                        } catch (\Exception $ex) {
                            LogMessage::error('Product Webhook Error :: ' . $pidx['productKey']);
                        }
                    }

                } catch (\Exception $eex) {
                    LogMessage::error('Product Webhook Error');
                }
            }

            $endTime = CommonUtil::getMicroTime();

            LogMessage::info('TF Batch Start :: ' . $startTimeStamp);
            LogMessage::info('TF Batch End :: ' . CommonUtil::getDateTime() . ' ' . $endTime);
            LogMessage::info('TF Batch Runtime :: ' . ($endTime - $startTime));

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }

        /**
         * 특정 상품의 배치
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function setProductBatch(Request $request, Response $response, $args)
        {

            $productIdx = $args['product_idx'] ?? null;
            $areaIdx = $args['area_idx'] ?? null;

            try {

                if (false === CommonUtil::validateParams($args, ['product_idx', 'area_idx'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                // 모상품 상세정보
                $redisKey = TFConst::REDIS_TF_PRODUCT_REDIS_KEY . '_' . $productIdx;
                //if (false === ($productDetail = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {
                LogMessage::info('TF product_idx :: ' . $productIdx);
                $targetUrl = TFConst::TF_URL . TFConst::TF_GET_PRODUCTS_URI . '/' . $productIdx;
                $productDetail = Http::httpRequest($targetUrl, $this->guzzleOpt);
                $productDetail['area_idx'] = $areaIdx;
                $this->_setRedis($redisKey, $productDetail);
                //}

                try {

                    $tourItemIdx = 0;

                    //자상품 상세정보
                    if (isset($productDetail['tour_items']) && is_array($productDetail['tour_items'])) {
                        foreach ($productDetail['tour_items'] as $ti) {

                            $tourItemIdx = $ti['tour_item_idx'];
                            $redisKey = TFConst::REDIS_TF_SUB_PRODUCT_REDIS_KEY . '_' . $tourItemIdx;
                            if (false === ($productSubDetail = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {

                                LogMessage::info('TF tour_item_idx :: ' . $tourItemIdx);
                                $productSubDetail[] = [
                                    'area_idx'    => $areaIdx,
                                    'product_idx' => $productIdx,
                                    'tour_item'   => $ti
                                ];

                                $this->_setRedis($redisKey, $productSubDetail);
                            }
                        }
                    }

                    // 픽업 상품
                    if (isset($productDetail['transfer_items']) && is_array($productDetail['transfer_items'])) {
                        foreach ($productDetail['transfer_items'] as $ti) {

                            $tourItemIdx = $ti['transfer_item_idx'];
                            $redisKey = TFConst::REDIS_TF_SUB_PRODUCT_REDIS_KEY . '_' . $tourItemIdx;
                            if (false === ($productSubDetail = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {

                                LogMessage::info('TF tour_item_idx :: ' . $tourItemIdx);
                                $productSubDetail[] = [
                                    'area_idx'      => $areaIdx,
                                    'product_idx'   => $productIdx,
                                    'transfer_item' => $ti
                                ];

                                $this->_setRedis($redisKey, $productSubDetail);
                            }
                        }
                    }

                } catch (\Exception $ee) {
                    LogMessage::error('SubProduct Detail Error :: ' . $productIdx . '-' . $tourItemIdx . '_' . $areaIdx);
                    LogMessage::error($ee->getMessage());
                    LogMessage::info(json_encode($ti, JSON_UNESCAPED_UNICODE));
                }

                $products[] = [
                    'productKey' => $productIdx,
                    'updatedAt'  => $productDetail['modify_datetime'] ?? null,
                    'title'      => $productDetail['product_ko']
                ];

                $results = [
                    'result' => ErrorConst::SUCCESS_STRING,
                ];

            } catch (\Exception $ex) {

                LogMessage::error('Product Detail Error :: ' . $productIdx . '_' . $areaIdx);
                LogMessage::error($ex->getMessage());
                LogMessage::info(json_encode($productDetail, JSON_UNESCAPED_UNICODE));

                $results = [
                    'result' => ErrorConst::FAIL_STRING,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }


            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * 타이드 스퀘어 권한 체크
         *
         * @param Request  $request
         * @param Response $response
         *
         * @return Response
         */
        public function authCheck(Request $request, Response $response)
        {
            try {

                $params = $request->getAttribute('params');

                if (false === CommonUtil::validateParams($params, ['supplierCode'])) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                if ($params['supplierCode'] !== TFConst::TS_SUPPLIER_CODE) {
                    LogMessage::error('TS supplierCode :: ' . $params['supplierCode']);
                    throw new \Exception('', ErrorConst::ERROR_RDB_APP_AUTH_FAIL);
                }

                $accessToken = '';
                $headers = $request->getHeaders();

                // supplier code
                if (isset($headers['HTTP_AUTHORIZATION']) && ! empty($headers['HTTP_AUTHORIZATION'])) {
                    $accessToken = str_replace('Bearer ', '', $headers['HTTP_AUTHORIZATION'][0]);
                } elseif (isset($headers['HTTP_ACCESS_TOKEN']) && ! empty($headers['HTTP_ACCESS_TOKEN'])) {
                    $accessToken = $headers['HTTP_ACCESS_TOKEN'][0];
                }

                if ($accessToken !== TFConst::TS_ACCESS_TOKEN) {
                    LogMessage::error('TS Auth Fail - AccessToken :: ' . $accessToken);
                    throw new \Exception('', ErrorConst::ERROR_RDB_APP_AUTH_FAIL);
                }

                // supplier code
                if ( ! isset($headers['HTTP_SUPPLIER_CODE'])) {
                    LogMessage::error('TS Auth Fail - Supplier Code is not exist');
                    throw new \Exception('', ErrorConst::ERROR_RDB_APP_AUTH_FAIL);
                }

                if ($headers['HTTP_SUPPLIER_CODE'][0] !== TFConst::TS_SUPPLIER_CODE) {
                    LogMessage::error('TS Auth Fail - Supplier Code :: ' . $accessToken);
                    throw new \Exception('', ErrorConst::ERROR_RDB_APP_AUTH_FAIL);
                }

                $results = [
                    'result' => true
                ];

            } catch (\Exception $ex) {

                $results = [
                    'result' => false,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * * 예약진행
         *
         * @param Request  $request
         * @param Response $response
         *
         * 1. Tour(인원별): tour_item_idx, tour_season_idx, tour_customer_idx
         * 2. Transfer(차량별): transfer_item_idx, transfer_price_idx
         * 3. 공통사항: 이용일, 여행자정보, 옵션 등
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function addBooking(Request $request, Response $response)
        {

            $adultAmount = 0;
            $childrenAmount = 0;
            $totalAmount = 0;

            try {

                $params = $request->getAttribute('params');

                if (false === CommonUtil::validateParams($params, ['product', 'adults', 'children', 'arrivalDate', 'selectTimeslot'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $product = $params['product'];

                $result = [
                    'product_idx'         => (int)$product['product_idx'],
                    'sub_product_idx'     => (int)$params['productTypeKey'],
                    'customer_salutation' => $params['customer_salutation'],
                    'customer_firstName'  => $params['customer_firstName'],
                    'customer_lastName'   => $params['customer_lastName'],
                    'customer_email'      => $params['customer_email'],
                    'customer_phone'      => $params['customer_phone'],
                    'arrival_date'        => $params['arrivalDate'],
                    'select_time_slot'    => $params['selectTimeslot'] ?? '00:00',
                    'message'             => $params['message'] ?? '',
                    'options'             => $params['options'] ?? [],
                    'adults'              => (int)($params['adults'] ?? 0),
                    'children'            => (int)($params['children'] ?? 0),
                ];

                if (!isset($params['options']['perBooking'])) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                if (!isset($params['options']['perPax'])) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $arrivalDate = date('Ymd', strtotime($params['arrivalDate']));
                foreach ($params['options']['perBooking'] as $pBooking) {
                    if ($pBooking['id'] === 'arrival_date') {
                        $arrivalDate = date('Ymd', strtotime($pBooking['value']));
                        break;
                    };
                }

                // 투어 상품과 픽업 상품이 나뉨
                switch (true) {
                    case (isset($product['tour_items']) && is_array($product['tour_items'])) : // 투어상품

                        if (false === ($tourIdx = array_search($params['productTypeKey'], array_column($product['tour_items'], 'tour_item_idx')))) {
                            throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                        }

                        $tourItem = $product['tour_items'][$tourIdx];
                        $tourItemIdx = (int)$tourItem['tour_item_idx'];
                        $tourSuppliers = $tourItem['tour_suppliers'] ?? null;
                        $result['tour_item_idx'] = $tourItemIdx;
                        $tourCustomIdxs = [];

                        if (empty($tourSuppliers)) {
                            throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                        }

                        foreach ($tourSuppliers as $ts) {
                            if (isset($ts['tour_seasons']) && is_array($ts['tour_seasons'])) {
                                foreach ($ts['tour_seasons'] as $tss) {

                                    // 날짜 비교
                                    if (false === $this->_validRangeDate($tss['season_date_from'], $tss['season_date_to'], $arrivalDate)) {
                                        throw new \Exception('예약 생성에 실패하였습니다. - 날짜 오류 (' . $arrivalDate . ', '. $tss['season_date_from'] . ', '. $tss['season_date_to'] .')');
                                    }

                                    $weeks = explode( ',', $tss['season_week']);
                                    $tmpWeek = date('w', strtotime($arrivalDate));

                                    if (! in_array($tmpWeek, $weeks)) {
                                        throw new \Exception('예약 생성에 실패하였습니다. - 날짜(요일) 오류' . $tmpWeek . ' ' . json_encode($tmpWeek));
                                    }

                                    $result['tour_season_idx'] = (int)$tss['tour_season_idx'];

                                    // 성인 가격
                                    if ( ! empty($params['adults'])) {
                                        $adultsCount = $params['adults'];
                                        do {
                                            $adultsCount--;
                                            foreach ($tss['tour_prices'] as $tp) {
                                                if ($tp['sale_unit_ko'] === '성인') {
                                                    $tourCustomIdxs[] = (int)$tp['tour_customer_idx'];
                                                    $adultAmount += (int)$tp['sale_price'];
                                                    $totalAmount += (int)$tp['sale_price'];
                                                }
                                            }
                                        } while (0 < $adultsCount);
                                    }

                                    // 아동 가격
                                    if ( ! empty($params['children'])) {
                                        $childrenCount = $params['children'];
                                        do {
                                            $childrenCount--;
                                            foreach ($tss['tour_prices'] as $tp) {
                                                if ($tp['sale_unit_ko'] === '아동') {
                                                    $tourCustomIdxs[] = (int)$tp['tour_customer_idx'];
                                                    $childrenAmount += (int)$tp['sale_price'];
                                                    $totalAmount += (int)$tp['sale_price'];
                                                }
                                            }
                                        } while (0 < $childrenCount);
                                    }

                                }
                            }
                        }

                        $result['tour_customer_idx'] = $tourCustomIdxs;

                        break;

                    case (isset($product['transfer_items']) && is_array($product['transfer_items'])) :  // 픽업상품

                        if (false === ($tourIdx = array_search($params['productTypeKey'],
                                array_column($product['transfer_items'], 'transfer_item_idx')))
                        ) {
                            throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                        }

                        $tourItem = $product['transfer_items'][$tourIdx];
                        $tourItemIdx = $tourItem['transfer_item_idx'];
                        $result['transfer_item_idx'] = (int)$tourItemIdx;

                        $tourSuppliers = $tourItem['transfer_suppliers'] ?? null;

                        if (empty($tourSuppliers)) {
                            throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                        }

                        foreach ($tourSuppliers as $ts) {
                            if (isset($ts['transfer_seasons']) && is_array($ts['transfer_seasons'])) {
                                foreach ($ts['transfer_seasons'] as $tss) {

                                    // 날짜 비교
                                    if (false === $this->_validRangeDate($tss['season_date_from'], $tss['season_date_to'], $arrivalDate)) {
                                        throw new \Exception('예약 생성에 실패하였습니다. - 날짜 오류');
                                    }

                                    $weeks = explode( ',', $tss['season_week']);
                                    $tmpWeek = date('w', strtotime($arrivalDate));

                                    if (! in_array($tmpWeek, $weeks)) {
                                        throw new \Exception('예약 생성에 실패하였습니다. - 날짜 오류');
                                    }

                                    if ( ! isset($params['options']['perBooking'][0]['id'])) {
                                        LogMessage::error('No Option data');
                                        throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                                    }

                                    $perBookingOption = $params['options']['perBooking'] ?? null;
                                    $transferInfo = array_shift($perBookingOption);

                                    $transferPriceIdx = $transferInfo['id'];
                                    $totalAmount += (int)($transferInfo['price'] ?? 0);

                                    if (false === ($tourIdx = array_search($transferPriceIdx, array_column($tss['transfer_prices'], 'transfer_price_idx')))) {
                                        continue;
                                    }

                                    $result['transfer_price_idx'] = (int)$transferPriceIdx;
                                    $result['options']['perBooking'] = $perBookingOption;

                                }
                            }
                        }

                        break;
                }

                $result['amount'] = [
                    'adult' => (int)$adultAmount,
                    'child' => (int)$childrenAmount,
                    'total' => (int)$totalAmount
                ];

                $redisArr = ['action' => 'addBooking', 'datetime' => date('Y-m-d H:i:s')];
                $redisArr = array_merge($redisArr, $result);

                $eventKey = $this->_getRedisEventKey($redisArr);

                $this->guzzleOpt['form_params'] = ['event_key' => $eventKey];
                $this->guzzleOpt['allow_redirects'] = true;
                $targetUrl = TFConst::TF_BOOK_ADD_URL;
                $addResult = Http::httpRequest($targetUrl, $this->guzzleOpt, CommonConst::REQ_METHOD_POST);

               LogMessage::error('addBooking Result ::' . json_encode($addResult, JSON_UNESCAPED_UNICODE));

                if (empty($addResult)) {
                    throw new \Exception('예약 생성에 실패하였습니다. - TF 통신 오류');
                }

                if (!empty($addResult['errors'])) {
                    throw new \Exception('예약 생성에 실패하였습니다.');
                }

                if (!isset($addResult['book']['book_cd']) || empty($addResult['book']['book_cd'])) {
                    throw new \Exception('예약 생성에 실패하였습니다.');
                }

                $bookIdx = $addResult['book']['book_cd'];
                switch ($addResult['book']['book_status_cd']) {
                    case TFConst::TF_BOOK_CD_INS :
                    case TFConst::TF_BOOK_CD_ORD :
                    case TFConst::TF_BOOK_CD_REQ :
                        $status = TFConst::RESERVE_STATUS_RESV;
                        break;
                    default :
                        throw new \Exception('예약 생성에 실패하였습니다. - 진행할 수 있는 예약이 아닙니다.');
                }

                $results = [
                    'result' => [
                        'success'          => true,
                        'status'           => $status,
                        'bookingKey'       => $bookIdx,
                        'partnerReference' => $params['partnerReference'] ?? null,
                        'totalAmount'      => $totalAmount,
                    ]
                ];

                $this->_setBookingRedis($bookIdx, [
                    'success'          => true,
                    'status'           => $status,
                    'bookingKey'       => $bookIdx,
                    'partnerReference' => $params['partnerReference'] ?? null,
                    'totalAmount'      => $totalAmount,
                    'tf_result'        => $addResult
                ]);

                $this->_saveBookingLog($bookIdx, $results);

            } catch (\Exception $ex) {

                $results = [
                    'result' => [
                        'success'     => false,
                        'status'      => TFConst::STATUS_ERROR,
                        'bookingKey'  => null,
                        'totalAmount' => $totalAmount,
                        'message'     => $this->_getErrorMessage($ex, true, [
                            'subject' => '부킹 생성에러',
                            'body'    => $ex->getMessage(),
                        ]),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }

        /**
         * 예약 진행
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function proceedBooking(Request $request, Response $response, $args)
        {
            try {

                if (false === CommonUtil::validateParams($args, ['bookingKey'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $redisDb = CommonConst::REDIS_MESSAGE_SESSION;
                $redisKey = TFConst::REDIS_TF_BOOKING_REDIS_KEY . '_' . $args['bookingKey'];

                if (false === ($bookingStatusRedis = RedisUtil::getData($this->redis, $redisKey, $redisDb))) {
                    throw new \Exception(ErrorConst::ERROR_DESCRIPTION_ARRAY[ErrorConst::ERROR_RDB_NO_DATA_EXIST]);
                }

                if ($bookingStatusRedis['status'] !== TFConst::RESERVE_STATUS_RESV) {
                    throw new \Exception('이미 예약 진행중 이거나 진행 가능한 주문이 아닙니다.');
                }

                $this->guzzleOpt['json'] = ['event_key' => $this->_getRedisEventKey([
                    'action' => 'proceedBooking',
                    'bookingKey' => $args['bookingKey'],
                    'datetime'   => date('Y-m-d H:i:s'),
                ])];

                $this->guzzleOpt['allow_redirects'] = true;
                $targetUrl = TFConst::TF_BOOK_PROC_URL . '/' . $args['bookingKey'];
                $procResult = Http::httpRequest($targetUrl, $this->guzzleOpt, CommonConst::REQ_METHOD_PATCH);

                if (empty($procResult)) {
                    throw new \Exception('예약 진행에 실패하였습니다. - TF 서버오류');
                }

                if (!empty($procResult['errors'])) {
                    throw new \Exception('예약 진행에 실패하였습니다.');
                }

                if (!isset($procResult['book']['book_cd']) || empty($procResult['book']['book_cd'])) {
                    throw new \Exception('예약 진행에 실패하였습니다.');
                }

                $results = [
                    'success'          => true,
                    'status'           => TFConst::RESERVE_STATUS_WAIT,
                    'bookingKey'       => $procResult['book']['book_cd'],
                    'partnerReference' => $bookingStatusRedis['partnerReference'] ?? null,
                    'totalAmount'      => (int)$procResult['book']['sale_price'],
                    'refundAmount'     => (int)$procResult['book']['sale_price'],
                    'tf_result'        => $procResult
                ];

                $this->_setBookingRedis($results['bookingKey'], $results);
                $this->_saveBookingLog($results['bookingKey'], $results);

                unset($results['refundAmount']);
                unset($results['tf_result']);

            } catch (\Exception $ex) {

                $results = [
                    'success'          => false,
                    'bookingKey'       => $args['bookingKey'],
                    'partnerReference' => null,
                    'totalAmount'      => 0,
                    'message'          => $ex->getMessage(),
                ];

                $body = json_encode($results, JSON_UNESCAPED_UNICODE) . PHP_EOL;
                $body .= $ex->getMessage();

                $this->_getErrorMessage($ex, true, ['subject' => '부킹 진행에러', 'body' => $body]);

            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * 예약 취소
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         */
        public function cancelBooking(Request $request, Response $response, $args)
        {
            try {

                if (false === CommonUtil::validateParams($args, ['bookingKey'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $redisDb = CommonConst::REDIS_MESSAGE_SESSION;
                $redisKey = TFConst::REDIS_TF_BOOKING_REDIS_KEY . '_' . $args['bookingKey'];

                if (false === ($bookingStatusRedis = RedisUtil::getData($this->redis, $redisKey, $redisDb))) {
                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                }

                if ($bookingStatusRedis['status'] === TFConst::RESERVE_STATUS_CNCL || $bookingStatusRedis['status'] === TFConst::CANCEL_STATUS_DENY) {
                    throw new \Exception('예약 취소 가능한 주문이 아닙니다.');
                }

                if ($bookingStatusRedis['status'] === TFConst::CANCEL_STATUS_WAIT) {
                    throw new \Exception('예약 취소 진행중 입니다.');
                }

                $subject = 'TF 예약취소 요청';
                $body = '<h1>TF 예약취소 요청</h1>'
                      . '<p>주문 키 : ' . $bookingStatusRedis['bookingKey'] . '</p>'
                      . '<p>주문금액 : ' . number_format($bookingStatusRedis['totalAmount']) . '</p>'
                      . '<p>환불금액 : ' . number_format($bookingStatusRedis['refundAmount']) . '</p>'
                      ;

                if (false === AwsUtil::sendEmail($this->recipients, $subject, $body)) {
                    throw new \Exception(null, ErrorConst::ERROR_SEND_EMAIL);
                }

                $results = [
                    'success'          => true,
                    'status'           => TFConst::CANCEL_STATUS_WAIT,
                    'bookingKey'       => $args['bookingKey'],
                    'partnerReference' => $bookingStatusRedis['partnerReference'] ?? null,
                    'totalAmount'      => $bookingStatusRedis['totalAmount'],
                    'refundAmount'     => $bookingStatusRedis['refundAmount'],
                    'tf_result'        => $bookingStatusRedis['tf_result']
                ];

                $this->_setBookingRedis($args['bookingKey'], $results);
                $this->_saveBookingLog($args['bookingKey'], $results);

                unset($results['tf_result']);
                unset($results['totalAmount']);
                unset($results['partnerReference']);

            } catch (\Exception $ex) {

                $results = [
                    'success'      => false,
                    'status'       => TFConst::STATUS_ERROR,
                    'bookingKey'   => $args['bookingKey'],
                    'refundAmount' => 0,
                    'message'      => $this->_getErrorMessage($ex),
                ];

            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);
        }


        /**
         * 예약 상태 확인
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function checkBookingStatus(Request $request, Response $response, $args)
        {
            try {

                if (false === CommonUtil::validateParams($args, ['bookingKey'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                $redisDb = CommonConst::REDIS_MESSAGE_SESSION;
                $redisKey = TFConst::REDIS_TF_BOOKING_REDIS_KEY . '_' . $args['bookingKey'];

                $bookingDataType = 'redis';
                if (false === ($bookingData = RedisUtil::getData($this->redis, $redisKey, $redisDb))) {

                    $this->guzzleOpt['query'] = [
                        'event_key' => $this->_getRedisEventKey([
                            'action' => 'checkBookingStatus',
                            'bookingKey' => $args['bookingKey'],
                            'datetime'   => date('Y-m-d H:i:s'),
                        ])
                    ];

                    $this->guzzleOpt['allow_redirects'] = true;
                    $targetUrl = TFConst::TF_BOOK_PROC_URL . '/' . $args['bookingKey'];
                    $bookingData = Http::httpRequest($targetUrl, $this->guzzleOpt);
                    $bookingDataType = 'travel';

                    if (!empty($bookingData['errors'])) {
                        throw new \Exception('예약 조회에 실패하였습니다.');
                    }

                }

                $redisStatus = $bookingData['status'] ?? null;
                $tfResult = (isset($bookingData['tf_result'])) ? $bookingData['tf_result'] : $bookingData;
                $bookingStatus = (isset($bookingData['tf_result'])) ? $bookingData['tf_result']['book'] : $bookingData['book'];

                if (!isset($bookingStatus['book_status_cd']) || empty($bookingStatus['book_status_cd'])) {
                    throw new \Exception('예약 조회에 실패하였습니다.');
                }

                $results = [
                    'success'          => true,
                    'status'           => $redisStatus ?? $this->_getStatusCode($bookingStatus['book_status_cd']),
                    'bookingKey'       => $bookingStatus['book_cd'],
                    'partnerReference' => $bookingStatus['partnerReference'] ?? null,
                    'totalAmount'      => $bookingStatus['sale_price'] ?? 0,
                    'refundAmount'     => $bookingStatus['sale_price'] ?? 0,
                    'tf_result'        => $tfResult
                ];

                $this->_setBookingRedis($bookingStatus['book_cd'], $results);
                unset($results['tf_result']);

            } catch (\Exception $ex) {

                $results = [
                    'success'      => false,
                    'status'       => TFConst::STATUS_ERROR,
                    'bookingKey'   => $args['bookingKey'],
                    'message'     => $this->_getErrorMessage($ex, true, [
                        'subject' => '부킹 확인 에러',
                        'body'    => $ex->getMessage(),
                    ]),
                ];

            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);
        }


        /**
         * 트레블포레스트에서 주문관련 업데이트시 호출할 Webhook
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function bookingUpdate(Request $request, Response $response, $args)
        {
            $updateType = null;
            $oldStatus = null;

            try {

                if (false === CommonUtil::validateParams($args, ['bookingKey', 'updateType'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                // confirm || cancel || close
                switch ($args['updateType']) {
                    case TFConst::TF_BOOK_CD_CFM :
                    case TFConst::TF_BOOK_CD_CCL :
                    case TFConst::TF_BOOK_CD_CLS :
                        break;
                    default :
                        throw new \Exception(null, ErrorConst::ERROR_STEP_FAIL);
                }

                $redisDb = CommonConst::REDIS_MESSAGE_SESSION;
                $redisKey = TFConst::REDIS_TF_BOOKING_REDIS_KEY . '_' . $args['bookingKey'];

                if (false === ($bookingStatusRedis = RedisUtil::getData($this->redis, $redisKey, $redisDb))) {
                    throw new \Exception(ErrorConst::ERROR_DESCRIPTION_ARRAY[ErrorConst::ERROR_RDB_NO_DATA_EXIST]);
                }

                $oldStatus = $bookingStatusRedis['status'];
                $updateType = $this->_getStatusCode($args['updateType']);

                if (empty($updateType)) {
                    throw new \Exception(null, ErrorConst::ERROR_STEP_FAIL);
                }

                $this->guzzleOpt['query'] = ['event_key' => $this->_getRedisEventKey([
                    'action'     => 'bookingUpdate',
                    'updateType' => $args['updateType'],
                    'bookingKey' => $args['bookingKey'],
                    'datetime'   => date('Y-m-d H:i:s'),
                ])];

                $this->guzzleOpt['allow_redirects'] = true;
                $statusTargetUrl = TFConst::TF_BOOK_PROC_URL . '/' . $args['bookingKey'];
                $bookingStatus = Http::httpRequest($statusTargetUrl, $this->guzzleOpt, CommonConst::REQ_METHOD_GET);
                $curStatus = $bookingStatus['book']['book_status_cd'];

                if ($args['updateType'] != $curStatus) {
                    throw new \Exception(null, ErrorConst::ERROR_STEP_FAIL);
                }

                // 미리 CONFIRM 상태로 업데이트
                $bookingStatusRedis['status'] = $updateType;
                $this->_setBookingRedis($args['bookingKey'], $bookingStatusRedis);

                // Webhook
                if (APP_ENV == APP_ENV_PRODUCTION || APP_ENV === APP_ENV_STAGING) {

                    $targetUrl = (APP_ENV === APP_ENV_PRODUCTION) ? TFConst::TS_WEBHOOK_URL : TFConst::TS_WEBHOOK_DEV_URL ;
                    $targetUrl .= TFConst::TS_RESERVE_WEBHOOK_URI;

                    $sleep = (APP_ENV === APP_ENV_PRODUCTION) ? 300 : 3;
                    $maxTryCount = (APP_ENV === APP_ENV_PRODUCTION) ? $this->maxTryCount : 3;
                    $tryCount = 0;

                    $updateParams = [
                        'bookingKey' => $args['bookingKey'],
                        'updateType' => $updateType,
                        'updateAt'   => date('Y-m-d H:i:s')
                    ];

                    do {
                        $result = $this->_webhook($targetUrl, $updateParams, $this->noWait, $sleep);
                        $tryCount++;
                    } while ($result['success'] === false && $tryCount <= $maxTryCount);

                    if ( ! isset($result['success']) || true !== $result['success']) {
                        $bookingStatusRedis['status'] = $oldStatus;
                        $this->_setBookingRedis($args['bookingKey'], $bookingStatusRedis);
                        throw new \Exception('Webhook Error');
                    }

                }

                $results = [
                    'success'          => true,
                    'status'           => $updateType,
                    'bookingKey'       => $bookingStatus['book']['book_cd'],
                    'partnerReference' => $bookingStatusRedis['partnerReference'] ?? null,
                    'totalAmount'      => $bookingStatus['book']['sale_price'],
                    'refundAmount'     => $bookingStatus['book']['sale_price'],
                    'tf_result'        => $bookingStatus,
                ];

                RedisUtil::setDataWithExpire($this->redis, $redisDb, $redisKey, CommonConst::REDIS_SESSION_EXPIRE_TIME_DAY_7, $results);
                $this->_saveBookingLog($args['bookingKey'], $results);

                unset($results['tf_result']);

                $body = '<h1>주문정보 업데이트 처리요청</h1>' . '<p>주문 키 : ' . $args['bookingKey'] . '</p>' . '<p>' . $args['updateType'] . ' 처리 성공</p>';

            } catch (\Exception $ex) {

                $body = '<h1>주문정보 업데이트 처리요청</h1>' . '<p>주문 키 : ' . $args['bookingKey'] . '</p>' . '<p>' . $args['updateType'] . ' 처리 실패</p>';

                $results = [
                    'success'      => false,
                    'status'       => TFConst::STATUS_ERROR,
                    'bookingKey'   => $args['bookingKey'],
                    'message'      => $this->_getErrorMessage($ex),
                ];

                $this->_saveBookingLog($args['bookingKey'], $results);
            }

            try {
                if (false === AwsUtil::sendEmail($this->recipients, '주문정보 업데이트 결과', $body)) {
                    throw new \Exception(null, ErrorConst::ERROR_SEND_EMAIL);
                }
            } catch (\Exception $ex) {

            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);
        }


        /**
         * 트레블포레스트에서 상품 관련 업데이트시 호출할 Webhook
         *
         * @param Request  $request
         * @param Response $response
         * @param          $args
         *
         * @return Response
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        public function productUpdate(Request $request, Response $response, $args)
        {
            try {

                $results = $this->jsonResult;
                $tryCount = 0;
                $maxTryCount = $this->maxTryCount;
                $updateType = null;

                $targetUrl = (APP_ENV === APP_ENV_PRODUCTION) ? TFConst::TS_WEBHOOK_URL : TFConst::TS_WEBHOOK_DEV_URL ;
                $targetUrl .= TFConst::TS_PRODUCT_WEBHOOK_URI;

                $parameters = $request->getAttribute('params');
                switch (true) {
                    case (is_array($parameters) && is_array($args)) :
                        $params = array_merge($parameters, $args);
                        break;
                    case (is_array($parameters) && !empty($parameters)) :
                        $params = $parameters;
                        break;
                    default :
                        $params = $args;
                        break;
                }

                if (false === CommonUtil::validateParams($params, ['productKey', 'updateType'], true)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                switch ($params['updateType']) {
                    case 1 : // 상품추가
                        $updateType = 'ADD';
                        break;
                    case 2 : // 상품삭제
                        $updateType = 'DELETE';
                        break;
                    case 3 : // 업데이트
                        $updateType = 'UPDATE';
                        break;
                }

                if (empty($updateType)) {
                    throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                }

                if ($updateType === 'ADD' || $updateType === 'UPDATE') {
                    if (false === CommonUtil::validateParams($params, ['title'], true)) {
                        throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
                    }
                }

                if (false === ($products = RedisUtil::getData($this->redis, TFConst::REDIS_TF_PRODUCTS_REDIS_KEY, $this->redisSession))) {
                    throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                }

                // 상품검색
                if (false === ($productKeyIdx = array_search($params['productKey'], array_column($products, 'productKey')))) {
                    if ($updateType !== 'ADD') {
                        throw new \Exception('', ErrorConst::ERROR_RDB_NO_DATA_EXIST);
                    }
                }

                $updateParams = [
                    'productKey'     => $params['productKey'],
                    'productTypeKey' => $params['productTypeKey'] ?? null,
                    'updateType'     => $updateType,
                    'updateAt'       => date('Y-m-d H:i:s')
                ];

                // Redis 갱신
                switch ($params['updateType']) {
                    case 1 : // 상품추가
                        $products[] = [
                            'productKey' => $params['productKey'],
                            'title'      => '',
                            'updateAt'   => date('Y-m-d H:i:s')
                        ];
                        break;
                    case 2 : // 상품삭제
                        unset($products[$productKeyIdx]);
                        break;
                    case 3 : // 업데이트
                        $products[$productKeyIdx] = [
                            'productKey' => $updateParams['productKey'],
                            'updateAt'   => $updateParams['updateAt'],
                            'title'      => $params['title'] ?? ($products[$productKeyIdx]['title'] ?? ''),
                        ];
                        break;
                }

                $this->_setRedis(TFConst::REDIS_TF_PRODUCTS_REDIS_KEY, $products);

                // webhook
                do {
                    LogMessage::info('try Count :: ' . $tryCount . ' - noWait :: ' . $this->noWait);
                    $result = $this->_webhook($targetUrl, $updateParams, $this->noWait);
                    $tryCount++;

                } while ($result['success'] === false && $tryCount <= $maxTryCount);

                if ( ! isset($result['success']) || true !== $result['success']) {
                    throw new \Exception('Webhook Error');
                }


            } catch (\Exception $ex) {

                $results = [
                    'result' => false,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];

            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);
        }


        public function getEventKey(Request $request, Response $response)
        {
            $results = $this->jsonResult;

            try {

                $results['data']['event_key'] = $this->_getRedisEventKey([], CommonConst::REDIS_SESSION_EXPIRE_TIME_MIN_10);

            } catch (\Exception $ex) {

                $results = [
                    'result' => false,
                    'data'   => [
                        'message' => $this->_getErrorMessage($ex),
                    ]
                ];
            }

            return $response->withJson($results, ErrorConst::SUCCESS_CODE);

        }


        /**
         * @param      $targetUrl
         * @param      $params
         * @param bool $noWait
         * @param int  $sleep
         *
         * @return array
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        private function _webhook($targetUrl, $params, $noWait = true, $sleep = 1)
        {
            if ($noWait === false) {
                sleep($sleep);
            }

            try {

                $webHookOption = [
                    'verify'          => false,
                    'timeout'         => CommonConst::HTTP_RESPONSE_TIMEOUT,
                    'connect_timeout' => CommonConst::HTTP_CONNECT_TIMEOUT,
                    'headers'         => [
                        'User-Agent'    => 'Synctree/2.1 - ' . APP_ENV,
                        'Access-Token'  => (APP_ENV === APP_ENV_PRODUCTION) ? TFConst::TS_ACCESS_TOKEN : TFConst::TS_DEV_ACCESS_TOKEN,
                        'Supplier-Code' => TFConst::TS_SUPPLIER_CODE,
                        'Content-Type'  => 'application/json',
                        'Accept'        => 'application/json'
                    ],
                    'json' => $params,
                    'allow_redirects' => true,
                ];

                $result = Http::httpRequest($targetUrl, $webHookOption, CommonConst::REQ_METHOD_PUT);

                if ( ! isset($result['success']) || true !== $result['success']) {
                    $this->noWait = false;
                    $result['success'] = false;
                    LogMessage::error('Webhook Error :: ' . json_encode($result, JSON_UNESCAPED_UNICODE));
                }

            } catch (\Exception $ex) {
                LogMessage::error('Webhook Error :: ' . $ex->getMessage());
                $result['success'] = false;
            }

            return $result;

        }


        /**
         * 신청 가능 날짜
         *
         * @param $from
         * @param $to
         * @param $arrivalDate
         *
         * @return bool
         */
        private function _validRangeDate($from, $to, $arrivalDate)
        {
            $tourDateFrom = date('Y-m-d', strtotime($from));
            $tourDateTo = date('Y-m-d', strtotime($to));
            $arrivalDate = date('Y-m-d', strtotime($arrivalDate));

            if ($arrivalDate < $tourDateFrom || $arrivalDate > $tourDateTo) {
                return false;
            }

            return true;
        }


        /**
         * 트레블 포레스트 지역 정보 패치
         *
         * @return array|bool|mixed|string
         * @throws \GuzzleHttp\Exception\GuzzleException
         */
        private function _getArea()
        {
            $redisKey = TFConst::REDIS_TF_AREA_REDIS_KEY;

            if (false === ($area = RedisUtil::getData($this->redis, $redisKey, $this->redisSession))) {
                $targetUrl = TFConst::TF_URL . TFConst::TF_GET_AREA_URI;
                $area = Http::httpRequest($targetUrl, $this->guzzleOpt);
                $this->_setRedis($redisKey, $area);
            }

            return $area;
        }


        /**
         *
         * @param           $redisKey
         * @param           $data
         * @param float|int $expire
         */
        private function _setRedis($redisKey, $data, $expire = CommonConst::REDIS_SESSION_EXPIRE_TIME_DAY_2)
        {
            $redisSession = TFConst::REDIS_TF_SESSION;

            try {
                //RedisUtil::setDataWithExpire($this->redis, $redisSession, $redisKey, $expire, $data);
                RedisUtil::setData($this->redis, $redisSession, $redisKey, $data);
            } catch (\Exception $ex) {
                LogMessage::error('TravelForest - Set Redis Error (' . $redisKey . ')');
            }
        }

        private function _setBookingRedis($bookKey, $data, $expire = CommonConst::REDIS_SESSION_EXPIRE_TIME_DAY_7)
        {
            $result = true;

            $redisDb = CommonConst::REDIS_MESSAGE_SESSION;
            $redisKey = TFConst::REDIS_TF_BOOKING_REDIS_KEY . '_' . $bookKey;

            try {
                $result = RedisUtil::setDataWithExpire($this->redis, $redisDb, $redisKey, $expire, $data);
            } catch (\Exception $ex) {
                LogMessage::error('TravelForest - Set Redis Error (' . $redisKey . ')');
            }

            return $result;

        }

        private function _saveBookingLog($bookingIdx, $contents = [])
        {

            $result = true;

            try {

                $curDateTime = $timestamp ?? date('Y-m-d H:i:s');
                $contents['timestamp'] = $curDateTime;
                $contents = json_encode($contents, JSON_UNESCAPED_UNICODE);

                $filePath = BASE_DIR . '/logs/biz/';
                $fileName = 'TFbooking.' . $bookingIdx . '.log';
                $file = $filePath . $fileName;

                $logfile = fopen($file, 'a');
                fwrite($logfile, $contents . "\n\n");
                fclose($logfile);

                $s3FileName = date('Y/m/d', strtotime($curDateTime));
                $s3FileName .= '/' . $fileName;

                if (true === (AwsUtil::s3FileUpload($s3FileName, $file, 's3Log'))) {
                    @unlink($file);
                }

            } catch (\Exception $ex) {
                $result = false;
            }

            return $result;
        }

        /**
         * @param $str
         *
         * @return mixed
         */
        private function _replaceSpecialChar($str)
        {
            return str_replace(['&GT;', '&gt;', '&lt;'], ['>', '>', '<'], $str);

        }

        /**
         * @param $tf_status
         *
         * @return string
         */
        private function _getStatusCode($tf_status)
        {
            switch ($tf_status) {
                case TFConst::TF_BOOK_CD_ORD :
                case TFConst::TF_BOOK_CD_REQ :
                case TFConst::TF_BOOK_CD_INS :
                    $status = TFConst::RESERVE_STATUS_RESV;
                    break;
                case TFConst::TF_BOOK_CD_BOK :
                    $status = TFConst::RESERVE_STATUS_WAIT;
                    break;
                case TFConst::TF_BOOK_CD_CFM :
                    $status = TFConst::RESERVE_STATUS_CFRM;
                    break;
                case TFConst::TF_BOOK_CD_CLS :
                case TFConst::TF_BOOK_CD_CCL :
                    $status = TFConst::RESERVE_STATUS_CNCL;
                    break;
                default :
                    $status = null;
            }

            return $status;

        }


    }<file_sep><?php
/**
 * 생성된 파일의 부모 클래스
 * 이 클래스는 추상 클래스를 부모로 둔다.
 *
 * @author kimildo
 */


namespace controllers\generated;

use Slim\Container;
use Slim\Http\Request;
use Slim\Http\Response;
use abstraction\classes\SynctreeAbstract;
use libraries\{constant\CommonConst,
    constant\ErrorConst,
    log\LogMessage,
    util\CommonUtil,
    util\RedisUtil,
    util\AwsUtil};

class Synctree extends SynctreeAbstract
{
    protected $ci;
    protected $logger;
    protected $renderer;
    protected $redis;
    protected $jsonResult;
    protected $httpClient;
    protected $promise;
    protected $promiseResponseData;
    protected $httpReqTimeout = 3;
    protected $httpReqVerify = false;


    /**
     * Synctree constructor.
     *
     * @param Container $ci
     *
     * @throws \Interop\Container\Exception\ContainerException
     */
    public function __construct(Container $ci)
    {
        $this->ci = $ci;

        try {
            $this->ci = $ci;
            $this->logger = $ci->get('logger');
            $this->renderer = $ci->get('renderer');
            $this->redis = $ci->get('redis');
        } catch (\Exception $ex) {
            LogMessage::error($ex->getMessage());
        }

        $this->jsonResult = [
            'result' => ErrorConst::SUCCESS_CODE,
            'data'   => [
                'message' => '',
            ]
        ];
    }

    /**
     * @param Request  $request
     * @param Response $response
     *
     * @return Response
     */
    public function getCommand(Request $request, Response $response)
    {
        $results = $this->jsonResult;

        try {

            $params = $request->getAttribute('params');
            if (false === CommonUtil::validateParams($params, ['event_key'])) {
                throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
            }

            // get redis data
            if (false === ($redisData = RedisUtil::getDataWithDel($this->redis, $params['event_key'], CommonConst::REDIS_SECURE_PROTOCOL_COMMAND))) {
                throw new \Exception(null, ErrorConst::ERROR_RDB_NO_DATA_EXIST);
            }

            $results['data'] = $redisData;

        } catch (\Exception $ex) {
            $results = $this->_getErrorMessage($ex);
        }

        return $response->withJson($results, ErrorConst::SUCCESS_CODE);
    }


    /**
     * 역방향 보안프로토콜을 위한 리스너
     *
     * @param $params
     *
     * @return bool
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    protected function _eventListener($params)
    {
        $result = false;

        if (false === CommonUtil::validateParams($params, ['target_url', 'event_key'])) {
            return $result;
        }

        $options = [
            'verify' => $this->httpReqVerify,
            'timeout' => $this->httpReqTimeout,
            'json' => ['event_key' => $params['event_key']]
        ];

        $result = $this->_httpRequest($params['target_url'], $options);

        $resStatus = $result['res_status'];
        LogMessage::info('_eventListener :: ' . $resStatus);

        return $result;
    }

    /**
     * guzzle request
     *
     * @param        $targetUrl
     * @param        $options
     * @param string $method
     *
     * @return array
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    protected function _httpRequest($targetUrl, $options, $method = 'POST')
    {
        $resData = null;
        $resStatus = null;

        try {

            if (empty($this->httpClient) || ! is_object($this->httpClient)) {
                $this->httpClient = new \GuzzleHttp\Client();
            }

            $ret = $this->httpClient->request($method, $targetUrl, $options);
            $resData = $ret->getBody()->getContents();
            $resData = strip_tags($resData);
            $resData = CommonUtil::getValidJSON($resData);
            $resStatus = $ret->getStatusCode() . ' ' . $ret->getReasonPhrase();

        } catch (\GuzzleHttp\Exception\ServerException $e) {
            preg_match('/`(5[0-9]{2}[a-z\s]+)`/i', $e->getMessage(), $output);
            $resStatus = $output[1];
            LogMessage::error('url :: ' . $targetUrl . ', error :: ' . $resStatus . ', options :: ' . json_encode($options, JSON_UNESCAPED_UNICODE));
        } catch (\GuzzleHttp\Exception\ClientException $e) {
            preg_match('/`(4[0-9]{2}[a-z\s]+)`/i', $e->getMessage(), $output);
            $resStatus = $output[1];
            LogMessage::error('url :: ' . $targetUrl . ', error :: ' . $resStatus);
        } catch (\Exception $e) {
            $resStatus = "Name or service not known";
            LogMessage::error('url :: ' . $targetUrl . ', error :: ' . $resStatus);
        }

        return [
            'res_status' => $resStatus,
            'res_data'   => $resData,
        ];

    }

    /**
     * 비동기 호출
     *
     * @param array $asyncRequests
     * @param int $concurrency
     * @param bool $wait
     *
     * @return array
     *
     * $domainArr = [
            [
            'url' => 'https://3069d955-08a7-4052-8f61-a83f488a32a6.mock.pstmn.io/getJson',
            'method' => 'POST'
            ],
            [
            'url' => 'https://www.naver.com/efwefwe',
            'method' => 'POST'
            ],
            [
            'url' => 'https://www.daum.net/ffff',
            'method' => 'GET'
            ],
            [
            'url' => 'https://3069d955-08a7-4052-8f61-a83f488a32a6.mock.pstmn.io/getJson',
            'method' => 'GET'
            ]
       ];
        $responseDatas['async'] = $this->_httpAsyncRequest($domainArr);
     */
    protected function _httpAsyncRequest(array $asyncRequests, $wait = true, $concurrency = 5)
    {
        $this->promiseResponseData = [];

        try {

            if (empty($this->httpClient) || ! is_object($this->httpClient)) {
                $this->httpClient = new \GuzzleHttp\Client();
            }

            if (true === $wait) {

                $requestPromises = function ($targets) {
                    foreach ($targets as $target) {
                        yield function() use ($target) {
                            return $this->httpClient->requestAsync($target['method'], $target['url'], $target['options'] ?? [] );
                        };
                    }
                };

                $pool = new \GuzzleHttp\Pool($this->httpClient, $requestPromises($asyncRequests), [
                    'concurrency' => $concurrency,
                    'fulfilled'   => function ($response, $index) {
                        // this is delivered each successful response
                        $resData = json_decode($response->getBody()->getContents(), true);
                        $this->promiseResponseData['seq'][] = $index;
                        $this->promiseResponseData['data'][] = $resData;

                        $generatedFile = fopen('/home/ubuntu/apps/secure/upload/' . $index . '.txt', 'w');
                        fwrite($generatedFile, json_encode($resData, JSON_UNESCAPED_UNICODE));
                        fclose($generatedFile);

                    },
                    'rejected'    => function ($reason, $index) {
                        // this is delivered each failed request
                    },
                ]);

                $promise = $pool->promise();
                $promise->wait();

            } else {

                foreach ($asyncRequests as $index => $target) {
                    $this->promiseResponseData['seq'][] = $index;
                    $promise[] = $this->httpClient->requestAsync($target['method'], $target['url'], $target['options'] ?? [] );
                }
                //$pool = new \GuzzleHttp\Pool($this->httpClient, $requestPromises($asyncRequests));
            }

            //CommonUtil::showArrDump($promise);

        } catch (\Exception $e) {

        }

        return $this->promiseResponseData;

    }


    /**
     * 로그 S3 업로드
     * @todo 추후 로그서버를 별도로 구축, 비동기로 전환
     *
     * @param $fileName
     * @param $contents
     * @param $appId
     * @param $bizId
     *
     * @return bool
     */
    protected function _saveLog($fileName, $contents, $appId, $bizId)
    {
        try {

            $filePath = BASE_DIR . '/logs/biz/';
            $fileName = $fileName . CommonUtil::getMicroTime() . '-' . APP_ENV . '.log';
            $file = $filePath . $fileName;

            $logfile = fopen($file, 'w');
            fwrite($logfile, $contents);
            fclose($logfile);

            $s3FileName = date('Y/m/d');
            $s3FileName .= '/' . $appId . '/' . $bizId . '/' . $fileName;

            if (APP_ENV === APP_ENV_PRODUCTION) {
                if (true === ($s3Result = AwsUtil::s3FileUpload($s3FileName, $file, 's3Log'))) {
                    @unlink($file);
                    return true;
                }
            }

            $result = true;

        } catch (\Exception $ex) {
            $result = false;
        }

        return $result;


    }

    /**
     * 에러메세지 출력
     *
     * @param \Exception $ex
     *
     * @return array
     */
    protected function _getErrorMessage(\Exception $ex)
    {
        $results = [
            'result' => ErrorConst::FAIL_CODE,
            'data'   => [
                'message' => CommonUtil::getErrorMessage($ex),
            ]
        ];

        return $results;
    }


}
<file_sep><?php

$app->group('/synctree-tf', function () {

    $this->get('/getProducts', 'controllers\internal\TravelForest:getProducts')->setName('product'); // 모상품리스트
    $this->post('/getProduct', 'controllers\internal\TravelForest:getProduct')->setName('product'); // 모상품 상세정보
    $this->post('/getProduct/{subProduct}', 'controllers\internal\TravelForest:getProduct')->setName('product'); // 자상품 상세 정보
    $this->post('/authCheck', 'controllers\internal\TravelForest:authCheck')->setName('auth'); // 권한 체크
    $this->get('/getProductIdx/{productTypeKey:[0-9]+}', 'controllers\internal\TravelForest:getProductIdx')->setName('product'); // 자상품 IDX로 모상품 IDX 가지고 온다.

    $this->get('/setProductsBatch', 'controllers\internal\TravelForest:setProductsBatch')->setName('batch'); // TF 상품 배치
    $this->get('/setProductBatch/{area_idx}/{product_idx}', 'controllers\internal\TravelForest:setProductBatch')->setName('batch');

    $this->post('/addBooking', 'controllers\internal\TravelForest:addBooking')->setName('booking'); // 예약생성 메퍼
    $this->get('/proceedBooking/{bookingKey:[0-9]+}', 'controllers\internal\TravelForest:proceedBooking')->setName('booking'); // 예약진행 매퍼
    $this->get('/cancelBooking/{bookingKey:[0-9]+}', 'controllers\internal\TravelForest:cancelBooking')->setName('booking'); // 예약취소 매퍼
    $this->get('/checkBookingStatus/{bookingKey:[0-9]+}', 'controllers\internal\TravelForest:checkBookingStatus')->setName('booking'); // 예약진행 매퍼

    $this->put('/bookingUpdate/{bookingKey:[0-9]+}/{updateType:[a-z]+}', 'controllers\internal\TravelForest:bookingUpdate')->setName('booking'); // 예약 Webhook

    $this->put('/productUpdate/{productKey:[0-9]+}', 'controllers\internal\TravelForest:productUpdate')->setName('booking'); // 상품 Webhook
    $this->put('/productUpdate/{productKey:[0-9]+}/{productTypeKey:[0-9]+}', 'controllers\internal\TravelForest:productUpdate')->setName('booking'); // 상품 Webhook

    $this->post('/secure/getCommand', 'controllers\internal\TravelForest:getCommand')->setName('getCommand'); // 보안 프로토콜

    if (APP_ENV !== APP_ENV_PRODUCTION) {
        $this->get('/secure/getEventKey', 'controllers\internal\TravelForest:getEventKey')->setName('getCommand'); // 보안 프로토콜 이벤트키 발급
    }

    $this->group('/tourvis/api', function () {

        switch (APP_ENV) {
            case APP_ENV_DEVELOPMENT_LOCAL_KIMILDO :
            case APP_ENV_DEVELOPMENT :

                // 모상품 목록
                $this->get('/v1/{supplierCode}/products', 'controllers\generated\usr\owner_nntuple_com\GenerateffUecF43IcM1m5cBrAGXHkController:main')->setName('main');

                // 모상품 상세
                $this->get('/v1/{supplierCode}/products/{productKey:[0-9]+}',
                    'controllers\generated\usr\owner_nntuple_com\GenerateuONxfxovEnkAZvMFCAWBzlController:main')->setName('main');

                // 자상품 리스트
                $this->get('/v1/{supplierCode}/products/{productKey:[0-9]+}/{productTypes:[a-z\-]{13}}',
                    'controllers\generated\usr\owner_nntuple_com\GenerateHRNHfBC1C0ccaCzCxfvddbController:main')->setName('main');

                // 자상품 상세
                $this->get('/v1/{supplierCode}/{productTypes:[a-z\-]{13}}/{productTypeKey:[0-9]+}',
                    'controllers\generated\usr\owner_nntuple_com\Generate47p9A8I1VuMguPA5Cz0xa8Controller:main')->setName('main');

                // 자상품 날짜 검색
                $this->get('/v1/{supplierCode}/{productTypes:[a-z\-]{13}}/{productTypeKey:[0-9]+}/price',
                    'controllers\generated\usr\owner_nntuple_com\GeneratelLmX6Q5SqAABaMDRTGqaNnController:main')->setName('main');

                // 예약생성
                $this->post('/v1/{supplierCode}/book',
                    'controllers\generated\usr\owner_nntuple_com\GenerateKdEx1crXzerSfj8dWr9pmmController:main')->setName('main');

                // 예약진행
                $this->put('/v1/{supplierCode}/book/{bookingKey:[0-9]+}/confirm',
                    'controllers\generated\usr\owner_nntuple_com\GenerateXmKWZuZgxTAMz9RVM3DcgyController:main')->setName('main');

                // 예약취소
                $this->put('/{supplierCode}/book/{bookingKey:[0-9]+}/cancel',
                    'controllers\generated\usr\owner_nntuple_com\GenerateM9GXOBdtvQRA8A1clW15BHController:main')->setName('main');

                // 예약상태 확인
                $this->get('/{supplierCode}/book/{bookingKey:[0-9]+}',
                    'controllers\generated\usr\owner_nntuple_com\GeneratevXEhAKkdIJQL6HBpSLMOALController:main')->setName('main');

                break;

            case APP_ENV_STAGING :
            case APP_ENV_PRODUCTION :

                // 모상품 목록
                $this->get('/v1/{supplierCode}/products', 'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateekbjUdv93ES7MfWKZgNM99Controller:main')->setName('main');

                // 모상품 상세
                $this->get('/v1/{supplierCode}/products/{productKey:[0-9]+}',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateWZpYpTdzbqVYdtXfSACyMXController:main')->setName('main');

                // 자상품 리스트
                $this->get('/v1/{supplierCode}/products/{productKey:[0-9]+}/{productTypes:[a-z\-]{13}}',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateUsR8n8zTC0YG7fr41ozpDbController:main')->setName('main');

                // 자상품 상세
                $this->get('/v1/{supplierCode}/{productTypes:[a-z\-]{13}}/{productTypeKey:[0-9]+}',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\Generatep8bnPaFrXRD18rQnu3eIu8Controller:main')->setName('main');

                // 자상품 날짜 검색
                $this->get('/v1/{supplierCode}/{productTypes:[a-z\-]{13}}/{productTypeKey:[0-9]+}/price',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateGkqiAEHnbFsBhwSqxzkJ74Controller:main')->setName('main');

                // 예약생성
                $this->post('/v1/{supplierCode}/book',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\Generate2ikG4ygrAGw2znVr8hdq9vController:main')->setName('main');

                // 예약진행
                $this->put('/v1/{supplierCode}/book/{bookingKey:[0-9]+}/confirm',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GeneratebzjSIazKVCVMmtSG92ZpP0Controller:main')->setName('main');

                // 예약취소
                $this->put('/{supplierCode}/book/{bookingKey:[0-9]+}/cancel',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateTSQwk0ViFINl2i7BcfJ7vAController:main')->setName('main');

                // 예약상태 확인
                $this->get('/{supplierCode}/book/{bookingKey:[0-9]+}',
                    'controllers\generated\usr\tony_oh_travelforest_co_kr\GenerateTbURRHd5VhbdVBQH30A9b2Controller:main')->setName('main');


                break;
        }

    });



})
->add(new \middleware\Common($app->getContainer(), false))
;<file_sep># Synctree Studio

## 기본 컨셉
> - 앱이 최상위 단위이고 계정마다 여러개를 가질 수 있다.
> - 비즈유닛은 앱에 속한다. 비즈유닛간의 연계성은 없다.
> - 비즈유닛은 한개의 Endpoint를 갖는다.
> - 오퍼레이터는 통신의 기본단위 개념이다. 팀에 속하며 비즈유닛에 바인딩 시킬 수 있다.
> - 비즈유닛 안에서 오퍼레이터는 여러개 존재할 수 있으며 오퍼레이터 간 릴레이를 설정할 수 있다.
> - 오퍼레이터의 속성에 통신할 엔드포인트 및 Request/Response 변수를 지정 할 수 있다.
> - 앱, 비즈유닛, 오퍼레이터의 정보값은 DB에 있고, 이를 조합해 Redis에 메타데이터를 생성해 갖고 있다.
> - 비유닛을 저장하면 디비와 Redis의 정보를 갱신하고, 빌드하면 해당 메타데이터를 통해 파일을 생성한다.
> - CodeDeploy를 통해 컨슈머의 서버에 배포한다.
> - 생성된 비즈유닛이 호출될때 S3에 로깅파일을 업로드 한다.

## 개발 기능
> - 비즈유닛에 오퍼레이터를 바인딩하고 오퍼레이터 간의 변수 연계 지원
> - 비즈유닛 파일 생성 기능
> - 오퍼레이터 JSON 형식 지원
> - 오퍼레이터 통신형식 형식 지원
> - 오퍼레이터 보안프로토콜 적용
> - 오퍼레이터 바인딩시 Authorization 적용
> - ALT 컨트롤 기능 (오퍼레이터 switch case)
> - PHP Class, route 파일 생성 및 배포 기능
> - Async
> - Loop (예정)
> - 오퍼레이터의 변수 require field 기능
<file_sep><?php

$app->group('/GenbPk6Bidk0jrzhNQkKprzhs', function () {

    $this->post('/secure/getCommand', 'controllers\generated\usr\owner_nntuple_com\GeneratebPk6Bidk0jrzhNQkKprzhsController:getCommand')->setName('getCommand');
    $this->get('', 'controllers\generated\usr\owner_nntuple_com\GeneratebPk6Bidk0jrzhNQkKprzhsController:main')->setName('main');
    $this->get('/', 'controllers\generated\usr\owner_nntuple_com\GeneratebPk6Bidk0jrzhNQkKprzhsController:main')->setName('main');

})->add(new \middleware\Common($app->getContainer(), APP_ENV === APP_ENV_PRODUCTION));<file_sep><?php
/**
 * Studio PHP Generate Engine
 *
 * @author kimildo
 * @see https://packagist.org/packages/nette/php-generator
 *
 */

namespace libraries\util;

use libraries\{
    constant\CommonConst,
    constant\GeneratorConst,
    util\CommonUtil,
    log\LogMessage
};

use Nette\PhpGenerator as PGen;

class GenerateUtil
{
    private $config;
    private $fileObj;
    private $fileInfo;
    private $params;
    private $userPath;
    private $class;
    private $error = false;
    private $ci;

    /**
     * GenerateUtil constructor.
     *
     * @param       $ci
     * @param array $params
     */
    public function __construct($ci, $params = [])
    {
        $configFile = include APP_DIR . 'config/' . APP_ENV . '.php';
        $this->config = $configFile['settings']['home_path'] ?? null;

        $this->fileObj = new PGen\PhpFile;
        $this->fileObj->addComment('This file is Created by Ntuple GeneratedEngine.');
        $this->fileObj->addComment(date('Y-m-d H:i:s'));

        $this->params = $params;
        $this->userPath = str_replace(['@', '.'], '_', $params['user_id']);

        $this->ci = $ci;
    }

    /**
     * Controller 파일 생성
     *
     * @return $this
     * @throws \Exception
     */
    public function setControllerFile()
    {
        if (!empty($this->error)) {
            return $this;
        }

        $className = str_replace('.php', '', $this->fileInfo['file_name']);

        $namespace = $this->fileObj->addNamespace('controllers\\generated\\usr\\' . $this->userPath);
        $namespace
            ->addUse('Slim\Http\Request')
            ->addUse('Slim\Http\Response')
            ->addUse('Slim\Container')
            ->addUse('libraries\constant\CommonConst')
            ->addUse('libraries\constant\ErrorConst')
            ->addUse('libraries\log\LogMessage')
            ->addUse('libraries\util\CommonUtil')
            ->addUse('libraries\util\RedisUtil')
            ->addUse('controllers\generated\Synctree')
            ->addUse('Ramsey\Uuid\Uuid')
            ->addUse('Ramsey\Uuid\Exception\UnsatisfiedDependencyException')
        ;

        $this->class = $namespace->addClass($className)->setExtends('Synctree');
        $this->class->addConstant('BIZOPS', json_encode($this->params, JSON_UNESCAPED_UNICODE));
        //$class->addConstant('BUNIT_TOKEN', $this->params['token']);

        $this->class->addProperty('eventKey')->setVisibility('private');
        $this->class->addProperty('resultParams')->setVisibility('private');
        $this->class->addProperty('params')->setVisibility('private');

        $this->class->addMethod('__construct')->setBody('parent::__construct($ci);')->addParameter('ci')->setTypeHint('Container');

        $mainMethod = $this->class->addMethod(GeneratorConst::GEN_MAIN_METHOD_NAME);
        $mainBody = <<<'SOURCE'
$result = [];
$result['result'] = ErrorConst::SUCCESS_STRING;
$startTime = CommonUtil::getMicroTime();
$result['timestamp']['start'] = CommonUtil::getDateTime() . ' ' . $startTime;
SOURCE;

        $mainBody .= PHP_EOL . PHP_EOL . 'try {' . PHP_EOL;
        $mainBody .= '    $this->params = $request->getAttribute(\'params\');';
        $mainBody .= PHP_EOL . PHP_EOL;

        //$request = ['token'];
        $request = [];
        $requestEncode = '';
        foreach ($this->params['request'] as $req) {
            $request[] = $req['req_key'];
            if ($req['req_var_type'] === CommonConst::VAR_TYPE_JSON) {
                $requestEncode .= '$this->params[\'' . $req['req_key'] . '\'] = CommonUtil::getValidJSON($this->params[\'' . $req['req_key'] . '\']);' . PHP_EOL . PHP_EOL;
            }
        }

        $request = "'" . implode("', '", $request) . "'";
        $mainBody .= '    if (false === CommonUtil::validateParams($this->params, [' . $request . '])) {' . PHP_EOL;
        $mainBody .= <<<'SOURCE'
        LogMessage::error('Not found required field');
        throw new \Exception(null, ErrorConst::ERROR_NOT_FOUND_REQUIRE_FIELD);
    }
    
    $bizOps = json_decode(static::BIZOPS, true);
    
	$responseDatas = [];
    $result['request'] = [
        'actor_alias' => $bizOps['actor_alias'] ?? null,
        'request' => $this->params,
    ];
    
SOURCE;

        $mainBody .= $requestEncode;

        $mainBodyArr = [];
        $controlInfoArr = [];

        // 모든 오퍼레이터 서브메소드 생성
        foreach ($this->params['operators'] as $opSeq => $row) {

            $bindSeq = $opSeq;
            $controlInfo = null;
            if (!empty($row['control_container_code'])) {
                $controlInfo = json_decode($row['control_container_info'], true);
                $controlIndex = array_search($controlInfo['control_id'], array_column($this->params['controls'], 'control_alt_id'));
                $bindSeq = $this->params['controls'][$controlIndex]['binding_seq'];
            }

            $subMethodName = GeneratorConst::GEN_SUB_METHOD_NAME . strtoupper($row['operation_key']) . $opSeq;
            $resSourceText = '    $responseDatas[] = $this->resultParams = $this->' . $subMethodName . '($request, $response);';
            if (!empty($row['control_container_code'])) {
                $mainBodyArr[$bindSeq][$opSeq] = $resSourceText;
                $controlInfoArr[$opSeq] = $controlInfo;
            } else {
                $mainBodyArr[$opSeq] = $resSourceText;
            }

            if (false === $this->_makeSubfuntion($subMethodName, $row, $opSeq)) {
                $this->error = 'Error While Make Subfunction';
                LogMessage::error($this->error);
                return $this;
            }

        } // end of $operation foreach


        // 메인메소드 조건문 or Response 호출 생성
        $controlOperators = CommonConst::CONTROLL_OPERATORS;
        foreach ($mainBodyArr as $key => $row) {
            // if alt
            if (is_array($row)) {

                $mainBody .= PHP_EOL . '    switch (true) {' . PHP_EOL;
                foreach ($row as $opSeq => $source) {

                    $controlInfo = $controlInfoArr[$opSeq];
                    $controlIndex = array_search($controlInfo['control_id'], array_column($this->params['controls'], 'control_alt_id'));

                    $parameterKeyName = $this->params['controls'][$controlIndex]['parameter_key_name'];
                    $parameterJson = (!empty($this->params['controls'][$controlIndex]['sub_parameter_path']))
                                   ? $this->_jsonPathToArrayString($this->params['controls'][$controlIndex]['sub_parameter_path']) : '';

                    $parameterFrom = (!empty($this->params['controls'][$controlIndex]['biz_ops_id'])) ? '$params' : '$this->resultParams[\'response\']';
                    $targetValue = (is_int($controlInfo['value'])) ? (int)$controlInfo['value'] : '\'' . $controlInfo['value'] . '\'';

                    //if ($key === array_key_last($row)) {
                    //    $mainBody .= '        default : ' . PHP_EOL . '      ' . $source . PHP_EOL;
                    //} else {
                        $mainBody .= '        case ('. $parameterFrom .'[\'' . $parameterKeyName . '\']' . $parameterJson . ' ' . $controlOperators[$controlInfo['operator']]
                                  . ' ' . $targetValue . ') :'  . PHP_EOL
                                  . '          ' . $source . PHP_EOL
                                  . '              break;' . PHP_EOL
                                  ;
                    //}
                }

                $mainBody .= '        default : ' . PHP_EOL . '      ' . PHP_EOL;
                $mainBody .= '    }' . PHP_EOL;

            } else {
                $mainBody .= $row . PHP_EOL;
            }
        }

        $mainBody .= <<<'SOURCE'
            
    $result['data'] = $responseDatas;

} catch (\Exception $ex) {
    $result = $this->_getErrorMessage($ex);
}

$endTime = CommonUtil::getMicroTime();
$result['timestamp']['end'] = CommonUtil::getDateTime() . ' ' . $endTime;
$result['timestamp']['runtime'] = $endTime - $startTime;

try {
    $this->_saveLog('logBiz-' . $bizOps['biz_id'] . '-access', json_encode($result, JSON_UNESCAPED_UNICODE), $bizOps['app_id'], $bizOps['biz_id']);
} catch (\Exception $ex) {
    LogMessage::error('Save Log Fail (biz:: ' . $bizOps['biz_id'] . ') - ' . json_encode($this->_getErrorMessage($ex), JSON_UNESCAPED_UNICODE));
}

unset($result['timestamp']);
unset($result['result']);

return $response->withJson($result, ErrorConst::SUCCESS_CODE);
SOURCE;

        $mainMethod->addComment($this->params['biz_name'] . " Main Method\n");

        $mainMethod->setBody($mainBody);
        $mainMethod->addParameter('request')->setTypeHint('Request');
        $mainMethod->addParameter('response')->setTypeHint('Response');

        $mainMethod->addComment("@param Request \$request\n");
        $mainMethod->addComment("@param Response \$response\n");

        if ($this->params['req_method'] == CommonConst::REQ_METHOD_GET_STR) {
            $mainMethod->addParameter('args')->setTypeHint('array');
            $mainMethod->addComment("@param args \$args\n");
        }

        $mainMethod->addComment("@return Response\n");
        $mainMethod->addComment("@throws \\Exception\n");

        return $this;

    }

    /**
     * 라우트 파일 소스 생성
     *
     * @return $this
     */
    public function setRouteFile()
    {
        if (!empty($this->error)) {
            return $this;
        }

        //LogMessage::debug('fileInfo :: ' . json_encode($this->fileInfo));
        $sourceText = '<?php' . PHP_EOL . PHP_EOL;
        $sourceText .= '$app->group(\'/Gen' . $this->params['biz_uid'] . '\', function () {' . PHP_EOL;
        $sourceText .= PHP_EOL . '    $this->post(\'' . CommonConst::GET_COMMAND_URL . '\', \'controllers\\generated\\usr\\' . $this->userPath . '\\'
            . $this->fileInfo['template_name'] . $this->params['biz_uid'] . 'Controller:getCommand\')->setName(\'getCommand\');';

        /*
        $sourceText .= PHP_EOL . '    $this->' . ($this->params['req_method'] == 'P' ? 'post' : 'get') . '(\'';
        if ($this->params['req_method'] == 'G' && !empty($this->params['request'])) {
            foreach ($this->params['request'] as $row) {
                $sourceText .= '/{' . $row['req_key'] . '}';
            }
        }
        $sourceText .= '/{token}\', ';
        */

        $reqMethod = ($this->params['req_method'] === CommonConst::REQ_METHOD_GET_CODE) ? CommonConst::REQ_METHOD_GET : CommonConst::REQ_METHOD_POST ;
        $reqMethod = strtolower($reqMethod);

        $sourceText .= PHP_EOL . '    $this->' . $reqMethod . '(\'\', ';
        $sourceText .= '\'controllers\\generated\\usr\\' . $this->userPath . '\\' . $this->fileInfo['template_name'] . $this->params['biz_uid']
            . 'Controller:'. GeneratorConst::GEN_MAIN_METHOD_NAME .'\')->setName(\''. GeneratorConst::GEN_MAIN_METHOD_NAME .'\');';
        $sourceText .= PHP_EOL . '    $this->' . $reqMethod . '(\'/\', ';
        $sourceText .= '\'controllers\\generated\\usr\\' . $this->userPath . '\\' . $this->fileInfo['template_name'] . $this->params['biz_uid']
            . 'Controller:'. GeneratorConst::GEN_MAIN_METHOD_NAME .'\')->setName(\''. GeneratorConst::GEN_MAIN_METHOD_NAME .'\');' . PHP_EOL;

        $sourceText .= PHP_EOL . '})->add(new \middleware\Common($app->getContainer(), APP_ENV === APP_ENV_PRODUCTION));';

        $this->fileObj = $sourceText;

        return $this;
    }


    /**
     * 파일 이름 얻기
     *
     * @param string $type
     * @param string $templateName
     *
     * @return $this|bool
     */
    public function getFileName($type = 'CONTROLLER', $templateName = GeneratorConst::GEN_FILE_PREFIX)
    {

        try {

            $filePath = $this->config . GeneratorConst::PATH_RULE[$type]['PATH'] . $this->userPath . '/';
            $fileName = ucfirst(strtolower($templateName)) . $this->params['biz_uid'] . GeneratorConst::PATH_RULE[$type]['SUBFIX'];

            if ( ! file_exists($filePath)) {
                mkdir($filePath, 0755, true);
            }

            $this->fileInfo = [
                'file_path'     => $filePath,
                'file_name'     => $fileName,
                'template_name' => $templateName
            ];

        } catch (\Exception $ex) {
            $this->error = 'Fail to get filename';
            LogMessage::error($this->error);
        }

        return $this;
    }

    /**
     * 파일쓰기
     *
     * @param string $mode
     *
     * @return string
     */
    public function writeFile($mode = 'w')
    {
        try {

            if (!empty($this->error)) {
                throw new \Exception($this->error);
            }

            $fileContents = $this->_refining($this->fileObj);
            $generatedFile = fopen($this->fileInfo['file_path'] . $this->fileInfo['file_name'], $mode);
            fwrite($generatedFile, $fileContents);
            fclose($generatedFile);

        } catch (\Exception $ex) {
            LogMessage::error('Fail to write file :: ' . $this->fileInfo['file_path'] . $this->fileInfo['file_name']);
            return false;
        }

        return $this->fileInfo['file_name'];
    }

    public function exportBizUnit()
    {
        return false;
    }

    /**
     * 오퍼레이션에 따른 서브메소드 생성
     *
     * @param $methodName
     * @param $op
     * @param $bindingSeq
     *
     * @return bool
     * @throws \Exception
     */
    private function _makeSubfuntion($methodName, $op, $bindingSeq)
    {
        $subBody = '';
        $subBody .= '$targetUrl = \'' . $op['target_url'] . (( ! empty($op['target_method'])) ? '/' . $op['target_method'] : '') . '\';' . PHP_EOL;

        switch ($op['req_method']) {
            case CommonConst::REQ_METHOD_POST_STR :
                $opReqType = CommonConst::REQ_METHOD_POST;
                $opReqTypeVar = 'form_params';
                break;
            default :
                $opReqType = CommonConst::REQ_METHOD_GET;
                $opReqTypeVar = 'query';
        }

        switch ($op['header_transfer_type_code']) {
            case CommonConst::HTTP_HEADER_CONTENTS_TYPE_JSON_CODE :
                $opReqTypeVar = strtolower(CommonConst::VAR_TYPE_JSON_TEXT);
                break;
            case CommonConst::HTTP_HEADER_CONTENTS_TYPE_WWW_FORM_URLENCODED_CODE :
                $subBody .= '$options[\'headers\'] = [\'Content-Type\' => \''. CommonConst::HTTP_HEADER_CONTENTS_TYPE_WWW_FORM_URLENCODED_STR .'\'];' . PHP_EOL;
                break;
            case CommonConst::HTTP_HEADER_CONTENTS_TYPE_XML_CODE :
                $subBody .= '$options[\'headers\'] = [\'Content-Type\' => \''. CommonConst::HTTP_HEADER_CONTENTS_TYPE_XML_STR .'\'];' . PHP_EOL;
                $opReqTypeVar = 'body';
                break;
        }

        if ( ! empty($op['auth_type_code']) && ! empty($op['auth_keys']) ) {

            if (null === ($authKeys = CommonUtil::getValidJSON($op['auth_keys']))) {
                LogMessage::error('Auth_keys Error Not valid JSON - File Generator');
                return false;
            }

            switch ($op['auth_type_code']) {
                case CommonConst::API_AUTH_BASIC :
                    $subBody .= '$options[\'auth\'] = [\'' . $authKeys[0]['username'] . '\', \'' . $authKeys[0]['password'] . '\'];' . PHP_EOL;
                    break;
                case CommonConst::API_AUTH_BEARER_TOKEN :
                    $subBody .= '$options[\'headers\'][\'Authorization\'] = \'Bearer ' . $authKeys[0]['token'] . '\';' . PHP_EOL;
                    break;
            }
        }

        if ($op['method'] !== CommonConst::PROTOCOL_TYPE_SECURE) {
            $subBody .= '$options[\'verify\'] = $this->httpReqVerify;' . PHP_EOL;
            $subBody .= '$options[\'timeout\'] = $this->httpReqTimeout;' . PHP_EOL;
        }

        if ( ! empty($op['arguments'])) {
            $subBodyReqs = '';
            foreach ($op['arguments'] as $reqs) {
                $reqVal = '\'' . $reqs['argument_value'] . '\'';
                if ( ! empty($reqs['relay_flag'])) {

                    $reqVal = ( ! empty($reqs['relay_biz_ops_id'])) ? '$this->params[\'' . $reqs['relay_parameter_key_name'] . '\']'
                            : '$this->resultParams[\'response\'][\'' . $reqs['relay_parameter_key_name'] . '\']'
                            ;

                    // 파라미터 타입이 JSON인 경우
                    if ($reqs['relay_parameter_type_code'] === CommonConst::VAR_TYPE_JSON_CODE && !empty($reqs['relay_sub_parameter_path'])) {
                        $reqVal .= $this->_jsonPathToArrayString($reqs['relay_sub_parameter_path']);
                    }

                    $reqVal .= ' ?? null';
                }

                $subBodyReqs .= '    \'' . $reqs['parameter_key_name'] . '\' => ' . $reqVal . ',' . PHP_EOL;
            }
        }

        if (!empty($subBodyReqs)) {
            $subBody .= '$options[\'' . $opReqTypeVar . '\'] = [' . PHP_EOL;
            $subBody .= $subBodyReqs;
            $subBody .= '];' . PHP_EOL . PHP_EOL;
        }

        // 보안프로토콜 처리
        if ($op['method'] == CommonConst::PROTOCOL_TYPE_SECURE) {
            $subBody .= '$uuid = Uuid::uuid5(Uuid::NAMESPACE_DNS, session_id() . time());' . PHP_EOL;
            $subBody .= '$this->eventKey = strtoupper(\'event-\' . $uuid->toString());' . PHP_EOL;
            $subBody .= '$datas = [\'event_key\' => $this->eventKey, \'params\' => $options[\'' . $opReqTypeVar . '\']];' . PHP_EOL;
            $subBody .= 'RedisUtil::setDataWithExpire($this->redis, CommonConst::REDIS_SECURE_PROTOCOL_COMMAND, $this->eventKey, CommonConst::REDIS_SESSION_EXPIRE_TIME_MIN_5, $datas);';
            $subBody .= PHP_EOL . '' . PHP_EOL;
        }

        if ($op['method'] == CommonConst::PROTOCOL_TYPE_SECURE) {
            $subBody .= '$secureData[\'' . $opReqTypeVar . '\'] = [\'event_key\' => $this->eventKey];' . PHP_EOL;
            $subBody .= '$ret = $this->_httpRequest($targetUrl, $secureData, \'' . $opReqType . '\');' . PHP_EOL;
        } else {
            $subBody .= '$ret = $this->_httpRequest($targetUrl, $options, \'' . $opReqType . '\');' . PHP_EOL;
        }

        $subBody .= PHP_EOL . '$result = [' . PHP_EOL;
        $subBody .= '    \'op_name\' => \'' . $op['op_name'] . '\',' . PHP_EOL;
        $subBody .= '    \'request_target_url\' => $targetUrl,' . PHP_EOL;
        $subBody .= '    \'server_status\' => $ret[\'res_status\'],' . PHP_EOL;

        if ($op['method'] == CommonConst::PROTOCOL_TYPE_SIMPLE_HTTP) {
            $subBody .= '    \'request\' => $options[\'' . $opReqTypeVar . '\'],' . PHP_EOL;
        }

        $subBody .= '    \'response\' => [' . PHP_EOL;
        foreach ($op['response'] as $ress) {
            $subBody .= '        \'' . $ress['res_key'] . '\' => $ret[\'res_data\'][\'' . $ress['res_key'] . '\'] ?? null,' . PHP_EOL;
        }
        $subBody .= '    ]' . PHP_EOL;
        $subBody .= '];' . PHP_EOL;
        $subBody .=  PHP_EOL . 'return $result;' . PHP_EOL;

        // 하위 메소드 생성
        $method = $this->class->addMethod($methodName)->setVisibility('private');
        $method->setBody($subBody);
        $method->addParameter('request')->setTypeHint('Request');
        $method->addParameter('response')->setTypeHint('Response');
        //$method->addParameter('params')->setTypeHint('array');
        //if ($bindingSeq > 1) {
        //    $method->addParameter('responseDatas')->setTypeHint('array');
        //}

        $method->addComment('Operation Name : ' . $op['op_name']);
        $method->addComment('ID : ' . $op['op_id']);
        $method->addComment('Description : ' . $op['op_desc']);
        $method->addComment('Binding Seq : ' . $bindingSeq);

        return true;
    }

    /**
     * jsonpath 를 배열스트링으로 변환해 반환
     *
     * @param $jsonPath
     *
     * @return string
     */
    private function _jsonPathToArrayString($jsonPath)
    {
        $arrayString = '';
        //$relayJson = json_decode($reqs['relay_sub_parameter_format'], true);
        $relayJsonPath = str_replace('$.', '', $jsonPath);

        $tmp = explode('.', $relayJsonPath);
        foreach ($tmp as $tpKey) {
            if (!empty(strpos($tpKey, '['))) {
                $tpKeyTemp = explode('[', $tpKey);
                $arrayString .= '[\'' . $tpKeyTemp[0] . '\']' . '[' . str_replace(']', '' , $tpKeyTemp[1]) . ']';
            } else {
                $arrayString .= '[\'' . $tpKey . '\']';
            }
        }

        return $arrayString;


    }

    private function _refining($file)
    {
        $file = str_replace([
            'extends \\Synctree',
            '(\\Container',
            '(\\Request $request, \\Response $response',
            '(\\Request $request, \\Response $response, \\$args',
        ], [
            'extends Synctree',
            '(Container',
            '(Request $request, Response $response',
            '(Request $request, Response $response, $args',
        ], $file);

        return $file;
    }

}
