Copy Domains";
+ break;
+ }
+ return $label;
+ }
+}
diff --git a/app/Helpers/CommonHelper.php b/app/Helpers/CommonHelper.php
new file mode 100644
index 0000000..68462ba
--- /dev/null
+++ b/app/Helpers/CommonHelper.php
@@ -0,0 +1,295 @@
+getRandomString($length, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_-=+;:,.?");
+ } //
+ //byte값을 알아보기 쉽게 변환
+ final public function getSizeForHuman($bytes)
+ {
+ $ext = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
+ $unitCount = 0;
+ for (; $bytes > 1024; $unitCount++) {
+ $bytes /= 1024;
+ }
+ return floor($bytes) . $ext[$unitCount];
+ } //
+ //Proxy등을 통하여 Client_IP가 알수없는경우 실제사용자의 IP를 가져오기 위한것
+ final public function getClientIP($clientIP = false)
+ {
+ if (isset($_SERVER['HTTP_CLIENT_IP'])) {
+ $clientIP = $_SERVER['HTTP_CLIENT_IP'];
+ } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+ $clientIP = $_SERVER['HTTP_X_FORWARDED_FOR'];
+ } else if (isset($_SERVER['HTTP_X_FORWARDED'])) {
+ $clientIP = $_SERVER['HTTP_X_FORWARDED'];
+ } else if (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
+ $clientIP = $_SERVER['HTTP_FORWARDED_FOR'];
+ } else if (isset($_SERVER['HTTP_FORWARDED'])) {
+ $clientIP = $_SERVER['HTTP_FORWARDED'];
+ } else if (isset($_SERVER['REMOTE_ADDR'])) {
+ $clientIP = $_SERVER['REMOTE_ADDR'];
+ }
+ return $clientIP;
+ } //
+ final public function isDomain(string $domain): bool
+ {
+ $parttern_validation = '/((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z0-9\&\.\/\?\:@\-_=#])*/';
+ return preg_match("$parttern_validation", $domain);
+ }
+ final public function isIPAddress(string $ip, $type = false): bool
+ {
+ switch ($type) {
+ case 'ipv4':
+ $result = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
+ break;
+ case 'ipv6':
+ $result = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
+ break;
+ case 'all':
+ $result = filter_var($ip, FILTER_VALIDATE_IP);
+ break;
+ default:
+ $result = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
+ break;
+ }
+ return $result;
+ }
+ final public function isHost(string $host): bool
+ {
+ $parttern_validation = '/[a-zA-Z0-9\.\/\?\:@\*\-_=#]/';
+ return preg_match($parttern_validation, $host);
+ }
+ //(EX:192.168.1.0 -> 192.168.001.000)
+ final public function convertIPV4toCIDR($cidr)
+ {
+ $temps = explode(".", $cidr);
+ return sprintf("%03d.%03d.%03d.%03d", $temps[0], $temps[1], $temps[2], $temps[3]);
+ } //
+ //(EX:192.168.001.0000 -> 192.168.1.0)
+ final public function convertCIDRtoIPV4($ipv4)
+ {
+ $temps = explode(".", $ipv4);
+ return sprintf("%d.%d.%d.%d", $temps[0], $temps[1], $temps[2], $temps[3]);
+ } //
+ final public function isMobile()
+ {
+ // Check the server headers to see if they're mobile friendly
+ if (isset($_SERVER["HTTP_X_WAP_PROFILE"])) {
+ return true;
+ }
+ // If the http_accept header supports wap then it's a mobile too
+ if (preg_match("/wap\.|\.wap/i", $_SERVER["HTTP_ACCEPT"])) {
+ return true;
+ }
+ // Still no luck? Let's have a look at the user agent on the browser. If it contains
+ // any of the following, it's probably a mobile device. Kappow!
+ if (isset($_SERVER["HTTP_USER_AGENT"])) {
+ $user_agents = array("midp", "j2me", "avantg", "docomo", "novarra", "palmos", "palmsource", "240x320", "opwv", "chtml", "pda", "windows\ ce", "mmp\/", "blackberry", "mib\/", "symbian", "wireless", "nokia", "hand", "mobi", "phone", "cdm", "up\.b", "audio", "SIE\-", "SEC\-", "samsung", "HTC", "mot\-", "mitsu", "sagem", "sony", "alcatel", "lg", "erics", "vx", "NEC", "philips", "mmm", "xx", "panasonic", "sharp", "wap", "sch", "rover", "pocket", "benq", "java", "pt", "pg", "vox", "amoi", "bird", "compal", "kg", "voda", "sany", "kdd", "dbt", "sendo", "sgh", "gradi", "jb", "\d\d\di", "moto");
+ foreach ($user_agents as $user_string) {
+ if (preg_match("/" . $user_string . "/i", $_SERVER["HTTP_USER_AGENT"])) {
+ return true;
+ }
+ }
+ }
+ // Let's NOT return "mobile" if it's an iPhone, because the iPhone can render normal pages quite well.
+ if (preg_match("/iphone/i", $_SERVER["HTTP_USER_AGENT"])) {
+ return false;
+ }
+ // None of the above? Then it's probably not a mobile device.
+ return false;
+ } //
+ final public function alert(string $msg, $url = null)
+ {
+ $msg = preg_replace("/\r/", "\\r", $msg);
+ $msg = preg_replace("/\n/", "\\n", $msg);
+ $msg = preg_replace("/\'/", "\'", $msg);
+ $msg = preg_replace("/\"/", "\'", $msg);
+ $msg = "alert(\"{$msg}\");";
+ switch ($url) {
+ case 'close':
+ $msg .= "window.close();";
+ break;
+ case 'back':
+ $msg .= "history.back();";
+ break;
+ default:
+ $msg .= $url ? "location.href=\"{$url}\";" : "";
+ break;
+ }
+ return "";
+ } //
+
+ public function getFieldLabel(string $field, array $viewDatas, array $extras = []): string
+ {
+ switch ($field) {
+ default:
+ if (strpos($viewDatas['field_rules'][$field], 'required') !== false) {
+ $extras = ["class" => 'text-danger', ...$extras];
+ }
+ $label = form_label(lang("{$viewDatas['class_path']}.label.{$field}"), $field, $extras);
+ break;
+ }
+ return $label;
+ }
+ //header.php에서 getFieldForm_Helper사용
+ public function getFieldForm(string $field, mixed $value, array $viewDatas, array $extras = []): string
+ {
+ $value = $value ?: DEFAULTS['EMPTY'];
+ switch ($field) {
+ case 'status':
+ $form = form_dropdown($field, [
+ "" => lang($viewDatas['class_path'] . '.label.' . $field) . ' 선택',
+ ...$viewDatas['field_options'][$field]
+ ], $value, $extras);
+ break;
+ case 'updated_at':
+ case 'created_at':
+ $form = form_input($field, $value, [
+ "class" => "calender form-control",
+ (strpos($viewDatas['field_rules'][$field], 'required') !== false) ? "required" : ""
+ ]);
+ break;
+ default:
+ $form = form_input($field, $value, [
+ "style" => "width:100%;",
+ "class" => "form-control",
+ (strpos($viewDatas['field_rules'][$field], 'required') !== false) ? "required" : ""
+ ]);
+ break;
+ }
+ return $form;
+ } //
+ public function getFieldView(string $field, array $viewDatas, array $extras = []): string
+ {
+ $value = $viewDatas['entity']->$field ?: DEFAULTS['EMPTY'];
+ switch ($field) {
+ case 'category_uid':
+ foreach (array_values($viewDatas['field_options'][$field]) as $category_2depths) {
+ foreach ($category_2depths as $key => $depth) {
+ if ($key == $depth) {
+ $value = $depth;
+ }
+ }
+ }
+ break;
+ case 'updated_at':
+ case 'created_at':
+ $value = $value ? date("Y-m-d", strtotime($value)) : "";
+ break;
+ default:
+ if (in_array($field, $viewDatas['filter_fields']) && $value) {
+ $extras["onChange"] = sprintf(
+ 'location.href="%s/toggle/%s/%s?%s="+this.options[this.selectedIndex].value',
+ current_url(),
+ $viewDatas['entity']->getPK(),
+ $field,
+ $field
+ );
+ $value = $this->getFieldForm($field, $viewDatas['entity']->$field, $viewDatas, $extras);
+ }
+ break;
+ }
+ return $value;
+ } //
+ public function getListRowColor($entity): string
+ {
+ return $entity->status != DEFAULTS['STATUS'] ? 'class="table-danger"' : "";
+ }
+ public function getListLabel(string $field, array $viewDatas, array $extras = []): string
+ {
+ switch ($field) {
+ default:
+ $label = $this->getFieldLabel($field, $viewDatas, $extras);
+ if (isset($viewDatas['order_field']) && $viewDatas['order_field'] == $field) {
+ $label .= $viewDatas['order_value'] == 'ASC' ? ICONS["UP"] : ICONS["DOWN"];
+ }
+ $viewDatas['uri']->addQuery('order_field', $field);
+ $viewDatas['uri']->addQuery('order_value', $viewDatas['order_value'] == 'DESC' ? "ASC" : "DESC");
+ $label = anchor((string) $viewDatas['uri'], $label);
+ break;
+ }
+ return $label;
+ }
+ final public function getListButtonLink(string $url, string $label, array $viewDatas, array $extras = []): string
+ {
+ switch ($viewDatas['action_form']) {
+ case FORMS['MODAL']:
+ $label = form_label(
+ $label,
+ "",
+ [
+ "data-src" => $url,
+ "data-bs-toggle" => "modal",
+ "data-bs-target" => "#index_action_form",
+ ...$extras
+ ]
+ );
+ break;
+ case FORMS['IFRAME']:
+ $label = form_label($label, "", [
+ "onClick" => "changeIframe_src('{$url}')",
+ ...$extras,
+ ]);
+ break;
+ default:
+ $label = anchor($url, $label, $extras);
+ break;
+ }
+ return $label;
+ }
+ public function getListButton(string $action, array $viewDatas, array $extras = []): string
+ {
+ switch ($action) {
+ case 'create':
+ $extras = ["class" => "btn btn-outline btn-primary btn-circle", "target" => "_self", ...$extras];
+ $action = $this->getListButtonLink(
+ current_url() . '/' . $action,
+ '입력',
+ $viewDatas,
+ $extras
+ );
+ break;
+ case 'modify':
+ $pk = $viewDatas['entity']->getPK();
+ $checkbox = form_checkbox([
+ "id" => "checkbox_uid_{$pk}",
+ "name" => "batchjob_uids[]",
+ "value" => $pk,
+ "class" => "batchjobuids_checkboxs",
+ "checked" => in_array($pk, old("batchjob_uids[]", []))
+ ]);
+ $action = $checkbox . $this->getListButtonLink(
+ current_url() . '/' . $action . '/' . $viewDatas['entity']->getPK(),
+ $viewDatas['cnt'],
+ $viewDatas,
+ $extras
+ );
+ break;
+ case 'delete':
+ $extras = ["class" => "btn btn-sm btn-danger btn-circle", "target" => "_self", ...$extras];
+ $action = anchor(
+ current_url() . '/' . $action . '/' . $viewDatas['entity']->getPK(),
+ ICONS['DELETE'],
+ $extras
+ );
+ break;
+ case 'batchjob':
+ $action = form_submit("batchjob_submit", '일괄처리', [
+ "class" => "btn btn-outline btn-warning",
+ "onclick" => "return submitBatchJob()"
+ ]);
+ break;
+ }
+ return $action;
+ }
+}
diff --git a/app/Helpers/MapurlHelper.php b/app/Helpers/MapurlHelper.php
new file mode 100644
index 0000000..f2bf327
--- /dev/null
+++ b/app/Helpers/MapurlHelper.php
@@ -0,0 +1,67 @@
+ "예)http://old.example.com",
+ "style" => "width:100%; ::placeholder{color:silver; opacity: 1;}",
+ "class" => "form-control",
+ "required" => ""
+ ]);
+ break;
+ case 'newurl':
+ $form = form_input($field, $value, [
+ "placeholder" => "예)https://new.example.com",
+ "style" =>
+ "width:100%; ::placeholder{color:silver; opacity: 1;}",
+ "class" => "form-control",
+ "required" => ""
+ ]);
+ break;
+ default:
+ $form = parent::getFieldForm($field, $value, $viewDatas, $extras);
+ break;
+ }
+ return $form;
+ } //
+ public function getRemapPage(array $viewDatas): string
+ {
+ $urls = [];
+ foreach ($viewDatas['entitys'] as $viewDatas['entity']) {
+ //한글을 포함하고 있는지 체크 HTTP 나 HTTPS 와 도메인 분리해서 한글도메인을 Punycode로 변환
+ if (preg_match("/[\xE0-\xFF][\x80-\xFF][\x80-\xFF]/", $viewDatas['entity']->oldurl)) {
+ preg_match("/^(https?:\/\/)(.*)/", $viewDatas['entity']->oldurl, $matches);
+ $oldurl = $matches[1] . idn_to_ascii($matches[2]);
+ } else {
+ $oldurl = trim($viewDatas['entity']->oldurl);
+ }
+ $urls[] = sprintf(
+ "case '%s': window.location.href='%s'; break;",
+ $oldurl,
+ trim($viewDatas['entity']->newurl)
+ );
+ }
+ return sprintf("
+ switch (window.location.origin) {
+ %s
+ default:
+ alert(window.location.origin + ' 지정되지 않은 URL입니다');
+ history.go(-1);
+ break;
+ }
+ ", implode("\n", $urls));
+ }
+}
diff --git a/app/Helpers/UserHelper.php b/app/Helpers/UserHelper.php
new file mode 100644
index 0000000..2dfacfb
--- /dev/null
+++ b/app/Helpers/UserHelper.php
@@ -0,0 +1,92 @@
+ "예)",
+ "style" => "width:100%; ::placeholder{color:silver; opacity: 1;}",
+ "class" => "form-control",
+ "required" => ""
+ ]);
+ break;
+ case 'passwd':
+ case 'confirmpassword':
+ $form = form_password($field, "", [
+ "style" => "width:100%;",
+ "class" => "form-control",
+ "required" => ""
+ ]);
+ break;
+ case 'email':
+ $form = form_input($field, $value, [
+ "placeholder" => "예)test@example.com",
+ "style" => "width:100%; ::placeholder{color:silver; opacity: 1;}",
+ "class" => "form-control",
+ "required" => ""
+ ]);
+ break;
+ case 'mobile':
+ $form = form_input($field, $value, [
+ "placeholder" => "예)010-0010-0010",
+ "style" => "width:100%; ::placeholder{color:silver; opacity: 1;}",
+ "class" => "form-control"
+ ]);
+ break;
+ case 'role':
+ if (in_array($viewDatas['action'], ['create', 'modify'])) {
+ $forms = [];
+ foreach ($viewDatas['field_options'][$field] as $key => $label) {
+ $values = is_array($value) ? $value : explode(DEFAULTS["DELIMITER_ROLE"], $value);
+ $forms[] = form_checkbox($field . "[]", $key, in_array($key, $values)) . $label;
+ }
+ $form = implode(" ", $forms);
+ } else {
+ $form = form_dropdown(
+ $field,
+ [
+ "" => lang($viewDatas['class_path'] . '.label.' . $field) . ' 선택',
+ ...$viewDatas['field_options'][$field]
+ ],
+ $value,
+ $extras
+ );
+ }
+ break;
+ default:
+ $form = parent::getFieldForm($field, $value, $viewDatas, $extras);
+ break;
+ }
+ return $form;
+ } //
+ public function getFieldView(string $field, array $viewDatas, array $extras = []): string
+ {
+ $value = $viewDatas['entity']->$field ?: DEFAULTS['EMPTY'];
+ switch ($field) {
+ case 'role':
+ $roles = [];
+ foreach (explode(DEFAULTS["DELIMITER_ROLE"], $value) as $key) {
+ $roles[] = $viewDatas['field_options'][$field][$key];
+ }
+ $value = implode(" , ", $roles);
+ break;
+ default:
+ $value = parent::getFieldView($field, $viewDatas, $extras);
+ break;
+ }
+ return $value;
+ } //
+}
diff --git a/app/Language/.gitkeep b/app/Language/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Language/en/Cloudflare/Account.php b/app/Language/en/Cloudflare/Account.php
new file mode 100644
index 0000000..40e74ba
--- /dev/null
+++ b/app/Language/en/Cloudflare/Account.php
@@ -0,0 +1,21 @@
+ "Account정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'auth_uid' => "인증",
+ 'title' => "계정명",
+ 'type' => "계정형식",
+ 'status' => "상태",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ ],
+ "TYPE" => [
+ "standard" => "standard",
+ "enterprise" => "enterprise",
+ ],
+ "STATUS" => [
+ "use" => "사용",
+ "unuse" => "사용않함",
+ ],
+];
diff --git a/app/Language/en/Cloudflare/Auth.php b/app/Language/en/Cloudflare/Auth.php
new file mode 100644
index 0000000..b2e1aff
--- /dev/null
+++ b/app/Language/en/Cloudflare/Auth.php
@@ -0,0 +1,17 @@
+ "Auth정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'id' => "인증ID",
+ 'authkey' => "인증Key",
+ 'oldkey' => "이전인증Key",
+ 'status' => "상태",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ ],
+ "STATUS" => [
+ "use" => "사용",
+ "unuse" => "사용않함",
+ ],
+];
diff --git a/app/Language/en/Cloudflare/Record.php b/app/Language/en/Cloudflare/Record.php
new file mode 100644
index 0000000..d616a57
--- /dev/null
+++ b/app/Language/en/Cloudflare/Record.php
@@ -0,0 +1,48 @@
+ "Record정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'zone_uid' => "도메인",
+ 'type' => "Type",
+ 'host' => "호스트명",
+ 'content' => "IP정보",
+ 'ttl' => "TTL",
+ 'proxiable' => "proxiable",
+ 'fixed' => "CDN잠금",
+ 'proxied' => "CDN사용여부",
+ 'locked' => "서비스",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ 'hosts' => "호스트명",
+ ],
+ "ZONE_UID" => [],
+ "TYPE" => [
+ 'A' => 'A',
+ 'AAAA' => 'AAAA(ipv6)',
+ 'CNAME' => 'CNAME',
+ 'NS' => 'NS',
+ 'MX' => 'MX',
+ 'PTR' => 'PTR',
+ 'SPF' => 'SPF',
+ 'TXT' => 'TXT',
+ 'SRV' => 'SRV',
+ 'INFO' => 'INFO',
+ ],
+ "FIXED" => [
+ "on" => "고정",
+ "off" => "비고정",
+ ],
+ "PROXIABLE" => [
+ "on" => "사용",
+ "off" => "사용 않함",
+ ],
+ "PROXIED" => [
+ "on" => "CDN 사용",
+ "off" => "CDN 미사용",
+ ],
+ "LOCKED" => [
+ "on" => "운영중",
+ "off" => "잠김",
+ ],
+];
diff --git a/app/Language/en/Cloudflare/Zone.php b/app/Language/en/Cloudflare/Zone.php
new file mode 100644
index 0000000..a90261e
--- /dev/null
+++ b/app/Language/en/Cloudflare/Zone.php
@@ -0,0 +1,43 @@
+ "Zone정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'account_uid' => "계정",
+ 'domain' => "도메인",
+ 'name_servers' => "네임서버",
+ 'original_name_servers' => "이전네임서버",
+ 'plan' => "plan",
+ 'development_mode' => "개발모드",
+ 'ipv6' => "ipv6",
+ 'security_level' => "공격방어",
+ 'status' => "서비스",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ 'domains' => "도메인명",
+ 'hosts' => "호스트명",
+ 'type' => "TYPE",
+ 'content' => "IP정보",
+ 'proxied' => "CDN기능",
+ ],
+ "ACCOUNT_UID" => [],
+ "DEVELOPMENT_MODE" => [
+ "on" => "사용",
+ "off" => "사용않함",
+ ],
+ "IPV6" => [
+ "on" => "사용",
+ "off" => "사용않함",
+ ],
+ "SECURITY_LEVEL" => [
+ "under_attack" => "under_attack",
+ "medium" => "medium",
+ "low" => "low",
+ "essentially_off" => "essentially_off",
+ ],
+ "STATUS" => [
+ "active" => "active",
+ "pending" => "pending",
+ "moved" => "moved"
+ ],
+];
diff --git a/app/Language/en/Mapurl.php b/app/Language/en/Mapurl.php
new file mode 100644
index 0000000..b2ebc5d
--- /dev/null
+++ b/app/Language/en/Mapurl.php
@@ -0,0 +1,16 @@
+ "URL Mapping 정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'oldurl' => "기존URL",
+ 'newurl' => "신규URL",
+ 'status' => "상태",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ ],
+ "STATUS" => [
+ "use" => "사용",
+ "unuse" => "사용않함",
+ ],
+];
diff --git a/app/Language/en/User.php b/app/Language/en/User.php
new file mode 100644
index 0000000..ae45d5f
--- /dev/null
+++ b/app/Language/en/User.php
@@ -0,0 +1,29 @@
+ "계정정보",
+ 'label' => [
+ 'uid' => "번호",
+ 'id' => "계정",
+ 'passwd' => "암호",
+ 'confirmpassword' => "암호확인",
+ 'email' => "메일",
+ 'mobile' => "연락처",
+ 'role' => "권한",
+ 'name' => "이름",
+ 'status' => "상태",
+ 'updated_at' => "수정일",
+ 'created_at' => "작성일",
+ ],
+ "ROLE" => [
+ "user" => "일반회원",
+ "vip" => "VIP회원",
+ "manager" => "관리자",
+ "cloudflare" => "Cloudflare관리자",
+ "director" => "감독자",
+ "master" => "마스터",
+ ],
+ "STATUS" => [
+ "use" => "사용",
+ "unuse" => "사용않함",
+ ],
+];
diff --git a/app/Language/en/Validation.php b/app/Language/en/Validation.php
new file mode 100644
index 0000000..54d1e7a
--- /dev/null
+++ b/app/Language/en/Validation.php
@@ -0,0 +1,4 @@
+_model === null) {
+ $this->_model = new AccountModel();
+ }
+ return $this->_model;
+ }
+ //Result 형태
+ // [
+ // {"id":"078e88a7735965b661715af13031ecb0",
+ // "name":"Cloudwin002@idcjp.jp's Auth",
+ // "type":"standard",
+ // "settings":{
+ // "enforce_twofactor":false,
+ // "api_access_enabled":null,
+ // "access_approval_expiry":null,
+ // "use_account_custom_ns_by_default":false
+ // },
+ // "legacy_flags":{"enterprise_zone_quota":{"maximum":0,"current":0,"available":0}},
+ // "created_on":"2017-06-26T05:44:49.470184Z"}
+ // ]
+ public function getArrayByResult($result, array $formDatas = []): array
+ {
+ $formDatas[AccountModel::PK] = $result->id;
+ $formDatas[AccountModel::PARENT] = $this->getAuthEntity()->getPK();
+ $formDatas[AccountModel::TITLE] = $result->name;
+ $formDatas['type'] = $result->type;
+ $formDatas['status'] = $this->getAuthEntity()->status;
+ $formDatas['updated_at'] = date("Y-m-d H:i:s");
+ $formDatas['created_at'] = $result->created_on;
+ return $formDatas;
+ }
+ public function reload(): array
+ {
+ log_message("notice", "-----{$this->getAuthEntity()->getTitle()} 처리 시작-----");
+ $entitys = [];
+ foreach ($this->reload_procedure("accounts") as $result) {
+ $formDatas = $this->getArrayByResult($result);
+ $entity = $this->getModel()->modify($this->getModel()->getEntity(), $formDatas);
+ $entitys[$entity->getPK()] = $entity;
+ }
+ //부모키를 기준으로 CF에 존재하지 않는 데이터 삭제용
+ $this->getModel()->where(AccountModel::PARENT, $this->getAuthEntity());
+ $this->getModel()->whereNotIn(AccountModel::PK, array_keys($entitys));
+ $this->getModel()->delete();
+ log_message("notice", "-----{$this->getAuthEntity()->getTitle()} 처리[" . count($entitys) . "개] 완료-----");
+ return $entitys;
+ }
+}
diff --git a/app/Libraries/Cloudflare/Cloudflare.php b/app/Libraries/Cloudflare/Cloudflare.php
new file mode 100644
index 0000000..ff7cc36
--- /dev/null
+++ b/app/Libraries/Cloudflare/Cloudflare.php
@@ -0,0 +1,75 @@
+_auth_entity = $auth_entity;
+ parent::__construct();
+ }
+ abstract protected function getArrayByResult($result, array $formDatas = []): array;
+ final public function getMySocket(): CloudflareSocket
+ {
+ if ($this->_mySocket === null) {
+ $this->_mySocket = new CloudflareSocket($this->getAuthEntity());
+ }
+ return $this->_mySocket;
+ }
+ final protected function getAuthEntity(): AuthEntity
+ {
+ if ($this->_auth_entity === null) {
+ throw new \Exception(__FUNCTION__ . "에서 인증정보가 없습니다.");
+ }
+ return $this->_auth_entity;
+ }
+ final protected function getAuthModel(): AuthModel
+ {
+ if ($this->_authModel === null) {
+ $this->_authModel = new AuthModel();
+ }
+ return $this->_authModel;
+ }
+ final protected function getAccountModel(): AccountModel
+ {
+ if ($this->_accountModel === null) {
+ $this->_accountModel = new AccountModel();
+ }
+ return $this->_accountModel;
+ }
+ final protected function reload_procedure($uri): array
+ {
+ $page = 1; //1부터 시작
+ $results = [];
+ do {
+ $query = [
+ 'page' => $page,
+ 'per_page' => $this->getMySocket()::$_request_perpage_max,
+ 'match' => 'all',
+ ];
+ $response = $this->getMySocket()->get($uri, $query);
+ $cf = json_decode($response->getBody());
+ if (!$cf->success) {
+ $message = __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ $results = array_merge($results, $cf->result);
+ if (count($cf->result) < $this->getMySocket()::$_request_perpage_max) {
+ break;
+ }
+ $page++;
+ } while (true);
+ return $results;
+ }
+}
diff --git a/app/Libraries/Cloudflare/Record.php b/app/Libraries/Cloudflare/Record.php
new file mode 100644
index 0000000..d04537e
--- /dev/null
+++ b/app/Libraries/Cloudflare/Record.php
@@ -0,0 +1,163 @@
+_zone_entity = $zone_entity;
+ $account_entity = $this->getAccountModel()->getEntityByPK($this->_zone_entity->getParent());
+ if ($account_entity === null) {
+ throw new \Exception("해당 계정정보를 찾을수 없습니다.");
+ }
+ $auth_entity = $this->getAuthModel()->getEntityByPK($account_entity->getParent());
+ if ($auth_entity === null) {
+ throw new \Exception("해당 계정정보를 찾을수 없습니다.");
+ }
+ parent::__construct($auth_entity);
+ }
+ protected function getModel(): RecordModel
+ {
+ if ($this->_model === null) {
+ $this->_model = new RecordModel();
+ }
+ return $this->_model;
+ }
+ public function getArrayByResult($result, array $formDatas = []): array
+ {
+ $formDatas[RecordModel::PK] = $result->id;
+ $formDatas[RecordModel::PARENT] = $result->zone_id;
+ $formDatas[RecordModel::TITLE] = $result->name;
+ $formDatas['type'] = $result->type;
+ $formDatas['content'] = $result->content;
+ $formDatas['ttl'] = (int)$result->ttl;
+ $formDatas['proxiable'] = $result->proxiable ? "on" : "off";
+ $formDatas['proxied'] = $result->proxied ? "on" : "off";
+ $formDatas['locked'] = "on";
+ if (isset($result->locked) && $result->locked) {
+ $formDatas['locked'] = "off";
+ }
+ $formDatas['updated_at'] = date("Y-m-d H:i:s");
+ $formDatas['created_at'] = $result->created_on;
+ return $formDatas;
+ }
+ public function create(string $host, string $type, string $content, string $proxied): RecordEntity
+ {
+ //Socket용
+ //호스트생성을 위해 Cloudflare에 전송
+ $datas = [
+ 'name' => $host,
+ 'type' => $type,
+ 'content' => $content,
+ 'proxied' => $proxied === 'on' ? true : false
+ ];
+ $cf = $this->getMySocket()->post("zones/{$this->_zone_entity->getPK()}/dns_records", $datas);
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Record:" . __FUNCTION__ . "에서 실패:\nrequest:" . var_export($datas, true) . "\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //DB생성
+ $formDatas = $this->getArrayByResult($cf->result);
+ return $this->getModel()->create($formDatas);
+ }
+ public function modify(RecordEntity $entity, array $formDatas): RecordEntity
+ {
+ //TTL값은 CDN(proxied)가 사용함일때는 무조건 1, 않함일때는 120이 적용
+ $datas = [
+ 'type' => isset($formDatas['type']) ? $formDatas['type'] : $entity->type,
+ 'name' => isset($formDatas['host']) ? $formDatas['host'] : $entity->host,
+ 'content' => isset($formDatas['content']) ? $formDatas['content'] : $entity->content,
+ 'proxied' => $entity->proxied == 'on' ? true : false,
+ 'ttl' => intval($entity->ttl),
+ ];
+ if (isset($formDatas['proxied'])) {
+ if ($formDatas['proxied'] === 'on') {
+ $datas['proxied'] = true;
+ $datas['ttl'] = 1;
+ } else {
+ $datas['proxied'] = false;
+ $datas['ttl'] = 120;
+ }
+ }
+ // 인코딩된 JSON을 확인
+ // throw new \Exception("Record:" . __FUNCTION__ . "\n" . json_encode($datas, JSON_PRETTY_PRINT) . "\n" . var_export($datas, true));
+ $cf = $this->getMySocket()->put("zones/{$this->_zone_entity->getPK()}/dns_records/{$entity->getPK()}", $datas);
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Record:" . __FUNCTION__ . "에서 실패:\nrequest:" . var_export($datas, true) . "\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //DB수정
+ $formDatas = $this->getArrayByResult($cf->result);
+ return $this->getModel()->modify($entity, $formDatas);
+ }
+ public function delete(RecordEntity $entity): void
+ {
+ $cf = $this->getMySocket()->delete("zones/{$this->_zone_entity->getPK()}/dns_records/{$entity->getPK()}");
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Record:" . __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //DB삭제
+ $this->getModel()->where(RecordModel::PK, $entity->getPK());
+ $this->getModel()->delete();
+ }
+ public function sync(RecordEntity $entity): void
+ {
+ // 기존 Sync형태
+ $cf = $this->getMySocket()->get("zones/{$this->_zone_entity->getPK()}/dns_records/{$entity->getPK()}");
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Record:" . __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ // DB수정
+ log_message("debug", var_export($cf->result, true));
+ $entity = $this->getModel()->modify($entity, $this->getArrayByResult($cf->result));
+ //Async형태
+ // $promise = $this->getMySocket()getAsync("zones/{$this->_zone_entity->getPK()}/dns_records/{$entity->getPK()}");
+ // $promise->then(
+ // onFulfilled: function ($response) use ($entity): RecordEntity {
+ // $record = json_decode($response->getBody(), true)['result'];
+ // log_message("debug", var_export($record));
+ // //DB수정
+ // return $this->getModel()->modify($entity, $this->getArrayByResult($record));
+ // },
+ // onRejected: function ($error) {
+ // log_message('error', 'Failed to fetch DNS records: ' . $error->getMessage());
+ // // throw new \Exception('Failed to fetch DNS records: ' . $error->getMessage());
+ // }
+ // );
+ // $promise->wait();
+ }
+ //Reload
+ public function reload(): array
+ {
+ log_message("notice", "-----{$this->_zone_entity->getTitle()} 처리 시작-----");
+ $entitys = [];
+ foreach ($this->reload_procedure("zones/{$this->_zone_entity->getPK()}/dns_records") as $result) {
+ $formDatas = $this->getArrayByResult($result);
+ $entity = $this->getModel()->modify($this->getModel()->getEntity(), $formDatas);
+ $entitys[$entity->getPK()] = $entity;
+ }
+ //부모키를 기준으로 CF에 존재하지 않는 데이터 DB삭제
+ $this->getModel()->where(RecordModel::PARENT, $this->_zone_entity);
+ $this->getModel()->whereNotIn(RecordModel::PK, array_keys($entitys));
+ $this->getModel()->delete();
+ log_message("notice", "-----{$this->_zone_entity->getTitle()} 처리[" . count($entitys) . "개] 완료-----");
+ return $entitys;
+ }
+}
diff --git a/app/Libraries/Cloudflare/Zone.php b/app/Libraries/Cloudflare/Zone.php
new file mode 100644
index 0000000..68c06ac
--- /dev/null
+++ b/app/Libraries/Cloudflare/Zone.php
@@ -0,0 +1,177 @@
+ 'off', 'ipv6' => 'off', 'security_level' => 'medium'];
+ public function __construct(AccountEntity $account_entity)
+ {
+ $this->_account_entity = $account_entity;
+ $auth_entity = $this->getAuthModel()->getEntityByPK($this->_account_entity->getParent());
+ if ($auth_entity === null) {
+ throw new \Exception("해당 계정정보를 찾을수 없습니다.");
+ }
+ parent::__construct($auth_entity);
+ }
+ protected function getModel(): ZoneModel
+ {
+ if ($this->_model === null) {
+ $this->_model = new ZoneModel();
+ }
+ return $this->_model;
+ }
+ protected function getArrayByResult($result, array $formDatas = []): array
+ {
+ $formDatas[ZoneModel::PK] = $result->id;
+ $formDatas[ZoneModel::PARENT] = $result->account->id;
+ $formDatas[ZoneModel::TITLE] = $result->name;
+ $formDatas['status'] = $result->status;
+ //$formDatas['type'] = $result->type; // full 이게있는데 뭔지 잘모름
+ $formDatas['name_servers'] = 'none';
+ if (isset($result->name_servers)) {
+ $formDatas['name_servers'] = is_array($result->name_servers) ?
+ implode(
+ ',',
+ $result->name_servers
+ ) : $result->name_servers;
+ }
+ $formDatas['original_name_servers'] = 'none';
+ if (isset($result->original_name_servers)) {
+ $formDatas['original_name_servers'] = is_array($result->original_name_servers) ?
+ implode(
+ ',',
+ $result->original_name_servers
+ ) : $result->original_name_servers;
+ }
+ // $formDatas['updated_at'] = $result->modified_on;
+ $formDatas['updated_at'] = date("Y-m-d H:i:s");
+ $formDatas['created_at'] = $result->created_on;
+ $formDatas['plan'] = $result->plan->name;
+ return $formDatas;
+ }
+ //Cfzone에서 가져온 값을 zone에 setting
+ private function getCFSetting(ZoneEntity $entity): array
+ {
+ $cf = $this->getMySocket()->get('zones/' . $entity->getPK() . '/settings');
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Zone:" . __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ $formDatas = [];
+ foreach ($cf->result as $cf) {
+ if (in_array($cf->id, array_keys($this->_setting_fields))) {
+ $formDatas[$cf->id] = $cf->value;
+ }
+ }
+ return $formDatas;
+ }
+ private function setCFSetting(ZoneEntity $entity, string $field, string $value): string
+ {
+ $datas = ['value' => $value];
+ $cf = $this->getMySocket()->patch('zones/' . $entity->getPK() . '/settings/' . $field, $datas);
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success || $cf->result->id !== $field) {
+ $message = "Zone:" . __FUNCTION__ . "에서 실패:\nrequest:" . var_export($datas, true) . "\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //최종 결과값은 body->result->id='필드명',body->result->value='on/off'이런식으로 받음
+ return $cf->result->value;
+ }
+ public function create(string $domain, bool $jump_start = false): ZoneEntity
+ {
+ //Socket용
+ //도메인생성을 위해 Cloudflare에 전송
+ $datas = [
+ 'accountId' => $this->_account_entity->getPK(),
+ 'name' => $domain,
+ 'jump_start' => $jump_start,
+ ];
+ $cf = $this->getMySocket()->post('zones/', $datas);
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Zone:" . __FUNCTION__ . "에서 실패:\nrequest:" . var_export($datas, true) . "\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //DB생성
+ $formDatas = $this->getArrayByResult($cf->result);
+ $entity = $this->getModel()->create($formDatas);
+
+ //초기화값 추가셋팅 ipv6 , development_mode , security_level
+ $formDatas = [];
+ foreach ($this->_setting_fields as $field => $default) {
+ $formDatas[$field] = $this->setCFSetting($entity, $field, $default);
+ }
+ //DB수정
+ return $this->getModel()->modify($entity, $formDatas);
+ }
+ public function modify(ZoneEntity $entity, array $formDatas): ZoneEntity
+ {
+ // throw new \Exception("zone:modify" . var_export($formDatas, true));
+ //modify,toggle,batchjob 사용 셋팅 ipv6 , //development_mode , //security_level
+ foreach ($formDatas as $field => $value) {
+ $formDatas[$field] = $this->setCFSetting($entity, $field, $value);
+ }
+ //DB수정
+ return $this->getModel()->modify($entity, $formDatas);
+ }
+ public function delete(ZoneEntity $entity): void
+ {
+ $cf = $this->getMySocket()->delete("zones/{$entity->getPK()}");
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Zone:" . __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ //DB삭제
+ $this->getModel()->where(ZoneModel::PK, $entity->getPK());
+ $this->getModel()->delete();
+ }
+ public function sync(ZoneEntity $entity): void
+ {
+ $cf = $this->getMySocket()->get("zones/{$entity->getPK()}");
+ $cf = json_decode($cf->getBody());
+ if (!$cf->success) {
+ $message = "Zone:" . __FUNCTION__ . "에서 실패:\nresponse:" . var_export($cf, true);
+ log_message("error", $message);
+ throw new \Exception($message);
+ }
+ $formDatas = $this->getArrayByResult($cf->result);
+ $entity = $this->getModel()->modify($this->getModel()->getEntity(), $formDatas);
+ //추가 셋팅용
+ $formDatas = $this->getCFSetting($entity);
+ $this->getModel()->modify($entity, $formDatas);
+ }
+ //Reload
+ public function reload(): array
+ {
+ log_message("notice", "-----{$this->_account_entity->getTitle()} 처리 시작-----");
+ $entitys = [];
+ foreach ($this->reload_procedure('zones') as $result) {
+ $formDatas = $this->getArrayByResult($result);
+ $entity = $this->getModel()->modify($this->getModel()->getEntity(), $formDatas);
+ //추가 셋팅용
+ $formDatas = $this->getCFSetting($entity);
+ $entity = $this->getModel()->modify($entity, $formDatas);
+ $entitys[$entity->getPK()] = $entity;
+ }
+ //부모키를 기준으로 CF에 존재하지 않는 데이터 삭제용
+ $this->getModel()->where(ZoneModel::PARENT, $this->_account_entity);
+ $this->getModel()->whereNotIn(ZoneModel::PK, array_keys($entitys));
+ $this->getModel()->delete();
+ log_message("notice", "-----{$this->_account_entity->getTitle()} 처리[" . count($entitys) . "개] 완료-----");
+ return $entitys;
+ }
+}
diff --git a/app/Libraries/CommonLibrary.php b/app/Libraries/CommonLibrary.php
new file mode 100644
index 0000000..cbc96c9
--- /dev/null
+++ b/app/Libraries/CommonLibrary.php
@@ -0,0 +1,23 @@
+_libraryDatas)) {
+ return null;
+ }
+ return $this->_libraryDatas[$name];
+ }
+
+ final public function __set($name, $value): void
+ {
+ $this->_libraryDatas[$name] = $value;
+ }
+}
diff --git a/app/Libraries/MyAuth/GoogleAuth.php b/app/Libraries/MyAuth/GoogleAuth.php
new file mode 100644
index 0000000..bb18972
--- /dev/null
+++ b/app/Libraries/MyAuth/GoogleAuth.php
@@ -0,0 +1,113 @@
+access_code = $access_code;
+ }
+
+ public function getMySocket(): GoogleSocket
+ {
+ if ($this->_mySocket === null) {
+ $this->_mySocket = new GoogleSocket(env('socket.google.client.token_name'));
+ $this->_mySocket->setClientId(env('socket.google.client.id'));
+ $this->_mySocket->setClientSecret(env('socket.google.client.key'));
+ $this->_mySocket->setRedirectUri(base_url(env('socket.google.client.callback_url')));
+ $this->_mySocket->addScope(env('socket.google.api.scope'));
+ $this->_mySocket->setToken($this->access_code);
+ }
+ return $this->_mySocket;
+ }
+
+ final protected function getModel(): SNSUserModel
+ {
+ if ($this->_model === null) {
+ $this->_model = model(SNSUserModel::class);
+ }
+ return $this->_model;
+ }
+
+ // throw new \Exception(__METHOD__ . "에서 데이터 처리 필요");
+ // DEBUG - 2023-07-13 12:54:51 --> \Google\Service\Oauth2\Userinfo::__set_state(array(
+ // 'internal_gapi_mappings' =>
+ // 'familyName' => 'family_name',
+ // 'givenName' => 'given_name',
+ // 'verifiedEmail' => 'verified_email',
+ // ),
+ // 'modelData' =>
+ // array (
+ // 'verified_email' => true,
+ // 'given_name' => '이름',
+ // 'family_name' => '성',
+ // ),
+ // 'processed' =>
+ // array (
+ // ),
+ // 'email' => 'twsdfsew342s@gmail.com',
+ // 'familyName' => '성',
+ // 'gender' => NULL,
+ // 'givenName' => '이름',
+ // 'hd' => NULL,
+ // 'id' => '103667492432234234236838324',
+ // 'link' => NULL,
+ // 'locale' => 'ko',
+ // 'name' => '성이름',
+ // 'picture' => 'https://lh3.googleusercontent.com/a/AAcHTteFSgefsdfsdRJBkJA2tBEmg4PQrvI1Ta_5IXu5=s96-c',
+ // 'verifiedEmail' => true,
+ // ))
+ public function checkUser(): UserEntity
+ {
+ try {
+ //Google 서비스 설정
+ $service = new Oauth2($this->getMySocket());
+ $authInfo = $service->userinfo->get();
+ log_message('debug', var_export($authInfo, true));
+ //기존 등록된 사용자가 있는지 검사
+ $this->getModel()->where(SNSUserModel::SITE, $this->_site);
+ $entity = $this->getModel()->getEntityByID($authInfo['id']);
+ if ($entity === null) {
+ //없다면 새로 등록
+ $formDatas = [
+ 'site' => $this->_site,
+ 'id' => $authInfo['id'],
+ 'name' => $authInfo['name'],
+ 'email' => $authInfo['email'],
+ 'detail' => json_encode($authInfo),
+ 'status' => 'standby',
+ ];
+ $entity = $this->getModel()->create($formDatas);
+ }
+ //상태가 use(승인완료)가 아니라면
+ if (
+ $entity->status !== DEFAULTS['STATUS']
+ ) {
+ throw new PageNotFoundException("{$this->_site}}의{$authInfo['email']}:{$authInfo['name']}님은 " . $entity->status . "입니다");
+ }
+ //local db 사용와의 연결 확인
+ $userModel = model(UserModel::class);
+ $user_entity = $userModel->getEntityByID($entity->getID());
+ if ($user_entity === null) {
+ throw new PageNotFoundException("{$this->_site}의{$authInfo['email']}:{$authInfo['name']}님은 아직 사용자 연결이 이루어지지 않았습니다. ");
+ }
+ return $user_entity;
+ } catch (\Exception $e) {
+ log_message('error', $e->getMessage());
+ throw new PageNotFoundException("관리자에게 문의하시기 바랍니다. {$e->getMessage()}");
+ }
+ }
+}
diff --git a/app/Libraries/MyAuth/LocalAuth.php b/app/Libraries/MyAuth/LocalAuth.php
new file mode 100644
index 0000000..3d13a3f
--- /dev/null
+++ b/app/Libraries/MyAuth/LocalAuth.php
@@ -0,0 +1,41 @@
+_model === null) {
+ $this->_model = new UserModel();
+ }
+ return $this->_model;
+ }
+
+ public function checkUser(array $formDatas): UserEntity
+ {
+ if (!isset($formDatas['id']) || !$formDatas['id']) {
+ throw new \Exception("사용자ID를 입력해주세요!");
+ }
+ if (!isset($formDatas['passwd']) || !$formDatas['passwd']) {
+ throw new \Exception("암호를 입력해주세요!");
+ }
+ $entity = $this->getModel()->getEntityByID($formDatas['id']);
+ if (is_null($entity) || !isset($entity->passwd)) {
+ throw new \Exception("사용자ID: {$formDatas['id']}가 존재하지 않습니다.");
+ }
+ if (!password_verify($formDatas['passwd'], $entity->passwd)) {
+ throw new \Exception("암호가 맞지 않습니다.");
+ }
+ return $entity;
+ }
+}
diff --git a/app/Libraries/MyAuth/MyAuth.php b/app/Libraries/MyAuth/MyAuth.php
new file mode 100644
index 0000000..72d1fbe
--- /dev/null
+++ b/app/Libraries/MyAuth/MyAuth.php
@@ -0,0 +1,26 @@
+_session = \Config\Services::session();
+ }
+
+ final public function setLogin(UserEntity $entity): void
+ {
+ $this->_session->set(SESSION_NAMES['ISLOGIN'], true);
+ $this->_session->set(SESSION_NAMES['AUTH'], [
+ 'uid' => $entity->getPK(),
+ 'name' => $entity->getTitle(),
+ 'role' => $entity->role
+ ]);
+ }
+}
diff --git a/app/Libraries/MySocket/CloudflareSocket.php b/app/Libraries/MySocket/CloudflareSocket.php
new file mode 100644
index 0000000..9c25534
--- /dev/null
+++ b/app/Libraries/MySocket/CloudflareSocket.php
@@ -0,0 +1,37 @@
+ 'https://api.cloudflare.com/client/v4/',
+ 'headers' => [
+ 'X-Auth-Email' => $auth_entity->getID(),
+ 'X-Auth-Key' => $auth_entity->getAuthKey(),
+ 'Content-Type' => 'application/json',
+ ]
+ ]);
+ }
+
+ final public function request(string $method, $uri = '', array $options = []): ResponseInterface
+ {
+ if (self::$_request >= self::$_request_max) {
+ log_message('warning', sprintf("--Cloudflare API Call %s초 대기 시작--", self::$_request_timewait));
+ sleep(intval(self::$_request_timewait));
+ self::$_request = 0;
+ log_message('warning', sprintf("--Cloudflare API Call %s초 대기 종료--", self::$_request_timewait));
+ }
+ self::$_request++;
+ return parent::request($method, $uri, $options);
+ }
+}
diff --git a/app/Libraries/MySocket/GoogleSocket.php b/app/Libraries/MySocket/GoogleSocket.php
new file mode 100644
index 0000000..d0a0ce0
--- /dev/null
+++ b/app/Libraries/MySocket/GoogleSocket.php
@@ -0,0 +1,42 @@
+session = Services::session();
+ $this->token_name = $token_name;
+ }
+
+ public function setToken(string $access_code): void
+ {
+ // 토큰 정보 가져오기
+ $tokenInfo = $this->fetchAccessTokenWithAuthCode($access_code);
+
+ if (isset($tokenInfo['error'])) {
+ throw new ConfigException($tokenInfo['error']);
+ }
+
+ $token = $tokenInfo[$this->token_name];
+
+ // Google Service에 접근하기 위해 Access Token 설정
+ $this->setAccessToken($token);
+
+ // 세션에 Token 값 설정
+ $this->session->set($this->token_name, $token);
+ }
+
+ public function getToken(): ?string
+ {
+ return $this->session->get($this->token_name);
+ }
+}
diff --git a/app/Libraries/MySocket/MySocket.php b/app/Libraries/MySocket/MySocket.php
new file mode 100644
index 0000000..b2e55d0
--- /dev/null
+++ b/app/Libraries/MySocket/MySocket.php
@@ -0,0 +1,80 @@
+_cookieJar === null) {
+ $this->_cookieJar = new CookieJar();
+ }
+ return $this->_cookieJar;
+ }
+ protected function getUserAgent(): string
+ {
+ // User-Agent 목록 배열
+ $userAgents = [
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1',
+ 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36'
+ ];
+ return $userAgents[array_rand($userAgents)];
+ }
+ protected function getRequestOptions(string $method, array $options = []): array
+ {
+ //cookies->쿠키값 , timeout->5초 안에 응답이 없으면 타임아웃
+ $requestOptions = [
+ 'cookies' => $this->getCookieJar(),
+ 'timeout' => getenv("socket.web.timeout"),
+ 'headers' => [
+ 'User-Agent' => $this->getUserAgent(),
+ ],
+ ];
+ return $requestOptions;
+ }
+
+ public function get($uri, array $options = []): ResponseInterface
+ {
+ return $this->request('GET', $uri, $options);
+ }
+ public function post($uri, array $options = []): ResponseInterface
+ {
+ return $this->request('POST', $uri, $options);
+ }
+ public function put($uri, array $options = []): ResponseInterface
+ {
+ return $this->request('PUT', $uri, $options);
+ }
+ public function patch($uri, array $options = []): ResponseInterface
+ {
+ return $this->request('PATCH', $uri, $options);
+ }
+ public function delete($uri, array $options = []): ResponseInterface
+ {
+ return $this->request('DELETE', $uri, $options);
+ }
+ public function request(string $method, $uri = '', array $options = []): ResponseInterface
+ {
+ $requestOptions = $this->getRequestOptions($method, $options);
+ $requestOptions[in_array($method, ['get', 'getAsync']) ? 'query' : 'json'] = $options;
+ log_message("debug", __FUNCTION__ .
+ "=> 호출 Socket URL:{$uri}\n--------------\n" .
+ var_export($options, true) .
+ "\n--------------\n");
+ return parent::request($method, $uri, $requestOptions);
+ }
+}
diff --git a/app/Libraries/MySocket/WebSocket.php b/app/Libraries/MySocket/WebSocket.php
new file mode 100644
index 0000000..76dcf11
--- /dev/null
+++ b/app/Libraries/MySocket/WebSocket.php
@@ -0,0 +1,39 @@
+_host = $host;
+ }
+ final public function getURL($uri): string
+ {
+ // url에 http 나 https가 포함되어 있지않으면
+ if (!preg_match('~^(http|https)://~i', $uri)) {
+ $uri = "{$this->_host}{$uri}";
+ }
+ return $uri;
+ }
+ public function getResponse($uri, array $options = []): ResponseInterface
+ {
+ $response = parent::get($this->getURL($uri), $options);
+ if ($response->getStatusCode() != 200) {
+ throw new \Exception("error", __FUNCTION__ .
+ "=> {$uri} 접속실패: " .
+ $response->getStatusCode());
+ }
+ return $response;
+ }
+ public function getContent(string $uri, array $options = []): string
+ {
+ $response = $this->getResponse($uri, $options);
+ // return $response->getBody()->getContents();
+ return $response->getBody();
+ }
+}
diff --git a/app/Libraries/MyStorage/FileStorage.php b/app/Libraries/MyStorage/FileStorage.php
new file mode 100644
index 0000000..6d43dc3
--- /dev/null
+++ b/app/Libraries/MyStorage/FileStorage.php
@@ -0,0 +1,111 @@
+_path = $path;
+ }
+ final public function getPath(): string
+ {
+ return $this->_path;
+ }
+ public function getUploadPath(): string
+ {
+ return "uploads";
+ }
+ final public function getFullPath(): string
+ {
+ $full_path = WRITEPATH . $this->getUploadPath() . DIRECTORY_SEPARATOR . $this->getPath();
+ $this->mkdir_FileTrait($full_path);
+ return $full_path;
+ }
+ public function getUploadURL(): string
+ {
+ return "uploads";
+ }
+ final public function getOriginName(): string
+ {
+ return $this->_originName;
+ }
+ final public function setOriginName(string $originName): void
+ {
+ $this->_originName = $originName;
+ }
+ final public function getOriginContent(): string
+ {
+ return $this->_originContent;
+ }
+ final public function setOriginContent(string $originContent): void
+ {
+ $this->_originContent = $originContent;
+ }
+ final public function getOriginMediaTag(): string
+ {
+ return $this->_originMediaTag;
+ }
+ final public function setOriginMediaTag(string $originMediaTag): void
+ {
+ $this->_originMediaTag = $originMediaTag;
+ }
+ final public function getOriginSequence(): int
+ {
+ return $this->_originSequence;
+ }
+ final public function setOriginSequence(int $originSequence): void
+ {
+ $this->_originSequence = $originSequence;
+ }
+ final public function getMimeType(): string
+ {
+ return $this->_mimeType;
+ }
+ final public function getFileSize(): int
+ {
+ return $this->_fileSize;
+ }
+ public function save(): static
+ {
+ // log_message("notice", __FUNCTION__ . " 원본파일 {$this->getOriginName()} 작업 시작 2");
+ $save_file = $this->getFullPath() . DIRECTORY_SEPARATOR . $this->getOriginName();
+ log_message("debug", __FUNCTION__ . " {$save_file} 작업 시작");
+ //중복된 파일명인지 확인후 새로운 이름으로 저장
+ if (file_exists($save_file)) {
+ switch (getenv("mangboard.uploads.file.collision")) {
+ case "unique":
+ $file_name = $this->getUniqueName_FileTrait($this->getFullPath(), $this->getOriginName());
+ log_message("notice", __FUNCTION__ . " 파일명 변경 : 원본파일 {$this->getOriginName()}->저장파일 {$file_name}");
+ $this->setOriginName($file_name);
+ $save_file = $this->getFullPath() . DIRECTORY_SEPARATOR . $this->getOriginName();
+ break;
+ case "notallow":
+ default:
+ throw new \Exception(__FUNCTION__ . " {$this->getOriginName()} 는 이미 존재하는 파일입니다.");
+ // break;
+ }
+ }
+ //원본이미지 저장
+ if (!file_put_contents($save_file, $this->getOriginContent())) {
+ throw new \Exception(__FUNCTION__ . " 파일저장 실패:{$save_file}");
+ }
+ $this->_mimeType = mime_content_type($save_file);
+ $this->_fileSize = filesize($save_file);
+ log_message("notice", __FUNCTION__ . " 원본파일 {$this->getOriginName()} 작업 완료");
+ return $this;
+ }
+}
diff --git a/app/Models/.gitkeep b/app/Models/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Models/Cloudflare/AccountModel.php b/app/Models/Cloudflare/AccountModel.php
new file mode 100644
index 0000000..d253626
--- /dev/null
+++ b/app/Models/Cloudflare/AccountModel.php
@@ -0,0 +1,86 @@
+table}.{$field}]" : "";
+ break;
+ case self::PARENT:
+ $rule = "required|numeric";
+ break;
+ case self::TITLE:
+ $rule = "required|trim|string";
+ break;
+ case "type":
+ $rule = "if_exist|in_list[standard,enterprise]";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getFormFieldOption(string $field, array $options = []): array
+ {
+ switch ($field) {
+ default:
+ $this->orderBy(self::TITLE, 'asc');
+ $options = parent::getFormFieldOption($field, $options);
+ break;
+ }
+ return $options;
+ }
+ public function getEntityByPK(string $uid): null|AccountEntity
+ {
+ $this->where($this->getPKField(), $uid);
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|AccountEntity
+ {
+ $this->where(self::TITLE, $id);
+ return $this->getEntity();
+ }
+ public function getEntitysByParent(AuthEntity $auth_entity)
+ {
+ $this->where(self::PARENT, $auth_entity->getPK());
+ return $this->getEntitys();
+ }
+ //create용
+ public function create(array $formDatas = []): AccountEntity
+ {
+ return $this->create_process(new AccountEntity(), $formDatas);
+ }
+ //modify용
+ public function modify(AccountEntity $entity, array $formDatas): AccountEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+}
diff --git a/app/Models/Cloudflare/AuthModel.php b/app/Models/Cloudflare/AuthModel.php
new file mode 100644
index 0000000..66ec28b
--- /dev/null
+++ b/app/Models/Cloudflare/AuthModel.php
@@ -0,0 +1,77 @@
+table}.{$field}]" : "";
+ break;
+ case "authkey":
+ $rule = "required|trim|string";
+ $rule .= $action == "create" ? "|is_unique[{$this->table}.{$field}]" : "";
+ break;
+ case "oldkey":
+ $rule = "if_exist|trim|string";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getFormFieldOption(string $field, array $options = []): array
+ {
+ switch ($field) {
+ default:
+ $this->orderBy(self::PK, 'asc');
+ $options = parent::getFormFieldOption($field, $options);
+ break;
+ }
+ return $options;
+ }
+ public function getEntityByPK(string $uid): null|AuthEntity
+ {
+ $this->where($this->getPKField(), intval($uid));
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|AuthEntity
+ {
+ $this->where(self::TITLE, $id);
+ return $this->getEntity();
+ }
+ //create용
+ public function create(array $formDatas = []): AuthEntity
+ {
+ return $this->create_process(new AuthEntity(), $formDatas);
+ }
+ //modify용
+ public function modify(AuthEntity $entity, array $formDatas): AuthEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+}
diff --git a/app/Models/Cloudflare/RecordModel.php b/app/Models/Cloudflare/RecordModel.php
new file mode 100644
index 0000000..9d7212f
--- /dev/null
+++ b/app/Models/Cloudflare/RecordModel.php
@@ -0,0 +1,134 @@
+table}.{$field}]" : "";
+ break;
+ case self::PARENT:
+ $rule = "required|trim|alpha_numeric";
+ break;
+ case self::TITLE:
+ case "content":
+ $rule = "required|trim|string";
+ break;
+ case "type":
+ $rule = "required|in_list[A,AAAA,CNAME,NS,MX,PTR,SPF,TXT,SRV,INFO]";
+ break;
+ case "ttl":
+ $rule = "if_exist|numeric";
+ break;
+ case "proxied":
+ $rule = "required|in_list[on,off]";
+ break;
+ case "proxiable":
+ case "fixed":
+ case "locked":
+ $rule = "if_exist|in_list[on,off]";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getFormFieldInputOption(string $field, array $options = []): array
+ {
+ switch ($field) {
+ default:
+ $this->orderBy(self::TITLE, 'asc');
+ $options = parent::getFormFieldInputOption($field, $options);
+ break;
+ }
+ return $options;
+ }
+ public function getEntityByPK(string $uid): null|RecordEntity
+ {
+ $this->where(self::PK, $uid);
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|RecordEntity
+ {
+ $this->where(self::TITLE, $id);
+ return $this->getEntity();
+ }
+ public function getEntitysByParent(ZoneEntity $zone_entity)
+ {
+ $this->where(self::PARENT, $zone_entity->getPK());
+ return $this->getEntitys();
+ }
+ //create용
+ public function create(array $formDatas = []): RecordEntity
+ {
+ return $this->create_process(new RecordEntity(), $formDatas);
+ }
+ //modify용
+ public function modify(RecordEntity $entity, array $formDatas): RecordEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+ //도메인이 이미 존재하는지 체크
+ public function isUniqueHost($zone_uid, string $host, string $content): bool
+ {
+ $this->where(self::PARENT, $zone_uid);
+ $this->where('host', $host);
+ $this->where('content', $content);
+ return is_null($this->first()) ? true : false;
+ }
+ //CDN값 수정 못하는 고정 Record 처리
+ public function setImmobilized(array $hosts)
+ {
+ if (count($hosts)) {
+ $this->whereIn('host', $hosts)->set(['fixed' => 'on'])->update();
+ log_message("notice", "-----set fixed Records " . implode(",", $hosts) . "처리 완료-----");
+ }
+ }
+ //List 검색용
+ public function setList_WordFilter(string $word, $field = null): void
+ {
+ parent::setList_WordFilter($word, $field);
+ $this->orLike('content', $word, 'both');
+ }
+ public function setList_OrderBy(string $order = ""): void
+ {
+ //Join을 해서 도메인부터 Sorting하기위함
+ $this->join(ZoneModel::TABLE, sprintf(
+ "%s.%s=%s.%s",
+ self::TABLE,
+ self::PARENT,
+ ZoneModel::TABLE,
+ ZoneModel::PK
+ ));
+ $this->orderBy(ZoneModel::TABLE . "." . ZoneModel::TITLE . " ASC ," . self::TITLE . " ASC");
+ parent::setList_OrderBy($order);
+ }
+}
diff --git a/app/Models/Cloudflare/ZoneModel.php b/app/Models/Cloudflare/ZoneModel.php
new file mode 100644
index 0000000..c510ed8
--- /dev/null
+++ b/app/Models/Cloudflare/ZoneModel.php
@@ -0,0 +1,138 @@
+table}.{$field}]" : "";
+ break;
+ case self::PARENT:
+ $rule = "required|trim|alpha_numeric";
+ break;
+ case self::TITLE:
+ case "plan":
+ $rule = "required|trim|string";
+ break;
+ case "name_servers":
+ case "security_level":
+ $rule = "if_exist|trim|string";
+ break;
+ case "development_mode":
+ case "ipv6":
+ $rule = "if_exist|in_list[on,off]";
+ break;
+ case "status":
+ $rule = "if_exist|in_list[active,pending,moved]";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getFormFieldOption(string $field, array $options = []): array
+ {
+ switch ($field) {
+ default:
+ $this->orderBy(self::TITLE, 'asc');
+ $options = parent::getFormFieldOption($field, $options);
+ break;
+ }
+ return $options;
+ }
+ public function getEntityByPK(string $uid): null|ZoneEntity
+ {
+ $this->where(self::PK, $uid);
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|ZoneEntity
+ {
+ $this->where(self::TITLE, $id);
+ return $this->getEntity();
+ }
+ public function getEntitysByParent(AccountEntity $account_entity)
+ {
+ $this->where(self::PARENT, $account_entity->getPK());
+ return $this->getEntitys();
+ }
+ //create용
+ public function create(array $formDatas = []): ZoneEntity
+ {
+ return $this->create_process(new ZoneEntity(), $formDatas);
+ }
+ //modify용
+ public function modify(ZoneEntity $entity, array $formDatas): ZoneEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+
+ //도메인이 이미 존재하는지 체크
+ public function isUniqueDomain(AccountEntity $account_entity, string $domain): bool
+ {
+ $this->where(self::PARENT, $account_entity->getPK());
+ $this->where(self::TITLE, $domain);
+ return is_null($this->first()) ? true : false;
+ }
+ //List 검색용
+ public function setList_WordFilter(string $word, $field = null): void
+ {
+ //Record의 content(IP검색)을 하기위함
+ //Join 방식사용
+ $this->select(self::TABLE . '.*')->distinct();
+ $this->join(RecordModel::TABLE, sprintf(
+ "%s.%s=%s.%s",
+ self::TABLE,
+ self::PK,
+ RecordModel::TABLE,
+ RecordModel::PARENT
+ ));
+ parent::setList_WordFilter($word, $field);
+ $this->orLike(RecordModel::TABLE . '.content', $word, 'both');
+ //Subquery 방식사용
+ // $recordModel = new RecordModel();
+ // $recordModel->like(RecordModel::TABLE . '.content', $word, 'both');
+ // $zone_uids = $recordModel->select(RecordModel::PARENT)->findAll();
+ // $zone_uids = array_column($zone_uids, RecordModel::PARENT);
+ // $this->orWhereIn(self::TABLE . '.' . self::PK, array_values($zone_uids));
+ }
+ public function setList_OrderBy(string $order = ""): void
+ {
+ //Join을 해서 도메인부터 Sorting하기위함
+ $this->join(AccountModel::TABLE, sprintf(
+ "%s.%s=%s.%s",
+ self::TABLE,
+ self::PARENT,
+ AccountModel::TABLE,
+ AccountModel::PK
+ ));
+ $this->orderBy(AccountModel::TABLE . "." . AccountModel::TITLE . " ASC ," . self::TITLE . " ASC");
+ parent::setList_OrderBy($order);
+ }
+}
diff --git a/app/Models/CommonModel.php b/app/Models/CommonModel.php
new file mode 100644
index 0000000..4fc2ea1
--- /dev/null
+++ b/app/Models/CommonModel.php
@@ -0,0 +1,230 @@
+table;
+ }
+ final public function getPKField(): string
+ {
+ return $this->primaryKey;
+ }
+ // final public function getFields(array $except_fields = []): array
+ // {
+ // return array_diff($this->allowedFields, $except_fields); //제외한 fields
+ // }
+ public function getFieldRule(string $action, string $field): string
+ {
+ if (is_array($field)) {
+ throw new \Exception(__FUNCTION__ . "=> field가 array 입니다.\n" . var_export($field, true));
+ }
+ switch ($field) {
+ case $this->getPKField():
+ //수동입력인경우
+ if (!$this->useAutoIncrement) {
+ $rule = "required|regex_match[/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/]";
+ $rule .= $action == "create" ? "|is_unique[{$this->table}.{$field}]" : "";
+ } else {
+ $rule = "required|numeric";
+ };
+ break;
+ case $this->getTitleField():
+ $rule = "required|string";
+ break;
+ case 'image':
+ $rule = "is_image[{$field}]|mime_in[{$field},image/jpg,image/jpeg,image/gif,image/png,image/webp]|max_size[{$field},300]|max_dims[{$field},2048,768]";
+ break;
+ case "status":
+ $rule = "if_exist|in_list[use,unuse]";
+ break;
+ case "updated_at":
+ case "created_at":
+ case "deleted_at":
+ $rule = "if_exist|valid_date";
+ break;
+ default:
+ $rule = "if_exist|string";
+ break;
+ }
+ return $rule;
+ }
+ final public function getFieldRules(string $action, array $fields, $rules = []): array
+ {
+ foreach ($fields as $field) {
+ $rules[$field] = $this->getFieldRule($action, $field);
+ }
+ return $rules;
+ }
+ public function getFormFieldOption(string $field, array $options = []): array
+ {
+ switch ($field) {
+ default:
+ foreach ($this->getEntitys() as $entity) {
+ $options[$entity->getPK()] = $entity->getTitle();
+ }
+ break;
+ }
+ // dd($options);
+ // exit;
+ return $options;
+ }
+ final public function getEntity(): array|object|null
+ {
+ return $this->asObject($this->returnType)->first();
+ }
+ final public function getEntitys(): array
+ {
+ return $this->asObject($this->returnType)->findAll();
+ }
+ //create , modify 직전 작업용 작업
+ protected function convertEntityData(string $field, array $formDatas): mixed
+ {
+ switch ($field) {
+ case $this->getPKField():
+ //$formDatas에 전달된 값이 없는경우
+ if (!array_key_exists($field, $formDatas)) {
+ $randomBytes = bin2hex(random_bytes(32));
+ $value = sprintf(
+ '%08s-%04s-%04x-%04x-%12s',
+ substr($randomBytes, 0, 8),
+ substr($randomBytes, 8, 4),
+ substr($randomBytes, 12, 4),
+ substr($randomBytes, 16, 4),
+ substr($randomBytes, 20, 12)
+ );
+ } else {
+ $value = $formDatas[$field];
+ }
+ break;
+ case "editor": //content등 textarea를 사용한 Field
+ $value = htmlentities($formDatas[$field], ENT_QUOTES);
+ break;
+ default:
+ $value = $formDatas[$field];
+ break;
+ }
+ return $value;
+ }
+
+ private function save_process($entity): mixed
+ {
+ //최종 변경사항이 있으면 저장
+ if ($entity->hasChanged()) {
+ if (!$this->save($entity)) {
+ throw new \Exception(sprintf(
+ "\n------%s SQL오류-----\n%s\n%s\n------------------------------\n",
+ __FUNCTION__,
+ $this->getLastQuery(),
+ var_export($this->errors(), true)
+ ));
+ }
+ log_message("notice", $this->getTable() . " => " . __FUNCTION__ . " DB 저장이 완료되었습니다.");
+ } else {
+ log_message("notice", __FUNCTION__ . " 변경된 내용이 없습니다.");
+ }
+ return $entity;
+ }
+ final protected function create_process($entity, array $formDatas): mixed
+ {
+ //Field에 맞는 Validation Rule 재정의
+ $this->setValidationRules($this->getFieldRules('create', $this->allowedFields));
+ //저장하기 전에 데이터 값 변경이 필요한 Field
+ foreach (array_keys($formDatas) as $field) {
+ $entity->$field = $this->convertEntityData($field, $formDatas);
+ }
+ $entity = $this->save_process($entity);
+ //primaryKey가 자동입력이면
+ if ($this->useAutoIncrement) {
+ $pkField = $this->getPKField();
+ $entity->$pkField = $this->getInsertID();
+ }
+ log_message("debug", $this->getTable() . " => " . __FUNCTION__ . " DB 작업 완료");
+ return $entity;
+ }
+ final protected function modify_process($entity, array $formDatas): mixed
+ {
+ //Field에 맞는 Validation Rule 재정의
+ $this->setValidationRules($this->getFieldRules('modify', $this->allowedFields));
+ //저장하기 전에 데이터 값 변경이 필요한 Field
+ foreach (array_keys($formDatas) as $field) {
+ $entity->$field = $this->convertEntityData($field, $formDatas);
+ }
+ $this->save_process($entity);
+ log_message("debug", $this->getTable() . " => " . __FUNCTION__ . " DB 작업 완료");
+ return $entity;
+ }
+
+ //List용
+ final public function setList_FieldFilter(string $field, int|string $value): void
+ {
+ $this->where($field, $value);
+ }
+ public function setList_WordFilter(string $word, string $field = null): void
+ {
+ $this->like($field ?? $this->getTitleField(), $word, 'both'); //befor , after , both
+ }
+ final public function setList_DateFilter(string $start, string $end, $field = "created_at"): void
+ {
+ if ($start !== DEFAULTS['EMPTY']) {
+ $this->where("{$field} >= '{$start} 00:00:00'");
+ }
+ if ($end !== DEFAULTS['EMPTY']) {
+ $this->where("{$field} <= '{$end} 23:59:59'");
+ }
+ }
+ public function setList_OrderBy(string $order = ""): void
+ {
+ if ($order) {
+ $this->orderBy($order);
+ }
+ }
+}
diff --git a/app/Models/MapurlModel.php b/app/Models/MapurlModel.php
new file mode 100644
index 0000000..9c683aa
--- /dev/null
+++ b/app/Models/MapurlModel.php
@@ -0,0 +1,64 @@
+ field가 array 입니다.\n" . var_export($field, true));
+ }
+ switch ($field) {
+ case "oldurl":
+ $rule = "required|valid_url_strict";
+ $rule .= $action == "create" ? "|is_unique[{$this->table}.{$field}]" : "";
+ break;
+ case "newurl":
+ $rule = "required|valid_url_strict";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getEntityByPK(string $uid): null|MapurlEntity
+ {
+ $this->where($this->getPKField(), intval($uid));
+ return $this->getEntity();
+ }
+ //create용
+ public function create(array $formDatas = []): MapurlEntity
+ {
+ return $this->create_process(new MapurlEntity(), $formDatas);
+ }
+
+ //modify용
+ public function modify(MapurlEntity $entity, array $formDatas): MapurlEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+}
diff --git a/app/Models/SNSUserModel.php b/app/Models/SNSUserModel.php
new file mode 100644
index 0000000..becee9d
--- /dev/null
+++ b/app/Models/SNSUserModel.php
@@ -0,0 +1,76 @@
+table}.{$field}]";
+ break;
+ case $this->getTitleField():
+ $rule = "required|trim|string";
+ break;
+ case "site":
+ $rule = "required|trim|string";
+ break;
+ case "email":
+ $rule = "if_exist|trim|valid_email";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ public function getEntityByPK(string $uid): null|SNSUSerEntity
+ {
+ $this->where($this->getPKField(), intval($uid));
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|SNSUSerEntity
+ {
+ $this->where('id', $id);
+ return $this->getEntity();
+ }
+
+ //create용
+ public function create(array $formDatas = []): SNSUSerEntity
+ {
+ return $this->create_process(new SNSUSerEntity(), $formDatas);
+ }
+
+ //modify용
+ public function modify(SNSUSerEntity $entity, array $formDatas): SNSUSerEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+}
diff --git a/app/Models/UserModel.php b/app/Models/UserModel.php
new file mode 100644
index 0000000..de31937
--- /dev/null
+++ b/app/Models/UserModel.php
@@ -0,0 +1,98 @@
+ field가 array 입니다.\n" . var_export($field, true));
+ }
+ switch ($field) {
+ case "id":
+ $rule = "required|trim|min_length[4]|max_length[20]";
+ $rule .= $action == "create" ? "|is_unique[{$this->table}.{$field}]" : "";
+ break;
+ case "passwd":
+ $rule = "required|trim|string";
+ break;
+ case "confirmpassword":
+ $rule = "required|trim|string|matches[passwd]";
+ break;
+ case "email":
+ $rule = "required|trim|valid_email";
+ break;
+ case "role":
+ $rule = "required|trim|string";
+ break;
+ default:
+ $rule = parent::getFieldRule($action, $field);
+ break;
+ }
+ return $rule;
+ }
+ protected function convertEntityData(string $field, array $formDatas): mixed
+ {
+ switch ($field) {
+ case "passwd":
+ $value = password_hash($formDatas[$field], PASSWORD_DEFAULT);
+ break;
+ case "confirmpassword":
+ $value = password_hash($formDatas[$field], PASSWORD_DEFAULT);
+ break;
+ default:
+ $value = parent::convertEntityData($field, $formDatas);
+ break;
+ }
+ return $value;
+ }
+
+ public function getEntityByPK(string $uid): null|UserEntity
+ {
+ $this->where($this->getPKField(), intval($uid));
+ return $this->getEntity();
+ }
+ public function getEntityByID(string $id): null|UserEntity
+ {
+ $this->where('id', $id);
+ return $this->getEntity();
+ }
+
+ //create용
+ public function create(array $formDatas = []): UserEntity
+ {
+ return $this->create_process(new UserEntity(), $formDatas);
+ }
+ //modify용
+ public function modify(UserEntity $entity, array $formDatas): UserEntity
+ {
+ return $this->modify_process($entity, $formDatas);
+ }
+}
diff --git a/app/ThirdParty/.gitkeep b/app/ThirdParty/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Traits/FileTrait.php b/app/Traits/FileTrait.php
new file mode 100644
index 0000000..3dea2f4
--- /dev/null
+++ b/app/Traits/FileTrait.php
@@ -0,0 +1,62 @@
+isFileType_FileTrait($file_ext, $type)) {
+ $files[] = $file_name;
+ }
+ }
+ return $files;
+ }
+
+ final protected function getUniqueName_FileTrait($path, $file_name)
+ {
+ $fileExtension = pathinfo($file_name, PATHINFO_EXTENSION);
+ $fileBaseName = pathinfo($file_name, PATHINFO_FILENAME);
+ $newFilename = $file_name;
+ $counter = 1;
+ // 중복된 파일명이 존재하는지 확인
+ while (file_exists($path . DIRECTORY_SEPARATOR . $newFilename)) {
+ // 중복된 파일명이 존재하면 숫자를 추가하여 새로운 파일명 생성
+ $newFilename = $fileBaseName . '_' . $counter . '.' . $fileExtension;
+ $counter++;
+ }
+ return $newFilename;
+ }
+}
diff --git a/app/Traits/ImageTrait.php b/app/Traits/ImageTrait.php
new file mode 100644
index 0000000..957bc78
--- /dev/null
+++ b/app/Traits/ImageTrait.php
@@ -0,0 +1,103 @@
+_image);
+ }
+ // 이미지의 현재 높이를 반환하는 메소드
+ final public function getHeight_ImageTrait()
+ {
+ return imagesy($this->_image);
+ }
+ // 이미지 파일을 로드하는 메소드
+ final public function load_ImageTrait($file)
+ {
+ $imageInfo = getimagesize($file);
+ $this->_imageType = $imageInfo[2];
+ switch ($this->_imageType) {
+ case IMAGETYPE_JPEG:
+ $this->_image = imagecreatefromjpeg($file);
+ break;
+ case IMAGETYPE_GIF:
+ $this->_image = imagecreatefromgif($file);
+ break;
+ case IMAGETYPE_PNG:
+ $this->_image = imagecreatefrompng($file);
+ break;
+ case IMAGETYPE_WEBP:
+ $this->_image = imagecreatefromwebp($file);
+ break;
+ }
+ }
+ // 이미지 크기를 지정된 너비, 높이로 변경하는 메소드
+ final public function resize_ImageTrait($width, $height)
+ {
+ $newImage = imagecreatetruecolor($width, $height);
+ imagecopyresampled(
+ $newImage,
+ $this->_image,
+ 0,
+ 0,
+ 0,
+ 0,
+ $width,
+ $height,
+ $this->getWidth_ImageTrait(),
+ $this->getHeight_ImageTrait()
+ );
+ $this->_image = $newImage;
+ }
+ // 이미지 비율을 유지하면서 크기를 조정하는 메소드
+ final public function resizeToWidth_ImageTrait($width)
+ {
+ $ratio = $width / $this->getWidth_ImageTrait();
+ $height = $this->getHeight_ImageTrait() * $ratio;
+ $this->resize_ImageTrait($width, $height);
+ }
+ final public function resizeToHeight_ImageTrait($height)
+ {
+ $ratio = $height / $this->getHeight_ImageTrait();
+ $width = $this->getWidth_ImageTrait() * $ratio;
+ $this->resize_ImageTrait($width, $height);
+ }
+ final public function scale($scale)
+ {
+ $width = $this->getWidth_ImageTrait() * ($scale / 100);
+ $height = $this->getHeight_ImageTrait() * ($scale / 100);
+ $this->resize_ImageTrait($width, $height);
+ }
+ // 이미지를 저장하는 메소드
+ final public function save_ImageTrait($file, $imageType = IMAGETYPE_WEBP, $compression = 75)
+ {
+ switch ($imageType) {
+ case IMAGETYPE_JPEG:
+ imagejpeg($this->_image, $file, $compression);
+ break;
+ case IMAGETYPE_GIF:
+ imagegif($this->_image, $file);
+ break;
+ case IMAGETYPE_PNG:
+ imagepng($this->_image, $file);
+ break;
+ case IMAGETYPE_WEBP:
+ default:
+ imagewebp($this->_image, $file, $compression);
+ break;
+ }
+ }
+ // 메모리 해제를 위한 메소드
+ final public function destroy_ImageTrait()
+ {
+ imagedestroy($this->_image);
+ }
+}
diff --git a/app/Views/admin/create.php b/app/Views/admin/create.php
new file mode 100644
index 0000000..66c39bf
--- /dev/null
+++ b/app/Views/admin/create.php
@@ -0,0 +1,26 @@
+= $this->extend("layouts/{$viewDatas['layout']}/{$viewDatas['action_form']}") ?>
+= $this->section('content') ?>
+
+= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/admin/index.php b/app/Views/admin/index.php
new file mode 100644
index 0000000..6a153c9
--- /dev/null
+++ b/app/Views/admin/index.php
@@ -0,0 +1,103 @@
+= $this->extend("layouts/{$viewDatas['layout']}") ?>
+= $this->section('content') ?>
+
+
+= form_open(current_url(), array("method" => "get")) ?>
+
+
+
+ 조건검색:
+
+ = $viewDatas['helper']->getFieldForm($field, $viewDatas[$field] ?: old($field), $viewDatas) ?>
+
+
+
+ 검색어:= form_input('word', $viewDatas['word']) ?>
+ 검색일:= form_input('start', $viewDatas['start'], ["class" => "calender"]) ?>= form_input('end', $viewDatas['end'], ["class" => "calender"]) ?>
+ = form_submit('', '검색하기') ?>
+ = anchor(current_url() . '/download/excel', ICONS['EXCEL'], ["target" => "_self", "class" => "excel"]) ?>
+
+
+ 페이지 : = $viewDatas['page'] ?>/= $viewDatas['total_page'] ?>
+ = form_dropdown('per_page', $viewDatas['page_options'], $viewDatas['per_page'], array('onChange' => 'this.form.submit()')) ?>
+ / 총:= $viewDatas['total_count'] ?>
+
+
+
+= form_close() ?>
+
+
+
+
+ 번호
+
+ = $viewDatas['helper']->getListLabel($field, $viewDatas) ?>
+
+ 작업
+
+
+
+
+
+ getListRowColor($entity) ?>>
+
+
+
+ = $viewDatas['helper']->getListButton('modify', $viewDatas) ?>
+
+
+ = $viewDatas['helper']->getFieldView($field, $viewDatas) ?>
+
+
+ = $viewDatas['helper']->getListButton('delete', $viewDatas) ?>
+
+
+
+
+
+
+
+
+ = form_open(current_url() . '/batchjob', $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
+
+ = form_checkbox(array("id" => "batchjobuids_checkbox")) ?>ALL
+
+ = $viewDatas['helper']->getFieldForm($field, DEFAULTS['EMPTY'], $viewDatas, ["id" => $field]) ?>
+
+ = $viewDatas['helper']->getListButton('batchjob', $viewDatas) ?>
+ = $viewDatas['helper']->getListButton('create', $viewDatas) ?>
+
+ = form_close() ?>
+
+
+
+
+ = $this->include('templates/' . $viewDatas['layout'] . '/' . $viewDatas['action_form']); ?>
+
+
+
+
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/admin/mapurl/remap_template.php b/app/Views/admin/mapurl/remap_template.php
new file mode 100644
index 0000000..d5d7349
--- /dev/null
+++ b/app/Views/admin/mapurl/remap_template.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/admin/modify.php b/app/Views/admin/modify.php
new file mode 100644
index 0000000..f30ec7e
--- /dev/null
+++ b/app/Views/admin/modify.php
@@ -0,0 +1,26 @@
+= $this->extend("layouts/{$viewDatas['layout']}/{$viewDatas['action_form']}") ?>
+= $this->section('content') ?>
+
+= form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/admin/welcome_message.php b/app/Views/admin/welcome_message.php
new file mode 100644
index 0000000..3b71da9
--- /dev/null
+++ b/app/Views/admin/welcome_message.php
@@ -0,0 +1,5 @@
+= $this->extend('layouts/admin') ?>
+= $this->section('content') ?>
+= $this->include('templates/admin/header'); ?>
+= $this->include('templates/admin/footer'); ?>
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/errors/cli/error_404.php b/app/Views/errors/cli/error_404.php
new file mode 100644
index 0000000..456ea3e
--- /dev/null
+++ b/app/Views/errors/cli/error_404.php
@@ -0,0 +1,7 @@
+getFile()) . ':' . $exception->getLine(), 'green'));
+CLI::newLine();
+
+$last = $exception;
+
+while ($prevException = $last->getPrevious()) {
+ $last = $prevException;
+
+ CLI::write(' Caused by:');
+ CLI::write(' [' . $prevException::class . ']', 'red');
+ CLI::write(' ' . $prevException->getMessage());
+ CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
+ CLI::newLine();
+}
+
+// The backtrace
+if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
+ $backtraces = $last->getTrace();
+
+ if ($backtraces) {
+ CLI::write('Backtrace:', 'green');
+ }
+
+ foreach ($backtraces as $i => $error) {
+ $padFile = ' '; // 4 spaces
+ $padClass = ' '; // 7 spaces
+ $c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
+
+ if (isset($error['file'])) {
+ $filepath = clean_path($error['file']) . ':' . $error['line'];
+
+ CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
+ } else {
+ CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
+ }
+
+ $function = '';
+
+ if (isset($error['class'])) {
+ $type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
+ $function .= $padClass . $error['class'] . $type . $error['function'];
+ } elseif (! isset($error['class']) && isset($error['function'])) {
+ $function .= $padClass . $error['function'];
+ }
+
+ $args = implode(', ', array_map(static fn ($value) => match (true) {
+ is_object($value) => 'Object(' . $value::class . ')',
+ is_array($value) => count($value) ? '[...]' : '[]',
+ $value === null => 'null', // return the lowercased version
+ default => var_export($value, true),
+ }, array_values($error['args'] ?? [])));
+
+ $function .= '(' . $args . ')';
+
+ CLI::write($function);
+ CLI::newLine();
+ }
+}
diff --git a/app/Views/errors/cli/production.php b/app/Views/errors/cli/production.php
new file mode 100644
index 0000000..7db744e
--- /dev/null
+++ b/app/Views/errors/cli/production.php
@@ -0,0 +1,5 @@
+
+
+
+
+ = lang('Errors.pageNotFound') ?>
+
+
+
+
+
+
404
+
+
+
+ = nl2br(esc($message)) ?>
+
+ = lang('Errors.sorryCannotFind') ?>
+
+
+
+
+
diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php
new file mode 100644
index 0000000..44d7498
--- /dev/null
+++ b/app/Views/errors/html/error_exception.php
@@ -0,0 +1,430 @@
+
+
+
+
+
+
+
+ = esc($title) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= esc(clean_path($file)) ?> at line = esc($line) ?>
+
+
+
+ = static::highlightFile($file, $line, 15); ?>
+
+
+
+
+
+ getPrevious()) {
+ $last = $prevException;
+ ?>
+
+
+ Caused by:
+ = esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
+
+ = nl2br(esc($prevException->getMessage())) ?>
+ getMessage())) ?>"
+ rel="noreferrer" target="_blank">search →
+ = esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $row) : ?>
+
+
+
+
+
+
+
+ {PHP internal code}
+
+
+
+
+ — = esc($row['class'] . $row['type'] . $row['function']) ?>
+
+
+ ( arguments )
+
+
+
+ getParameters();
+ }
+
+ foreach ($row['args'] as $key => $value) : ?>
+
+ = esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?>
+ = esc(print_r($value, true)) ?>
+
+
+
+
+
+
+ ()
+
+
+
+
+ — = esc($row['function']) ?>()
+
+
+
+
+
+
+ = static::highlightFile($row['file'], $row['line']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
$= esc($var) ?>
+
+
+
+
+ Key
+ Value
+
+
+
+ $value) : ?>
+
+ = esc($key) ?>
+
+
+ = esc($value) ?>
+
+ = esc(print_r($value, true)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
Constants
+
+
+
+
+ Key
+ Value
+
+
+
+ $value) : ?>
+
+ = esc($key) ?>
+
+
+ = esc($value) ?>
+
+ = esc(print_r($value, true)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Path
+ = esc($request->getUri()) ?>
+
+
+ HTTP Method
+ = esc($request->getMethod()) ?>
+
+
+ IP Address
+ = esc($request->getIPAddress()) ?>
+
+
+ Is AJAX Request?
+ = $request->isAJAX() ? 'yes' : 'no' ?>
+
+
+ Is CLI Request?
+ = $request->isCLI() ? 'yes' : 'no' ?>
+
+
+ Is Secure Request?
+ = $request->isSecure() ? 'yes' : 'no' ?>
+
+
+ User Agent
+ = esc($request->getUserAgent()->getAgentString()) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
$= esc($var) ?>
+
+
+
+
+ Key
+ Value
+
+
+
+ $value) : ?>
+
+ = esc($key) ?>
+
+
+ = esc($value) ?>
+
+ = esc(print_r($value, true)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ No $_GET, $_POST, or $_COOKIE Information to show.
+
+
+
+
+ headers(); ?>
+
+
+
Headers
+
+
+
+
+ Header
+ Value
+
+
+
+ $value) : ?>
+
+ = esc($name, 'html') ?>
+
+ getValueLine(), 'html');
+ } else {
+ foreach ($value as $i => $header) {
+ echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+ setStatusCode(http_response_code());
+ ?>
+
+
+
+ Response Status
+ = esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?>
+
+
+
+ headers(); ?>
+
+
Headers
+
+
+
+
+ Header
+ Value
+
+
+
+ $value) : ?>
+
+ = esc($name, 'html') ?>
+
+ getHeaderLine($name), 'html');
+ } else {
+ foreach ($value as $i => $header) {
+ echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = esc(clean_path($file)) ?>
+
+
+
+
+
+
+
+
+
+
+ Memory Usage
+ = esc(static::describeMemory(memory_get_usage(true))) ?>
+
+
+ Peak Memory Usage:
+ = esc(static::describeMemory(memory_get_peak_usage(true))) ?>
+
+
+ Memory Limit:
+ = esc(ini_get('memory_limit')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/errors/html/production.php b/app/Views/errors/html/production.php
new file mode 100644
index 0000000..2f59a8d
--- /dev/null
+++ b/app/Views/errors/html/production.php
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ = lang('Errors.whoops') ?>
+
+
+
+
+
+
+
+
= lang('Errors.whoops') ?>
+
+
= lang('Errors.weHitASnag') ?>
+
+
+
+
+
+
diff --git a/app/Views/front/login.php b/app/Views/front/login.php
new file mode 100644
index 0000000..375745a
--- /dev/null
+++ b/app/Views/front/login.php
@@ -0,0 +1,22 @@
+= $this->extend("layouts/{$viewDatas['layout']}/{$viewDatas['action_form']}") ?>
+= $this->section('content') ?>
+
+
+ = form_open(current_url(), $viewDatas['forms']['attributes'], $viewDatas['forms']['hiddens']) ?>
+
로그인
+
+ 아이디
+
+
+
+ 비밀번호
+
+
+
+ 로그인
+ = anchor($viewDatas['google_url'], ICONS['GOOGLE'] . 'Google 로그인', ["class" => "btn btn-danger"]) ?>
+ 회원가입
+
+ = form_close(); ?>
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/layouts/admin.php b/app/Views/layouts/admin.php
new file mode 100644
index 0000000..b956f83
--- /dev/null
+++ b/app/Views/layouts/admin.php
@@ -0,0 +1,42 @@
+
+
+
+
+ = LAYOUTS[$viewDatas['layout']]['title'] ?>
+
+ = $meta ?>
+
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/top'); ?>
+
+
+
= $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/left_menu'); ?>
+
+
+
= $this->renderSection('content') ?>
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/bottom'); ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/bottom.php b/app/Views/layouts/admin/bottom.php
new file mode 100644
index 0000000..a11ab5d
--- /dev/null
+++ b/app/Views/layouts/admin/bottom.php
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/direct_form.php b/app/Views/layouts/admin/direct_form.php
new file mode 100644
index 0000000..c3102ce
--- /dev/null
+++ b/app/Views/layouts/admin/direct_form.php
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/top'); ?>
+
+
+
= $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/left_menu'); ?>
+
+
+
= $this->renderSection('content') ?>
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/bottom'); ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/iframe_form.php b/app/Views/layouts/admin/iframe_form.php
new file mode 100644
index 0000000..01324f1
--- /dev/null
+++ b/app/Views/layouts/admin/iframe_form.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/left_menu.php b/app/Views/layouts/admin/left_menu.php
new file mode 100644
index 0000000..c07afb7
--- /dev/null
+++ b/app/Views/layouts/admin/left_menu.php
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/left_menu/base.php b/app/Views/layouts/admin/left_menu/base.php
new file mode 100644
index 0000000..d13a1e9
--- /dev/null
+++ b/app/Views/layouts/admin/left_menu/base.php
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/left_menu/cloudflare.php b/app/Views/layouts/admin/left_menu/cloudflare.php
new file mode 100644
index 0000000..8850010
--- /dev/null
+++ b/app/Views/layouts/admin/left_menu/cloudflare.php
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/left_menu/site.php b/app/Views/layouts/admin/left_menu/site.php
new file mode 100644
index 0000000..1bf5b18
--- /dev/null
+++ b/app/Views/layouts/admin/left_menu/site.php
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/modal_form.php b/app/Views/layouts/admin/modal_form.php
new file mode 100644
index 0000000..01324f1
--- /dev/null
+++ b/app/Views/layouts/admin/modal_form.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/admin/top.php b/app/Views/layouts/admin/top.php
new file mode 100644
index 0000000..bc7da30
--- /dev/null
+++ b/app/Views/layouts/admin/top.php
@@ -0,0 +1,41 @@
+
+
+
관리페이지
+
+
+
+ get(SESSION_NAMES['ISLOGIN'])): ?>
+
+
+ = ICONS['LOGIN'] . $viewDatas['session']->get(SESSION_NAMES['AUTH'])['name'] ?>
+
+
+
+ = ICONS['LOGIN'] ?>Login
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/empty.php b/app/Views/layouts/empty.php
new file mode 100644
index 0000000..4dfa306
--- /dev/null
+++ b/app/Views/layouts/empty.php
@@ -0,0 +1,29 @@
+
+
+
+
+ = LAYOUTS[$viewDatas['layout']]['title'] ?>
+
+ = $meta ?>
+
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front.php b/app/Views/layouts/front.php
new file mode 100644
index 0000000..b956f83
--- /dev/null
+++ b/app/Views/layouts/front.php
@@ -0,0 +1,42 @@
+
+
+
+
+ = LAYOUTS[$viewDatas['layout']]['title'] ?>
+
+ = $meta ?>
+
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/top'); ?>
+
+
+
= $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/left_menu'); ?>
+
+
+
= $this->renderSection('content') ?>
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/bottom'); ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front/bottom.php b/app/Views/layouts/front/bottom.php
new file mode 100644
index 0000000..a11ab5d
--- /dev/null
+++ b/app/Views/layouts/front/bottom.php
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front/direct_form.php b/app/Views/layouts/front/direct_form.php
new file mode 100644
index 0000000..c3102ce
--- /dev/null
+++ b/app/Views/layouts/front/direct_form.php
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/top'); ?>
+
+
+
= $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/left_menu'); ?>
+
+
+
= $this->renderSection('content') ?>
+
+
+
+
+ = $this->include(LAYOUTS[$viewDatas['layout']]['path'] . '/bottom'); ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front/iframe_form.php b/app/Views/layouts/front/iframe_form.php
new file mode 100644
index 0000000..01324f1
--- /dev/null
+++ b/app/Views/layouts/front/iframe_form.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front/modal_form.php b/app/Views/layouts/front/modal_form.php
new file mode 100644
index 0000000..01324f1
--- /dev/null
+++ b/app/Views/layouts/front/modal_form.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ = $viewDatas['title'] ?>
+
+ = $stylesheet ?>
+
+
+ = $javascript ?>
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/layouts/front/to.php b/app/Views/layouts/front/to.php
new file mode 100644
index 0000000..410b5fb
--- /dev/null
+++ b/app/Views/layouts/front/to.php
@@ -0,0 +1,41 @@
+
+
+
관리페이지
+
+
+
+ get(SESSION_NAMES['ISLOGIN'])): ?>
+
+
+ = ICONS['LOGIN'] . $viewDatas['session']->get(SESSION_NAMES['AUTH'])['name'] ?>
+
+
+
+ = ICONS['LOGIN'] ?>Login
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/Pagers/bootstrap_full.php b/app/Views/templates/Pagers/bootstrap_full.php
new file mode 100644
index 0000000..49af876
--- /dev/null
+++ b/app/Views/templates/Pagers/bootstrap_full.php
@@ -0,0 +1,35 @@
+setSurroundCount(2);
+?>
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/Pagers/bootstrap_simple.php b/app/Views/templates/Pagers/bootstrap_simple.php
new file mode 100644
index 0000000..e728703
--- /dev/null
+++ b/app/Views/templates/Pagers/bootstrap_simple.php
@@ -0,0 +1,17 @@
+setSurroundCount(0);
+?>
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/admin/footer.php b/app/Views/templates/admin/footer.php
new file mode 100644
index 0000000..7cdbd2f
--- /dev/null
+++ b/app/Views/templates/admin/footer.php
@@ -0,0 +1 @@
+= $viewDatas['session']->getFlashdata(SESSION_NAMES['RETURN_MSG']) ? $viewDatas['helper']->alert($viewDatas['session']->getFlashdata(SESSION_NAMES['RETURN_MSG'])) : "" ?>
\ No newline at end of file
diff --git a/app/Views/templates/admin/header.php b/app/Views/templates/admin/header.php
new file mode 100644
index 0000000..e35fef7
--- /dev/null
+++ b/app/Views/templates/admin/header.php
@@ -0,0 +1,87 @@
+
+
+
+ = ICONS['DESKTOP'] ?> = $viewDatas['title'] ?>
+
+
+
+ Cloudflare DNS
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/admin/iframe_form.php b/app/Views/templates/admin/iframe_form.php
new file mode 100644
index 0000000..dd9bf7e
--- /dev/null
+++ b/app/Views/templates/admin/iframe_form.php
@@ -0,0 +1,29 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/admin/iframe_form_close.php b/app/Views/templates/admin/iframe_form_close.php
new file mode 100644
index 0000000..48dc854
--- /dev/null
+++ b/app/Views/templates/admin/iframe_form_close.php
@@ -0,0 +1,8 @@
+= $this->extend("layouts/empty") ?>
+= $this->section('content') ?>
+
+
= $viewDatas['message'] ?>
+
작업 완료 후 아래 버튼을 클릭하면 모달이 닫힙니다.
+
작업 완료
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/templates/admin/modal_form.php b/app/Views/templates/admin/modal_form.php
new file mode 100644
index 0000000..b7753f0
--- /dev/null
+++ b/app/Views/templates/admin/modal_form.php
@@ -0,0 +1,61 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/admin/modal_form_close.php b/app/Views/templates/admin/modal_form_close.php
new file mode 100644
index 0000000..a48a201
--- /dev/null
+++ b/app/Views/templates/admin/modal_form_close.php
@@ -0,0 +1,8 @@
+= $this->extend("layouts/empty") ?>
+= $this->section('content') ?>
+
+
= $viewDatas['message'] ?>
+
작업 완료 후 아래 버튼을 클릭하면 모달이 닫힙니다.
+
작업 완료
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/templates/download.php b/app/Views/templates/download.php
new file mode 100644
index 0000000..3c52984
--- /dev/null
+++ b/app/Views/templates/download.php
@@ -0,0 +1,20 @@
+
+
+
+
+ = lang("{$viewDatas['class_path']}.label.{$field}") ?>
+
+
+
+
+
+
+
+
+ = $entity->$field ?>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/front/footer.php b/app/Views/templates/front/footer.php
new file mode 100644
index 0000000..7cdbd2f
--- /dev/null
+++ b/app/Views/templates/front/footer.php
@@ -0,0 +1 @@
+= $viewDatas['session']->getFlashdata(SESSION_NAMES['RETURN_MSG']) ? $viewDatas['helper']->alert($viewDatas['session']->getFlashdata(SESSION_NAMES['RETURN_MSG'])) : "" ?>
\ No newline at end of file
diff --git a/app/Views/templates/front/header.php b/app/Views/templates/front/header.php
new file mode 100644
index 0000000..e35fef7
--- /dev/null
+++ b/app/Views/templates/front/header.php
@@ -0,0 +1,87 @@
+
+
+
+ = ICONS['DESKTOP'] ?> = $viewDatas['title'] ?>
+
+
+
+ Cloudflare DNS
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/front/iframe_form.php b/app/Views/templates/front/iframe_form.php
new file mode 100644
index 0000000..dd9bf7e
--- /dev/null
+++ b/app/Views/templates/front/iframe_form.php
@@ -0,0 +1,29 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/front/iframe_form_close.php b/app/Views/templates/front/iframe_form_close.php
new file mode 100644
index 0000000..48dc854
--- /dev/null
+++ b/app/Views/templates/front/iframe_form_close.php
@@ -0,0 +1,8 @@
+= $this->extend("layouts/empty") ?>
+= $this->section('content') ?>
+
+
= $viewDatas['message'] ?>
+
작업 완료 후 아래 버튼을 클릭하면 모달이 닫힙니다.
+
작업 완료
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/templates/front/index_top.php b/app/Views/templates/front/index_top.php
new file mode 100644
index 0000000..07de259
--- /dev/null
+++ b/app/Views/templates/front/index_top.php
@@ -0,0 +1,24 @@
+= form_open(current_url(), array("method" => "get")) ?>
+
+
+
+ 조건검색:
+
+
+ = $fieldform_function($field, $viewDatas[$field] ?: old($field), $viewDatas) ?>
+
+
+
+ 검색어:= form_input('word', $viewDatas['word']) ?>
+ 검색일:= form_input('start', $viewDatas['start'], ["class" => "calender"]) ?>= form_input('end', $viewDatas['end'], ["class" => "calender"]) ?>
+ = form_submit('', '검색하기') ?>
+ = anchor(current_url() . '/download/excel', ICONS['EXCEL'], ["target" => "_self", "class" => "excel"]) ?>
+
+
+ 페이지 : = $viewDatas['page'] ?>/= $viewDatas['total_page'] ?>
+ = form_dropdown('per_page', $viewDatas['page_options'], $viewDatas['per_page'], array('onChange' => 'this.form.submit()')) ?>
+ / 총:= $viewDatas['total_count'] ?>
+
+
+
+= form_close() ?>
\ No newline at end of file
diff --git a/app/Views/templates/front/modal_form.php b/app/Views/templates/front/modal_form.php
new file mode 100644
index 0000000..b7753f0
--- /dev/null
+++ b/app/Views/templates/front/modal_form.php
@@ -0,0 +1,61 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/Views/templates/front/modal_form_close.php b/app/Views/templates/front/modal_form_close.php
new file mode 100644
index 0000000..a48a201
--- /dev/null
+++ b/app/Views/templates/front/modal_form_close.php
@@ -0,0 +1,8 @@
+= $this->extend("layouts/empty") ?>
+= $this->section('content') ?>
+
+
= $viewDatas['message'] ?>
+
작업 완료 후 아래 버튼을 클릭하면 모달이 닫힙니다.
+
작업 완료
+
+= $this->endSection() ?>
\ No newline at end of file
diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php
new file mode 100644
index 0000000..c18eca3
--- /dev/null
+++ b/app/Views/welcome_message.php
@@ -0,0 +1,331 @@
+
+
+
+
+ Welcome to CodeIgniter 4!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ About this page
+
+ The page you are looking at is being generated dynamically by CodeIgniter.
+
+ If you would like to edit this page you will find it located at:
+
+ app/Views/welcome_message.php
+
+ The corresponding controller for this page can be found at:
+
+ app/Controllers/Home.php
+
+
+
+
+
+
+
+ Go further
+
+
+
+ Learn
+
+
+ The User Guide contains an introduction, tutorial, a number of "how to"
+ guides, and then reference documentation for the components that make up
+ the framework. Check the User Guide !
+
+
+
+ Discuss
+
+
+ CodeIgniter is a community-developed open source project, with several
+ venues for the community members to gather and exchange ideas. View all
+ the threads on CodeIgniter's forum , or chat on Slack !
+
+
+
+ Contribute
+
+
+ CodeIgniter is a community driven project and accepts contributions
+ of code and documentation from the community. Why not
+
+ join us ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..69df4e1
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/builds b/builds
new file mode 100644
index 0000000..cc2ca08
--- /dev/null
+++ b/builds
@@ -0,0 +1,125 @@
+#!/usr/bin/env php
+ 'vcs',
+ 'url' => GITHUB_URL,
+ ];
+ }
+
+ $array['require']['codeigniter4/codeigniter4'] = 'dev-develop';
+ unset($array['require']['codeigniter4/framework']);
+ } else {
+ unset($array['minimum-stability']);
+
+ if (isset($array['repositories'])) {
+ foreach ($array['repositories'] as $i => $repository) {
+ if ($repository['url'] === GITHUB_URL) {
+ unset($array['repositories'][$i]);
+ break;
+ }
+ }
+
+ if (empty($array['repositories'])) {
+ unset($array['repositories']);
+ }
+ }
+
+ $array['require']['codeigniter4/framework'] = LATEST_RELEASE;
+ unset($array['require']['codeigniter4/codeigniter4']);
+ }
+
+ file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
+
+ $modified[] = $file;
+ } else {
+ echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL;
+ }
+ } else {
+ echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL;
+ }
+}
+
+$files = [
+ __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php',
+ __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist',
+ __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml',
+];
+
+foreach ($files as $file) {
+ if (is_file($file)) {
+ $contents = file_get_contents($file);
+
+ if ($dev) {
+ $contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents);
+ } else {
+ $contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents);
+ }
+
+ file_put_contents($file, $contents);
+
+ $modified[] = $file;
+ }
+}
+
+if ($modified === []) {
+ echo 'No files modified.' . PHP_EOL;
+} else {
+ echo 'The following files were modified:' . PHP_EOL;
+
+ foreach ($modified as $file) {
+ echo " * {$file}" . PHP_EOL;
+ }
+
+ echo 'Run `composer update` to sync changes with your vendor folder.' . PHP_EOL;
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..2dd91b8
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,51 @@
+{
+ "name": "codeigniter4/appstarter",
+ "description": "CodeIgniter4 starter app",
+ "license": "MIT",
+ "type": "project",
+ "homepage": "https://codeigniter.com",
+ "support": {
+ "forum": "https://forum.codeigniter.com/",
+ "source": "https://github.com/codeigniter4/CodeIgniter4",
+ "slack": "https://codeigniterchat.slack.com"
+ },
+ "require": {
+ "php": "^8.3",
+ "cloudflare/sdk": "^1.3",
+ "codeigniter4/framework": "^4.5",
+ "google/apiclient": "^2.15.0",
+ "guzzlehttp/guzzle": "^7.9",
+ "phpoffice/phpspreadsheet": "^1.27",
+ "symfony/css-selector": "^7.1",
+ "symfony/dom-crawler": "^7.1",
+ "tinymce/tinymce": "^7.3"
+ },
+ "require-dev": {
+ "fakerphp/faker": "^1.9",
+ "mikey179/vfsstream": "^1.6",
+ "phpunit/phpunit": "^10.5.16"
+ },
+ "autoload": {
+ "psr-4": {
+ "App\\": "app/",
+ "Config\\": "app/Config/"
+ },
+ "exclude-from-classmap": [
+ "**/Database/Migrations/**"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\Support\\": "tests/_support"
+ }
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true,
+ "process-timeout": 600
+ },
+ "scripts": {
+ "test": "phpunit"
+ }
+}
\ No newline at end of file
diff --git a/env b/env
new file mode 100644
index 0000000..f359ec2
--- /dev/null
+++ b/env
@@ -0,0 +1,69 @@
+#--------------------------------------------------------------------
+# Example Environment Configuration file
+#
+# This file can be used as a starting point for your own
+# custom .env files, and contains most of the possible settings
+# available in a default install.
+#
+# By default, all of the settings are commented out. If you want
+# to override the setting, you must un-comment it by removing the '#'
+# at the beginning of the line.
+#--------------------------------------------------------------------
+
+#--------------------------------------------------------------------
+# ENVIRONMENT
+#--------------------------------------------------------------------
+
+# CI_ENVIRONMENT = production
+
+#--------------------------------------------------------------------
+# APP
+#--------------------------------------------------------------------
+
+# app.baseURL = ''
+# If you have trouble with `.`, you could also use `_`.
+# app_baseURL = ''
+# app.forceGlobalSecureRequests = false
+# app.CSPEnabled = false
+
+#--------------------------------------------------------------------
+# DATABASE
+#--------------------------------------------------------------------
+
+# database.default.hostname = localhost
+# database.default.database = ci4
+# database.default.username = root
+# database.default.password = root
+# database.default.DBDriver = MySQLi
+# database.default.DBPrefix =
+# database.default.port = 3306
+
+# If you use MySQLi as tests, first update the values of Config\Database::$tests.
+# database.tests.hostname = localhost
+# database.tests.database = ci4_test
+# database.tests.username = root
+# database.tests.password = root
+# database.tests.DBDriver = MySQLi
+# database.tests.DBPrefix =
+# database.tests.charset = utf8mb4
+# database.tests.DBCollat = utf8mb4_general_ci
+# database.tests.port = 3306
+
+#--------------------------------------------------------------------
+# ENCRYPTION
+#--------------------------------------------------------------------
+
+# encryption.key =
+
+#--------------------------------------------------------------------
+# SESSION
+#--------------------------------------------------------------------
+
+# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
+# session.savePath = null
+
+#--------------------------------------------------------------------
+# LOGGER
+#--------------------------------------------------------------------
+
+# logger.threshold = 4
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..b408a99
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ./tests
+
+
+
+
+
+
+
+
+
+ ./app
+
+
+ ./app/Views
+ ./app/Config/Routes.php
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/preload.php b/preload.php
new file mode 100644
index 0000000..75d86f5
--- /dev/null
+++ b/preload.php
@@ -0,0 +1,104 @@
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+/*
+ *---------------------------------------------------------------
+ * Sample file for Preloading
+ *---------------------------------------------------------------
+ * See https://www.php.net/manual/en/opcache.preloading.php
+ *
+ * How to Use:
+ * 0. Copy this file to your project root folder.
+ * 1. Set the $paths property of the preload class below.
+ * 2. Set opcache.preload in php.ini.
+ * php.ini:
+ * opcache.preload=/path/to/preload.php
+ */
+
+// Load the paths config file
+require __DIR__ . '/app/Config/Paths.php';
+
+// Path to the front controller
+define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
+
+class preload
+{
+ /**
+ * @var array Paths to preload.
+ */
+ private array $paths = [
+ [
+ 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', // Change this path if using manual installation
+ 'exclude' => [
+ '/system/bootstrap.php',
+ // Not needed if you don't use them.
+ '/system/Database/OCI8/',
+ '/system/Database/Postgre/',
+ '/system/Database/SQLite3/',
+ '/system/Database/SQLSRV/',
+ // Not needed.
+ '/system/Database/Seeder.php',
+ '/system/Test/',
+ '/system/Language/',
+ '/system/CLI/',
+ '/system/Commands/',
+ '/system/Publisher/',
+ '/system/ComposerScripts.php',
+ '/Views/',
+ // Errors occur.
+ '/system/Config/Routes.php',
+ '/system/ThirdParty/',
+ ],
+ ],
+ ];
+
+ public function __construct()
+ {
+ $this->loadAutoloader();
+ }
+
+ private function loadAutoloader(): void
+ {
+ $paths = new Config\Paths();
+ require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'Boot.php';
+
+ CodeIgniter\Boot::preload($paths);
+ }
+
+ /**
+ * Load PHP files.
+ */
+ public function load(): void
+ {
+ foreach ($this->paths as $path) {
+ $directory = new RecursiveDirectoryIterator($path['include']);
+ $fullTree = new RecursiveIteratorIterator($directory);
+ $phpFiles = new RegexIterator(
+ $fullTree,
+ '/.+((? $file) {
+ foreach ($path['exclude'] as $exclude) {
+ if (str_contains($file[0], $exclude)) {
+ continue 2;
+ }
+ }
+
+ require_once $file[0];
+ echo 'Loaded: ' . $file[0] . "\n";
+ }
+ }
+ }
+}
+
+(new preload())->load();
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..abac3cb
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,49 @@
+# Disable directory browsing
+Options -Indexes
+
+# ----------------------------------------------------------------------
+# Rewrite engine
+# ----------------------------------------------------------------------
+
+# Turning on the rewrite engine is necessary for the following rules and features.
+# FollowSymLinks must be enabled for this to work.
+
+ Options +FollowSymlinks
+ RewriteEngine On
+
+ # If you installed CodeIgniter in a subfolder, you will need to
+ # change the following line to match the subfolder you need.
+ # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase
+ # RewriteBase /
+
+ # Redirect Trailing Slashes...
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_URI} (.+)/$
+ RewriteRule ^ %1 [L,R=301]
+
+ # Rewrite "www.example.com -> example.com"
+ RewriteCond %{HTTPS} !=on
+ RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
+ RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
+
+ # Checks to see if the user is attempting to access a valid file,
+ # such as an image or css document, if this isn't true it sends the
+ # request to the front controller, index.php
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
+
+ # Ensure Authorization header is passed along
+ RewriteCond %{HTTP:Authorization} .
+ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+
+
+
+ # If we don't have mod_rewrite installed, all 404's
+ # can be sent to index.php, and everything works as normal.
+ ErrorDocument 404 index.php
+
+
+# Disable server signature start
+ServerSignature Off
+# Disable server signature end
diff --git a/public/css/admin.css b/public/css/admin.css
new file mode 100644
index 0000000..b1cbc75
--- /dev/null
+++ b/public/css/admin.css
@@ -0,0 +1,83 @@
+/* ------------------------------------------------------------
+ * Name : admin.css
+ * Desc : Admin StyleSheet
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+* {
+ margin: 0px;
+ padding: 0px;
+ border: 0px;
+ /* font-size: 15px; */
+}
+
+html,
+body {
+ height: 100%;
+}
+
+div.top {
+ height: 51px;
+ margin-bottom: 10px;
+ border-top: 1px solid gray;
+ border-bottom: 1px solid gray;
+ background-color: #e8e9ea;
+}
+
+div.middle {
+ /* border: 1px solid blue; */
+}
+
+/* div.middle div.left {
+ border: 1px solid red;
+} */
+
+div.middle div.right {
+ width: 100%;
+ /* overflow: auto; */
+ /* border: 1px solid blue; */
+}
+
+div.middle div.right div.header {
+ /*content 헤더라인*/
+ height: 55px;
+ padding-top: 15px;
+ background-color: #e7e7e7;
+ border-top: 1px solid gray;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ border-radius: 15px 15px 0px 0px;
+}
+
+div.middle div.right div.header li.nav-item {
+ height: 40px;
+}
+
+div.middle div.right div.content {
+ /*content 부분*/
+ padding-top: 15px;
+ padding-left: 23px;
+ padding-right: 15px;
+ padding-bottom: 15px;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ background-color: white;
+}
+
+div.middle div.right div.footer {
+ /*content 하단라인*/
+ height: 20px;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ border-bottom: 1px solid gray;
+ border-radius: 0px 0px 15px 15px;
+}
+
+div.bottom {
+ height: 51px;
+ margin-top: 10px;
+ border-top: 1px solid gray;
+ border-bottom: 1px solid gray;
+ background-color: #efefef;
+ background-color: #e8e9ea;
+}
\ No newline at end of file
diff --git a/public/css/admin/form.css b/public/css/admin/form.css
new file mode 100644
index 0000000..7440ad5
--- /dev/null
+++ b/public/css/admin/form.css
@@ -0,0 +1,28 @@
+/* create,modify,view 페이지용 */
+div.action_form {
+ /* 블록 요소로 변경 */
+ margin-left: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ margin-right: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ /* border: 1px solid blue; */
+ /* table-layout: fixed; 고정 레이아웃 */
+ border-collapse: collapse;
+ /* 테두리 결합 */
+}
+
+div.action_form table {}
+
+div.action_form table th {
+ background-color: #f0f0f0;
+}
+
+div.action_form table td {
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+}
+
+/* create,modify,view 페이지용 */
\ No newline at end of file
diff --git a/public/css/admin/index.css b/public/css/admin/index.css
new file mode 100644
index 0000000..c759c4b
--- /dev/null
+++ b/public/css/admin/index.css
@@ -0,0 +1,157 @@
+/* create,modify,view 페이지용 */
+table.action_form {
+ /* 블록 요소로 변경 */
+ margin-left: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ margin-right: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ /* border: 1px solid blue; */
+ /* table-layout: fixed; 고정 레이아웃 */
+ border-collapse: collapse;
+ /* 테두리 결합 */
+}
+
+table.action_form th {
+ text-align: center;
+ background-color: #f5f5f5;
+}
+
+table.action_form td {
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+}
+
+/* create,modify,view 페이지용 */
+
+/*조건검색*/
+nav.index_top nav.condition {
+ border-color: 1px solid red;
+}
+
+/*검색*/
+nav.index_top nav.search {
+ position: relative;
+ height: 30px;
+ border-color: 1px solid red;
+}
+
+nav.index_top nav.search input[type="text"] {
+ width: 200px;
+ height: 30px;
+}
+
+/*검색submit*/
+nav.index_top nav.search input[type="submit"] {
+ font-weight: bold;
+ width: 80px;
+ height: 30px;
+ color: white;
+ background-color: #555555;
+}
+
+/*검색submit*/
+nav.index_top nav.search a.excel {
+ position: absolute;
+ top: -9px;
+ right: -45px;
+ /* border-color: 1px solid red; */
+}
+
+/*페이지정보*/
+nav.index_top nav.pageinfo {
+ font-weight: bold;
+ /* border-color: 1px solid red; */
+}
+
+/* Table 부분 */
+table.index_table {
+ width: 100%;
+ /* table-layout: fixed; */
+ border-collapse: collapse;
+}
+
+table.index_table thead th {
+ white-space: nowrap;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ font-weight: bold;
+ border-top: 2px solid black;
+ border-bottom: 1px solid silver;
+ background-color: #f5f5f5;
+ text-align: center;
+ /* border:1px solid silver; */
+}
+
+table.index_table thead th.index_head_short_column {
+ width: 80px;
+}
+
+table.index_table thead th:active {
+ cursor: grabbing;
+}
+
+div.index_batchjob {
+ padding-top: 15px;
+ text-align: center;
+ /* border: 1px solid red; */
+}
+
+div.index_pagination {
+ margin-top: 20px;
+ /* border: 1px solid red; */
+}
+
+div.index_pagination ul.pagination {
+ /* border: 1px solid green; */
+ width: fit-content;
+ /* UL의 너비를 내용에 맞춤 */
+ margin: 0 auto;
+ /* 좌우 마진을 자동으로 설정하여 중앙 배치 */
+ padding: 0;
+ list-style: none;
+ /* 기본 점 스타일 제거 (옵션) */
+}
+
+div.index_pagination ul.pagination li {
+ margin-left: 5px;
+}
+
+div.index_pagination ul.pagination li a {
+ padding: 5px 10px 5px 10px;
+ font-size: 1.5rem;
+ color: white;
+ background-color: #808080;
+}
+
+div.index_pagination ul.pagination li a:hover {
+ border: 1px solid black;
+}
+
+div.index_pagination ul.pagination li.active a {
+ color: black;
+ border: 1px solid #808080;
+}
+
+div.index_bottom {
+ padding-top: 15px;
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+ /* border: 1px solid red; */
+}
+
+div.index_bottom div.index_action_form {
+ margin-top: 20px;
+ /* border: 1px solid red; */
+}
+
+div.index_bottom div.index_action_form iframe {
+ width: 100%;
+ border: none;
+ /* border: 1px solid blue; */
+}
\ No newline at end of file
diff --git a/public/css/admin/left_menu.css b/public/css/admin/left_menu.css
new file mode 100644
index 0000000..cc89e40
--- /dev/null
+++ b/public/css/admin/left_menu.css
@@ -0,0 +1,56 @@
+div#left_menu {
+ position: fixed;
+ margin-top: 60px;
+ z-index: 100;
+ border: 1px solid silver;
+}
+
+div#left_menu div#menu_button {
+ position: absolute;
+ top: -1px;
+ right: -21px;
+ width: 20px;
+ height: 160px;
+ cursor: pointer;
+ writing-mode: vertical-rl;
+ /* 세로로 글자를 출력 */
+ text-orientation: upright;
+ /* 글자가 직립되도록 설정 */
+ border-top: 1px solid silver;
+ border-right: 1px solid silver;
+ border-bottom: 1px solid silver;
+ border-radius: 0px 5px 5px 0px;
+ background-color: #e8e9ea;
+}
+
+div#left_menu div.accordion {
+ display: none;
+ width: 20px;
+}
+
+div#left_menu div.accordion div.main {
+ height: 50px;
+ padding-top: 15px;
+ padding-left: 10px;
+ background-color: white;
+ border-bottom: 1px solid silver;
+}
+
+div#left_menu div.accordion div.accordion-item {
+ height: 50px;
+ padding-top: 15px;
+ background-color: #eeeeee;
+ border-bottom: 1px solid silver;
+}
+
+div#left_menu div.accordion div.accordion-item:hover {
+ background-color: silver;
+}
+
+div#left_menu div.accordion div.accordion-item a {
+ padding-left: 20px;
+}
+
+div#left_menu div.accordion div.accordion-collapse a {
+ padding-left: 30px;
+}
\ No newline at end of file
diff --git a/public/css/admin/member_link.css b/public/css/admin/member_link.css
new file mode 100644
index 0000000..38ed839
--- /dev/null
+++ b/public/css/admin/member_link.css
@@ -0,0 +1,17 @@
+nav.top_menu ul.member-link{
+ /* border:1px solid red; */
+ color:#3a37f3;
+ padding-right:20px;
+}
+
+nav.top_menu ul.member-link a{
+ color:#3a37f3;
+}
+
+nav.top_menu ul.member-link ul.dropdown-menu li:hover{
+ background-color: #eaeaea;
+}
+
+nav.top_menu ul.member-link ul.dropdown-menu li a{
+ padding-left:10px;
+}
\ No newline at end of file
diff --git a/public/css/common/resizeTable.css b/public/css/common/resizeTable.css
new file mode 100644
index 0000000..8c4e075
--- /dev/null
+++ b/public/css/common/resizeTable.css
@@ -0,0 +1,67 @@
+.rtc-wrapper table.rtc-table {
+ border-collapse: collapse;
+ white-space: nowrap;
+ margin: 0;
+}
+
+.rtc-wrapper table.rtc-table thead,
+.rtc-wrapper table.rtc-table tbody,
+.rtc-wrapper table.rtc-table tfoot {
+ margin: 0;
+}
+
+.rtc-wrapper table.rtc-table thead tr,
+.rtc-wrapper table.rtc-table tbody tr,
+.rtc-wrapper table.rtc-table tfoot tr {
+ margin: 0;
+}
+
+.rtc-wrapper table.rtc-table thead tr th,
+.rtc-wrapper table.rtc-table thead tr td,
+.rtc-wrapper table.rtc-table tbody tr th,
+.rtc-wrapper table.rtc-table tbody tr td,
+.rtc-wrapper table.rtc-table tfoot tr th,
+.rtc-wrapper table.rtc-table tfoot tr td {
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.rtc-wrapper table.rtc-table.rtc-table-resizing {
+ cursor: col-resize;
+}
+
+.rtc-wrapper table.rtc-table.rtc-table-resizing thead,
+.rtc-wrapper table.rtc-table.rtc-table-resizing thead > th,
+.rtc-wrapper table.rtc-table.rtc-table-resizing thead > th > a {
+ cursor: col-resize;
+}
+
+.rtc-wrapper table.rtc-table thead tr.invisible,
+.rtc-wrapper table.rtc-table thead tr.invisible th {
+ border: none;
+ margin: 0;
+ padding: 0;
+ height: 0 !important;
+}
+
+.rtc-wrapper .rtc-handle-container {
+ position: relative;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+.rtc-wrapper .rtc-handle-container .rtc-handle {
+ position: absolute;
+ width: 6.5px;
+ margin-left: -3.575px;
+ z-index: 2;
+ cursor: col-resize;
+}
+
+.rtc-wrapper .rtc-handle-container .rtc-handle:last-of-type {
+ width: 4.5px;
+ margin-left: -4.95px;
+}
\ No newline at end of file
diff --git a/public/css/common/style.css b/public/css/common/style.css
new file mode 100644
index 0000000..54b28ae
--- /dev/null
+++ b/public/css/common/style.css
@@ -0,0 +1,55 @@
+/* ------------------------------------------------------------
+ * Name : admin.css
+ * Desc : Admin StyleSheet
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+@charset "utf-8";
+
+/* user class */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+strong,
+th,
+.bold {
+ font-weight: 500;
+}
+
+input[type=text],
+input[type=password] {
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+ white-space: nowrap;
+}
+
+select,
+textarea,
+button {
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+ white-space: nowrap;
+}
+
+a:link {
+ text-decoration: none;
+}
+
+a:visited {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+a:active {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/public/css/empty.css b/public/css/empty.css
new file mode 100644
index 0000000..c110333
--- /dev/null
+++ b/public/css/empty.css
@@ -0,0 +1,12 @@
+/* ------------------------------------------------------------
+ * Name : admin.css
+ * Desc : Admin StyleSheet
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+* {
+ margin: 0px;
+ padding: 0px;
+ border: 0px;
+ /* font-size: 15px; */
+}
\ No newline at end of file
diff --git a/public/css/front.css b/public/css/front.css
new file mode 100644
index 0000000..403d3ad
--- /dev/null
+++ b/public/css/front.css
@@ -0,0 +1,74 @@
+/* ------------------------------------------------------------
+ * Name : admin.css
+ * Desc : Admin StyleSheet
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+* {
+ margin: 0px;
+ padding: 0px;
+ border: 0px;
+ /* font-size: 15px; */
+}
+
+html,
+body {
+ height: 100%;
+}
+
+div.top {
+ height: 51px;
+ margin-bottom: 10px;
+ border-top: 1px solid gray;
+ border-bottom: 1px solid gray;
+ background-color: #e8e9ea;
+}
+
+div.middle {
+ /* border: 1px solid red; */
+}
+
+div.middle div.header {
+ /*content 헤더라인*/
+ height: 55px;
+ padding-top: 15px;
+ background-color: #e7e7e7;
+ border-top: 1px solid gray;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ border-radius: 15px 15px 0px 0px;
+}
+
+div.middle div.header li.nav-item {
+ height: 40px;
+}
+
+div.middle div.content {
+ /*content 부분*/
+ padding-top: 15px;
+ padding-left: 23px;
+ padding-right: 15px;
+ padding-bottom: 15px;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ background-color: white;
+ border: 1px solid red;
+}
+
+div.middle div.footer {
+ /*content 하단라인*/
+ height: 20px;
+ border-left: 1px solid gray;
+ border-right: 1px solid gray;
+ border-bottom: 1px solid gray;
+ border-radius: 0px 0px 15px 15px;
+}
+
+div.bottom {
+ height: 51px;
+ margin-top: 10px;
+ border-top: 1px solid gray;
+ border-bottom: 1px solid gray;
+ background-color: #efefef;
+ background-color: #e8e9ea;
+}
\ No newline at end of file
diff --git a/public/css/front/copyright.css b/public/css/front/copyright.css
new file mode 100644
index 0000000..45a4a77
--- /dev/null
+++ b/public/css/front/copyright.css
@@ -0,0 +1,29 @@
+div#copyright{
+ width:100%;
+ height:300px;
+ padding-top:30px;
+ padding-bottom:30px;
+ background-color:#2d2e2e;
+ color:white;
+}
+
+div#copyright div#content_bottom{
+ color:white;
+ text-align:left;
+ /* border-left:1px solid silver;
+ border-right:1px solid silver; */
+}
+
+div#copyright div#content_bottom .item{
+ padding-top:5px;
+ padding-left:5px;
+ padding-right:5px;
+ border-top:1px solid silver;
+ border-left:1px solid silver;
+ border-right:1px solid silver;
+}
+
+div#copyright div#content_bottom div.company_info{
+ padding:10px;
+ border:1px solid silver;
+}
diff --git a/public/css/front/form.css b/public/css/front/form.css
new file mode 100644
index 0000000..7440ad5
--- /dev/null
+++ b/public/css/front/form.css
@@ -0,0 +1,28 @@
+/* create,modify,view 페이지용 */
+div.action_form {
+ /* 블록 요소로 변경 */
+ margin-left: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ margin-right: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ /* border: 1px solid blue; */
+ /* table-layout: fixed; 고정 레이아웃 */
+ border-collapse: collapse;
+ /* 테두리 결합 */
+}
+
+div.action_form table {}
+
+div.action_form table th {
+ background-color: #f0f0f0;
+}
+
+div.action_form table td {
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+}
+
+/* create,modify,view 페이지용 */
\ No newline at end of file
diff --git a/public/css/front/index.css b/public/css/front/index.css
new file mode 100644
index 0000000..c759c4b
--- /dev/null
+++ b/public/css/front/index.css
@@ -0,0 +1,157 @@
+/* create,modify,view 페이지용 */
+table.action_form {
+ /* 블록 요소로 변경 */
+ margin-left: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ margin-right: auto;
+ /* 자동 여백을 이용한 가로 가운데 정렬 */
+ /* border: 1px solid blue; */
+ /* table-layout: fixed; 고정 레이아웃 */
+ border-collapse: collapse;
+ /* 테두리 결합 */
+}
+
+table.action_form th {
+ text-align: center;
+ background-color: #f5f5f5;
+}
+
+table.action_form td {
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+}
+
+/* create,modify,view 페이지용 */
+
+/*조건검색*/
+nav.index_top nav.condition {
+ border-color: 1px solid red;
+}
+
+/*검색*/
+nav.index_top nav.search {
+ position: relative;
+ height: 30px;
+ border-color: 1px solid red;
+}
+
+nav.index_top nav.search input[type="text"] {
+ width: 200px;
+ height: 30px;
+}
+
+/*검색submit*/
+nav.index_top nav.search input[type="submit"] {
+ font-weight: bold;
+ width: 80px;
+ height: 30px;
+ color: white;
+ background-color: #555555;
+}
+
+/*검색submit*/
+nav.index_top nav.search a.excel {
+ position: absolute;
+ top: -9px;
+ right: -45px;
+ /* border-color: 1px solid red; */
+}
+
+/*페이지정보*/
+nav.index_top nav.pageinfo {
+ font-weight: bold;
+ /* border-color: 1px solid red; */
+}
+
+/* Table 부분 */
+table.index_table {
+ width: 100%;
+ /* table-layout: fixed; */
+ border-collapse: collapse;
+}
+
+table.index_table thead th {
+ white-space: nowrap;
+ padding-top: 15px;
+ padding-bottom: 15px;
+ font-weight: bold;
+ border-top: 2px solid black;
+ border-bottom: 1px solid silver;
+ background-color: #f5f5f5;
+ text-align: center;
+ /* border:1px solid silver; */
+}
+
+table.index_table thead th.index_head_short_column {
+ width: 80px;
+}
+
+table.index_table thead th:active {
+ cursor: grabbing;
+}
+
+div.index_batchjob {
+ padding-top: 15px;
+ text-align: center;
+ /* border: 1px solid red; */
+}
+
+div.index_pagination {
+ margin-top: 20px;
+ /* border: 1px solid red; */
+}
+
+div.index_pagination ul.pagination {
+ /* border: 1px solid green; */
+ width: fit-content;
+ /* UL의 너비를 내용에 맞춤 */
+ margin: 0 auto;
+ /* 좌우 마진을 자동으로 설정하여 중앙 배치 */
+ padding: 0;
+ list-style: none;
+ /* 기본 점 스타일 제거 (옵션) */
+}
+
+div.index_pagination ul.pagination li {
+ margin-left: 5px;
+}
+
+div.index_pagination ul.pagination li a {
+ padding: 5px 10px 5px 10px;
+ font-size: 1.5rem;
+ color: white;
+ background-color: #808080;
+}
+
+div.index_pagination ul.pagination li a:hover {
+ border: 1px solid black;
+}
+
+div.index_pagination ul.pagination li.active a {
+ color: black;
+ border: 1px solid #808080;
+}
+
+div.index_bottom {
+ padding-top: 15px;
+ text-align: center;
+ word-wrap: break-word;
+ /* 긴 단어 강제 줄바꿈 */
+ white-space: normal;
+ /* 자동 줄바꿈 */
+ /* border: 1px solid red; */
+}
+
+div.index_bottom div.index_action_form {
+ margin-top: 20px;
+ /* border: 1px solid red; */
+}
+
+div.index_bottom div.index_action_form iframe {
+ width: 100%;
+ border: none;
+ /* border: 1px solid blue; */
+}
\ No newline at end of file
diff --git a/public/css/front/left_menu.css b/public/css/front/left_menu.css
new file mode 100644
index 0000000..cc89e40
--- /dev/null
+++ b/public/css/front/left_menu.css
@@ -0,0 +1,56 @@
+div#left_menu {
+ position: fixed;
+ margin-top: 60px;
+ z-index: 100;
+ border: 1px solid silver;
+}
+
+div#left_menu div#menu_button {
+ position: absolute;
+ top: -1px;
+ right: -21px;
+ width: 20px;
+ height: 160px;
+ cursor: pointer;
+ writing-mode: vertical-rl;
+ /* 세로로 글자를 출력 */
+ text-orientation: upright;
+ /* 글자가 직립되도록 설정 */
+ border-top: 1px solid silver;
+ border-right: 1px solid silver;
+ border-bottom: 1px solid silver;
+ border-radius: 0px 5px 5px 0px;
+ background-color: #e8e9ea;
+}
+
+div#left_menu div.accordion {
+ display: none;
+ width: 20px;
+}
+
+div#left_menu div.accordion div.main {
+ height: 50px;
+ padding-top: 15px;
+ padding-left: 10px;
+ background-color: white;
+ border-bottom: 1px solid silver;
+}
+
+div#left_menu div.accordion div.accordion-item {
+ height: 50px;
+ padding-top: 15px;
+ background-color: #eeeeee;
+ border-bottom: 1px solid silver;
+}
+
+div#left_menu div.accordion div.accordion-item:hover {
+ background-color: silver;
+}
+
+div#left_menu div.accordion div.accordion-item a {
+ padding-left: 20px;
+}
+
+div#left_menu div.accordion div.accordion-collapse a {
+ padding-left: 30px;
+}
\ No newline at end of file
diff --git a/public/css/front/login.css b/public/css/front/login.css
new file mode 100644
index 0000000..2001247
--- /dev/null
+++ b/public/css/front/login.css
@@ -0,0 +1,17 @@
+.login-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+}
+
+.login-container {
+ width: 400px;
+ height: 400px;
+ overflow-y: auto;
+ /* 추가된 스타일 */
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/public/css/front/member_link.css b/public/css/front/member_link.css
new file mode 100644
index 0000000..38ed839
--- /dev/null
+++ b/public/css/front/member_link.css
@@ -0,0 +1,17 @@
+nav.top_menu ul.member-link{
+ /* border:1px solid red; */
+ color:#3a37f3;
+ padding-right:20px;
+}
+
+nav.top_menu ul.member-link a{
+ color:#3a37f3;
+}
+
+nav.top_menu ul.member-link ul.dropdown-menu li:hover{
+ background-color: #eaeaea;
+}
+
+nav.top_menu ul.member-link ul.dropdown-menu li a{
+ padding-left:10px;
+}
\ No newline at end of file
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..7ecfce2
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/images/auth/google_login_button.png b/public/images/auth/google_login_button.png
new file mode 100644
index 0000000..c19f257
Binary files /dev/null and b/public/images/auth/google_login_button.png differ
diff --git a/public/images/banner/sub_visual1.jpg b/public/images/banner/sub_visual1.jpg
new file mode 100644
index 0000000..df85468
Binary files /dev/null and b/public/images/banner/sub_visual1.jpg differ
diff --git a/public/images/banner/sub_visual2.jpg b/public/images/banner/sub_visual2.jpg
new file mode 100644
index 0000000..8699f2b
Binary files /dev/null and b/public/images/banner/sub_visual2.jpg differ
diff --git a/public/images/banner/sub_visual3.jpg b/public/images/banner/sub_visual3.jpg
new file mode 100644
index 0000000..4ee3220
Binary files /dev/null and b/public/images/banner/sub_visual3.jpg differ
diff --git a/public/images/banner/sub_visual4.jpg b/public/images/banner/sub_visual4.jpg
new file mode 100644
index 0000000..6708503
Binary files /dev/null and b/public/images/banner/sub_visual4.jpg differ
diff --git a/public/images/common/adminbg.png b/public/images/common/adminbg.png
new file mode 100644
index 0000000..68c6a93
Binary files /dev/null and b/public/images/common/adminbg.png differ
diff --git a/public/images/common/btn_login.png b/public/images/common/btn_login.png
new file mode 100644
index 0000000..c0b2782
Binary files /dev/null and b/public/images/common/btn_login.png differ
diff --git a/public/images/common/discord.png b/public/images/common/discord.png
new file mode 100644
index 0000000..4a2233c
Binary files /dev/null and b/public/images/common/discord.png differ
diff --git a/public/images/common/excel.png b/public/images/common/excel.png
new file mode 100644
index 0000000..e67fc00
Binary files /dev/null and b/public/images/common/excel.png differ
diff --git a/public/images/common/kakaotalk.png b/public/images/common/kakaotalk.png
new file mode 100644
index 0000000..cf7a87c
Binary files /dev/null and b/public/images/common/kakaotalk.png differ
diff --git a/public/images/common/menu_on.gif b/public/images/common/menu_on.gif
new file mode 100644
index 0000000..b6216bb
Binary files /dev/null and b/public/images/common/menu_on.gif differ
diff --git a/public/images/common/pdf.png b/public/images/common/pdf.png
new file mode 100644
index 0000000..bb18a19
Binary files /dev/null and b/public/images/common/pdf.png differ
diff --git a/public/images/common/telegram.png b/public/images/common/telegram.png
new file mode 100644
index 0000000..c88f3fa
Binary files /dev/null and b/public/images/common/telegram.png differ
diff --git a/public/images/common/top.png b/public/images/common/top.png
new file mode 100644
index 0000000..84d194d
Binary files /dev/null and b/public/images/common/top.png differ
diff --git a/public/images/common/top_skype.png b/public/images/common/top_skype.png
new file mode 100644
index 0000000..90355c0
Binary files /dev/null and b/public/images/common/top_skype.png differ
diff --git a/public/images/logo/android-icon-144x144.png b/public/images/logo/android-icon-144x144.png
new file mode 100644
index 0000000..3e09df3
Binary files /dev/null and b/public/images/logo/android-icon-144x144.png differ
diff --git a/public/images/logo/android-icon-192x192.png b/public/images/logo/android-icon-192x192.png
new file mode 100644
index 0000000..1aacd55
Binary files /dev/null and b/public/images/logo/android-icon-192x192.png differ
diff --git a/public/images/logo/android-icon-36x36.png b/public/images/logo/android-icon-36x36.png
new file mode 100644
index 0000000..701b1b6
Binary files /dev/null and b/public/images/logo/android-icon-36x36.png differ
diff --git a/public/images/logo/android-icon-48x48.png b/public/images/logo/android-icon-48x48.png
new file mode 100644
index 0000000..9d526d3
Binary files /dev/null and b/public/images/logo/android-icon-48x48.png differ
diff --git a/public/images/logo/android-icon-72x72.png b/public/images/logo/android-icon-72x72.png
new file mode 100644
index 0000000..15896b5
Binary files /dev/null and b/public/images/logo/android-icon-72x72.png differ
diff --git a/public/images/logo/android-icon-96x96.png b/public/images/logo/android-icon-96x96.png
new file mode 100644
index 0000000..627d135
Binary files /dev/null and b/public/images/logo/android-icon-96x96.png differ
diff --git a/public/images/logo/apple-icon-114x114.png b/public/images/logo/apple-icon-114x114.png
new file mode 100644
index 0000000..5e38c56
Binary files /dev/null and b/public/images/logo/apple-icon-114x114.png differ
diff --git a/public/images/logo/apple-icon-120x120.png b/public/images/logo/apple-icon-120x120.png
new file mode 100644
index 0000000..590102a
Binary files /dev/null and b/public/images/logo/apple-icon-120x120.png differ
diff --git a/public/images/logo/apple-icon-144x144.png b/public/images/logo/apple-icon-144x144.png
new file mode 100644
index 0000000..e57cbfe
Binary files /dev/null and b/public/images/logo/apple-icon-144x144.png differ
diff --git a/public/images/logo/apple-icon-152x152.png b/public/images/logo/apple-icon-152x152.png
new file mode 100644
index 0000000..28c2e43
Binary files /dev/null and b/public/images/logo/apple-icon-152x152.png differ
diff --git a/public/images/logo/apple-icon-180x180.png b/public/images/logo/apple-icon-180x180.png
new file mode 100644
index 0000000..f620e91
Binary files /dev/null and b/public/images/logo/apple-icon-180x180.png differ
diff --git a/public/images/logo/apple-icon-57x57.png b/public/images/logo/apple-icon-57x57.png
new file mode 100644
index 0000000..112e893
Binary files /dev/null and b/public/images/logo/apple-icon-57x57.png differ
diff --git a/public/images/logo/apple-icon-60x60.png b/public/images/logo/apple-icon-60x60.png
new file mode 100644
index 0000000..9f74028
Binary files /dev/null and b/public/images/logo/apple-icon-60x60.png differ
diff --git a/public/images/logo/apple-icon-72x72.png b/public/images/logo/apple-icon-72x72.png
new file mode 100644
index 0000000..336828b
Binary files /dev/null and b/public/images/logo/apple-icon-72x72.png differ
diff --git a/public/images/logo/apple-icon-76x76.png b/public/images/logo/apple-icon-76x76.png
new file mode 100644
index 0000000..d6e83f0
Binary files /dev/null and b/public/images/logo/apple-icon-76x76.png differ
diff --git a/public/images/logo/apple-icon-precomposed.png b/public/images/logo/apple-icon-precomposed.png
new file mode 100644
index 0000000..c54f500
Binary files /dev/null and b/public/images/logo/apple-icon-precomposed.png differ
diff --git a/public/images/logo/apple-icon.png b/public/images/logo/apple-icon.png
new file mode 100644
index 0000000..c54f500
Binary files /dev/null and b/public/images/logo/apple-icon.png differ
diff --git a/public/images/logo/favicon-16x16.png b/public/images/logo/favicon-16x16.png
new file mode 100644
index 0000000..d467493
Binary files /dev/null and b/public/images/logo/favicon-16x16.png differ
diff --git a/public/images/logo/favicon-32x32.png b/public/images/logo/favicon-32x32.png
new file mode 100644
index 0000000..67d0fb9
Binary files /dev/null and b/public/images/logo/favicon-32x32.png differ
diff --git a/public/images/logo/favicon-96x96.png b/public/images/logo/favicon-96x96.png
new file mode 100644
index 0000000..9b931ec
Binary files /dev/null and b/public/images/logo/favicon-96x96.png differ
diff --git a/public/images/logo/favicon.ico b/public/images/logo/favicon.ico
new file mode 100644
index 0000000..7de9353
Binary files /dev/null and b/public/images/logo/favicon.ico differ
diff --git a/public/images/logo/logo-small.png b/public/images/logo/logo-small.png
new file mode 100644
index 0000000..0b33a25
Binary files /dev/null and b/public/images/logo/logo-small.png differ
diff --git a/public/images/logo/ms-icon-144x144.png b/public/images/logo/ms-icon-144x144.png
new file mode 100644
index 0000000..e57cbfe
Binary files /dev/null and b/public/images/logo/ms-icon-144x144.png differ
diff --git a/public/images/logo/ms-icon-150x150.png b/public/images/logo/ms-icon-150x150.png
new file mode 100644
index 0000000..0c02de9
Binary files /dev/null and b/public/images/logo/ms-icon-150x150.png differ
diff --git a/public/images/logo/ms-icon-310x310.png b/public/images/logo/ms-icon-310x310.png
new file mode 100644
index 0000000..a1c230b
Binary files /dev/null and b/public/images/logo/ms-icon-310x310.png differ
diff --git a/public/images/logo/ms-icon-70x70.png b/public/images/logo/ms-icon-70x70.png
new file mode 100644
index 0000000..500895a
Binary files /dev/null and b/public/images/logo/ms-icon-70x70.png differ
diff --git a/public/images/main/bg_bnr.jpg b/public/images/main/bg_bnr.jpg
new file mode 100644
index 0000000..70e0e16
Binary files /dev/null and b/public/images/main/bg_bnr.jpg differ
diff --git a/public/images/main/bg_notice.gif b/public/images/main/bg_notice.gif
new file mode 100644
index 0000000..8eaf4f4
Binary files /dev/null and b/public/images/main/bg_notice.gif differ
diff --git a/public/images/main/icon1.png b/public/images/main/icon1.png
new file mode 100644
index 0000000..dbdb2c7
Binary files /dev/null and b/public/images/main/icon1.png differ
diff --git a/public/images/main/icon2.png b/public/images/main/icon2.png
new file mode 100644
index 0000000..bc56142
Binary files /dev/null and b/public/images/main/icon2.png differ
diff --git a/public/images/main/icon3.png b/public/images/main/icon3.png
new file mode 100644
index 0000000..ccdcecf
Binary files /dev/null and b/public/images/main/icon3.png differ
diff --git a/public/images/main/visual1.jpg b/public/images/main/visual1.jpg
new file mode 100644
index 0000000..e9c72fa
Binary files /dev/null and b/public/images/main/visual1.jpg differ
diff --git a/public/images/main/visual2.jpg b/public/images/main/visual2.jpg
new file mode 100644
index 0000000..b78e8c4
Binary files /dev/null and b/public/images/main/visual2.jpg differ
diff --git a/public/images/main/visual3.jpg b/public/images/main/visual3.jpg
new file mode 100644
index 0000000..0911c90
Binary files /dev/null and b/public/images/main/visual3.jpg differ
diff --git a/public/images/sub/com_icon1.png b/public/images/sub/com_icon1.png
new file mode 100644
index 0000000..bd7b57a
Binary files /dev/null and b/public/images/sub/com_icon1.png differ
diff --git a/public/images/sub/com_icon2.png b/public/images/sub/com_icon2.png
new file mode 100644
index 0000000..fa77ce2
Binary files /dev/null and b/public/images/sub/com_icon2.png differ
diff --git a/public/images/sub/com_icon3.png b/public/images/sub/com_icon3.png
new file mode 100644
index 0000000..eb662cd
Binary files /dev/null and b/public/images/sub/com_icon3.png differ
diff --git a/public/images/sub/com_icon4.png b/public/images/sub/com_icon4.png
new file mode 100644
index 0000000..0da8ec0
Binary files /dev/null and b/public/images/sub/com_icon4.png differ
diff --git a/public/images/sub/product_img1.jpg b/public/images/sub/product_img1.jpg
new file mode 100644
index 0000000..132c31c
Binary files /dev/null and b/public/images/sub/product_img1.jpg differ
diff --git a/public/images/sub/product_img2.jpg b/public/images/sub/product_img2.jpg
new file mode 100644
index 0000000..14e5914
Binary files /dev/null and b/public/images/sub/product_img2.jpg differ
diff --git a/public/images/sub/product_img3.jpg b/public/images/sub/product_img3.jpg
new file mode 100644
index 0000000..c045f4a
Binary files /dev/null and b/public/images/sub/product_img3.jpg differ
diff --git a/public/images/sub/sub1_1.jpg b/public/images/sub/sub1_1.jpg
new file mode 100644
index 0000000..36e9c46
Binary files /dev/null and b/public/images/sub/sub1_1.jpg differ
diff --git a/public/images/sub/sub2_1.jpg b/public/images/sub/sub2_1.jpg
new file mode 100644
index 0000000..cc46724
Binary files /dev/null and b/public/images/sub/sub2_1.jpg differ
diff --git a/public/images/sub/sub2_1_icon.gif b/public/images/sub/sub2_1_icon.gif
new file mode 100644
index 0000000..fc1dcd0
Binary files /dev/null and b/public/images/sub/sub2_1_icon.gif differ
diff --git a/public/images/sub/sub2_1_skype.gif b/public/images/sub/sub2_1_skype.gif
new file mode 100644
index 0000000..0789ce6
Binary files /dev/null and b/public/images/sub/sub2_1_skype.gif differ
diff --git a/public/images/sub/sub2_2_tit.png b/public/images/sub/sub2_2_tit.png
new file mode 100644
index 0000000..6ded24c
Binary files /dev/null and b/public/images/sub/sub2_2_tit.png differ
diff --git a/public/images/sub/sub3_1.jpg b/public/images/sub/sub3_1.jpg
new file mode 100644
index 0000000..28fecdc
Binary files /dev/null and b/public/images/sub/sub3_1.jpg differ
diff --git a/public/images/sub/sub_visual1.jpg b/public/images/sub/sub_visual1.jpg
new file mode 100644
index 0000000..df85468
Binary files /dev/null and b/public/images/sub/sub_visual1.jpg differ
diff --git a/public/images/sub/sub_visual2.jpg b/public/images/sub/sub_visual2.jpg
new file mode 100644
index 0000000..8699f2b
Binary files /dev/null and b/public/images/sub/sub_visual2.jpg differ
diff --git a/public/images/sub/sub_visual3.jpg b/public/images/sub/sub_visual3.jpg
new file mode 100644
index 0000000..4ee3220
Binary files /dev/null and b/public/images/sub/sub_visual3.jpg differ
diff --git a/public/images/sub/sub_visual4.jpg b/public/images/sub/sub_visual4.jpg
new file mode 100644
index 0000000..6708503
Binary files /dev/null and b/public/images/sub/sub_visual4.jpg differ
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..5ec58a7
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,56 @@
+systemDirectory . '/Boot.php';
+
+exit(CodeIgniter\Boot::bootWeb($paths));
diff --git a/public/js/admin.js b/public/js/admin.js
new file mode 100644
index 0000000..f1993c7
--- /dev/null
+++ b/public/js/admin.js
@@ -0,0 +1,81 @@
+/* ------------------------------------------------------------
+ * Name : admin.js
+ * Desc : Admin Javascrip
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+
+function is_NumericKey(evt,obj){
+ var charCode = (evt.which) ? evt.which : event.keyCode;
+ switch(charCode){
+ case 48://0
+ case 49://1
+ case 50://2
+ case 51://3
+ case 52://4
+ case 53://5
+ case 54://6
+ case 55://7
+ case 56://8
+ case 57://9
+ case 96://KeyPad:0
+ case 97://KeyPad:1
+ case 98://KeyPad:2
+ case 99://KeyPad:3
+ case 100://KeyPad:4
+ case 101://KeyPad:5
+ case 102://KeyPad:6
+ case 103://KeyPad:7
+ case 104://KeyPad:8
+ case 105://KeyPad:9
+ break;
+ default:
+ alert('숫자만 가능합니다['+charCode+']');
+ obj.value = obj.value.substring(0,obj.value.length-1);
+ break;
+ }
+}
+function is_NumericType(data){
+ if(!data.match(/^[0-9]+$/)){
+ throw (new Error('숫자가 아닌값['+data+']이 있습니다'));
+ }
+ return true;
+}//
+function change_CurrencyFormat(obj,currencies){
+ //var currencies = document.getElementsByClassName("currency");
+ var total_currency = 0;
+ for(i=0; i { alert("복사가 완료되었습니다."); })
+ .catch(err => { console.log('복사가 오류', err); })
+}
+
+//해당 URL의 페이지를 접속해서 targ의 위치에 표시한다.
+function loadContentForm(url,target) {
+ fetch(url)
+ .then(response => response.text())
+ .then(data => {target.innerHTML = data})
+ .catch(error => console.error('Error loading form:', error));
+}
\ No newline at end of file
diff --git a/public/js/admin/index.js b/public/js/admin/index.js
new file mode 100644
index 0000000..ea76567
--- /dev/null
+++ b/public/js/admin/index.js
@@ -0,0 +1,61 @@
+$(document).ready(function () {
+ //class가 calender인 inputbox용,날짜field용
+ $(".calender").datepicker({
+ changeYear: true,
+ changeMonth: true,
+ yearRange: "-10:+0",
+ dateFormat: "yy-mm-dd"
+ });
+ //id가 batchjobuids_checkbox인 버튼을 클릭시 class가 batchjobuids_checkboxs인 checkbox용
+ $('#batchjobuids_checkbox').click(function (event) {
+ if (this.checked) {
+ $('.batchjobuids_checkboxs').each(function () { //loop checkbox
+ $(this).prop('checked', true); //check
+ });
+ } else {
+ $('.batchjobuids_checkboxs').each(function () { //loop checkbox
+ $(this).prop('checked', false); //uncheck
+ });
+ }
+ });
+ //class가 select-field인 SelectBox용
+ $(".select-field").select2({
+ theme: "classic",
+ width: 'style',
+ dropdownAutoWidth: true
+ });
+ // text editor 초기화
+ //참고: https://phppot.com/menu/php/learn-php/
+ // class가 editor인 textarea용
+ tinymce.init({
+ selector: 'textarea.tinymce',
+ plugins: ['code', 'image', 'preview', 'table', 'emoticons', 'autoresize'],
+ height: 600,
+ // content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }'
+ automatic_uploads: false,
+ images_upload_url: '/tinymce_upload.php',
+ // images_upload_base_path: '/upload_images',
+ images_upload_handler: function (blobInfo, success, failure) {
+ var xhr, formData;
+ xhr = new XMLHttpRequest();
+ xhr.withCredentials = false;
+ xhr.open('POST', '/tinymce_upload.php');
+ xhr.onload = function () {
+ var json;
+ if (xhr.status != 200) {
+ failure('HTTP Error: ' + xhr.status);
+ return;
+ }
+ json = JSON.parse(xhr.responseText);
+ if (!json || typeof json.file_path != 'string') {
+ failure('Invalid JSON: ' + xhr.responseText);
+ return;
+ }
+ success(json.file_path);
+ };
+ formData = new FormData();
+ formData.append('file', blobInfo.blob(), blobInfo.filename());
+ xhr.send(formData);
+ },
+ });
+});
\ No newline at end of file
diff --git a/public/js/admin/left_menu.js b/public/js/admin/left_menu.js
new file mode 100644
index 0000000..1cfc98e
--- /dev/null
+++ b/public/js/admin/left_menu.js
@@ -0,0 +1,13 @@
+function sideMenuToggle(left_menu) {
+ $accordion = $("#accordion")[0];
+ if (accordion.clientWidth == 0){
+ accordion.style.display = "block";
+ $("#accordion").css({ "width": '217px' })
+ $("#menu_button").html("메뉴닫기");
+ }
+ else {
+ accordion.style.display = "none";
+ $("#accordion").css({"width":'20px'})
+ $("#menu_button").html("메뉴열기");
+ }
+}//toggleMenu
\ No newline at end of file
diff --git a/public/js/common/resizeTable.js b/public/js/common/resizeTable.js
new file mode 100644
index 0000000..58a8c6b
--- /dev/null
+++ b/public/js/common/resizeTable.js
@@ -0,0 +1,843 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.validide_resizableTableColumns = {}));
+})(this, (function (exports) {
+ 'use strict';
+
+ var ResizableConstants = /** @class */ (function () {
+ function ResizableConstants() {
+ }
+ ResizableConstants.dataPropertyName = 'validide_rtc_data_object';
+ ResizableConstants.classes = {
+ table: 'rtc-table',
+ wrapper: 'rtc-wrapper',
+ handleContainer: 'rtc-handle-container',
+ handle: 'rtc-handle',
+ tableResizing: 'rtc-table-resizing',
+ columnResizing: 'rtc-column-resizing',
+ };
+ ResizableConstants.attributes = {
+ dataResizable: 'data-rtc-resizable',
+ dataResizableTable: 'data-rtc-resizable-table'
+ };
+ ResizableConstants.data = {
+ resizable: 'rtcResizable',
+ resizableTable: 'rtcResizableTable'
+ };
+ ResizableConstants.events = {
+ pointerDown: ['mousedown', 'touchstart'],
+ pointerMove: ['mousemove', 'touchmove'],
+ pointerUp: ['mouseup', 'touchend'],
+ windowResize: ['resize'],
+ eventResizeStart: 'eventResizeStart.rtc',
+ eventResize: 'eventResize.rtc',
+ eventResizeStop: 'eventResizeStop.rtc'
+ };
+ return ResizableConstants;
+ }());
+
+ var WidthsData = /** @class */ (function () {
+ function WidthsData() {
+ this.column = 0;
+ this.table = 0;
+ }
+ return WidthsData;
+ }());
+ var PointerData = /** @class */ (function () {
+ function PointerData() {
+ this.x = null;
+ this.isDoubleClick = false;
+ }
+ return PointerData;
+ }());
+ var ResizableEventData = /** @class */ (function () {
+ function ResizableEventData(column, dragHandler) {
+ this.pointer = new PointerData();
+ this.originalWidths = new WidthsData();
+ this.newWidths = new WidthsData();
+ this.column = column;
+ this.dragHandler = dragHandler;
+ }
+ return ResizableEventData;
+ }());
+
+ var Utilities = /** @class */ (function () {
+ function Utilities() {
+ }
+ Utilities.kebabCaseToCamelCase = function (str) {
+ return str.replace(Utilities.kebabCaseRegex, function (m) { return m[1].toUpperCase(); });
+ };
+ Utilities.parseStringToType = function (str) {
+ if (str.length == 0 || Utilities.onlyWhiteSpace.test(str))
+ return str;
+ if (Utilities.trueRegex.test(str))
+ return true;
+ if (Utilities.falseRegex.test(str))
+ return false;
+ if (Utilities.notEmptyOrWhiteSpace.test(str)) {
+ var temp = +str;
+ if (!isNaN(temp))
+ return temp;
+ }
+ return str;
+ };
+ Utilities.regexEscapeRegex = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
+ Utilities.kebabCaseRegex = /(\-\w)/g;
+ Utilities.trueRegex = /^true$/i;
+ Utilities.falseRegex = /^false$/i;
+ Utilities.onlyWhiteSpace = /^\s$/;
+ Utilities.notEmptyOrWhiteSpace = /\S/;
+ return Utilities;
+ }());
+
+ var UtilitiesDOM = /** @class */ (function () {
+ function UtilitiesDOM() {
+ }
+ UtilitiesDOM.getDataAttributesValues = function (el) {
+ if (!el)
+ return null;
+ var returnValue = {};
+ if (el.dataset) {
+ for (var prop in el.dataset) {
+ if (el.dataset.hasOwnProperty(prop)) {
+ returnValue[prop] = Utilities.parseStringToType(el.dataset[prop] || '');
+ }
+ }
+ }
+ else {
+ for (var i = 0; i < el.attributes.length; i++) {
+ if (!/^data\-/.test(el.attributes[i].name))
+ continue;
+ var name_1 = Utilities.kebabCaseToCamelCase(el.attributes[i].name.replace('data-', ''));
+ returnValue[name_1] = Utilities.parseStringToType(el.attributes[i].value);
+ }
+ }
+ return returnValue;
+ };
+ return UtilitiesDOM;
+ }());
+
+ var ResizableOptions = /** @class */ (function () {
+ function ResizableOptions(options, element) {
+ if (options === void 0) { options = null; }
+ if (element === void 0) { element = null; }
+ this.resizeFromBody = true;
+ this.minWidth = 40;
+ this.maxWidth = null;
+ this.doubleClickDelay = 500;
+ this.maxInitialWidthHint = null;
+ this.store = null;
+ this.overrideValues(options);
+ this.overrideValuesFromElement(element);
+ }
+ ResizableOptions.prototype.overrideValues = function (options) {
+ if (options === void 0) { options = null; }
+ if (!options)
+ return;
+ for (var prop in options) {
+ if (this.hasOwnProperty(prop)) {
+ this[prop] = options[prop];
+ }
+ }
+ };
+ ResizableOptions.prototype.overrideValuesFromElement = function (element) {
+ if (element === void 0) { element = null; }
+ if (!element)
+ return;
+ var elementOptions = UtilitiesDOM.getDataAttributesValues(element);
+ this.overrideValues(elementOptions);
+ };
+ return ResizableOptions;
+ }());
+
+ var ResizableTableColumns = /** @class */ (function () {
+ function ResizableTableColumns(table, options) {
+ if (typeof table !== 'object' || table === null || table.toString() !== '[object HTMLTableElement]')
+ throw 'Invalid argument: "table".\nResizableTableColumns requires that the table element is a not null HTMLTableElement object!';
+ if (typeof table[ResizableConstants.dataPropertyName] !== 'undefined')
+ throw "Existing \"".concat(ResizableConstants.dataPropertyName, "\" property.\nTable element already has a '").concat(ResizableConstants.dataPropertyName, "' attached object!");
+ this.id = ResizableTableColumns.getInstanceId();
+ this.table = table;
+ this.options = new ResizableOptions(options, table);
+ this.wrapper = null;
+ this.ownerDocument = table.ownerDocument;
+ this.tableHeaders = [];
+ this.dragHandlesContainer = null;
+ this.originalWidths = [];
+ this.eventData = null;
+ this.lastPointerDown = 0;
+ this.init();
+ this.table[ResizableConstants.dataPropertyName] = this;
+ }
+ ResizableTableColumns.prototype.init = function () {
+ this.validateMarkup();
+ this.createHandlerReferences();
+ this.wrapTable();
+ this.assignTableHeaders();
+ this.storeOriginalWidths();
+ this.setHeaderWidths();
+ this.createDragHandles();
+ this.restoreColumnWidths();
+ this.checkTableWidth();
+ this.syncHandleWidths();
+ this.registerWindowResizeHandler();
+ };
+ ResizableTableColumns.prototype.dispose = function () {
+ this.destroyDragHandles();
+ this.restoreOriginalWidths();
+ this.unwrapTable();
+ this.onPointerDownRef = null;
+ this.onPointerMoveRef = null;
+ this.onPointerUpRef = null;
+ this.table[ResizableConstants.dataPropertyName] = void (0);
+ };
+ ResizableTableColumns.prototype.validateMarkup = function () {
+ var theadCount = 0;
+ var tbodyCount = 0;
+ var thead = null;
+ for (var index = 0; index < this.table.childNodes.length; index++) {
+ var element = this.table.childNodes[index];
+ if (element.nodeName === 'THEAD') {
+ theadCount++;
+ thead = element;
+ }
+ else if (element.nodeName === 'TBODY') {
+ tbodyCount++;
+ }
+ }
+ if (thead === null || theadCount !== 1)
+ throw "Markup validation: thead count.\nResizableTableColumns requires that the table element has one(1) table head element. Current count: ".concat(theadCount);
+ if (tbodyCount !== 1)
+ throw "Markup validation: tbody count.\nResizableTableColumns requires that the table element has one(1) table body element. Current count: ".concat(tbodyCount);
+ var theadRowCount = 0;
+ var firstRow = null;
+ for (var index = 0; index < thead.childNodes.length; index++) {
+ var element = thead.childNodes[index];
+ if (element.nodeName === 'TR') {
+ theadRowCount++;
+ if (firstRow === null) {
+ firstRow = element;
+ }
+ }
+ }
+ if (firstRow === null || theadRowCount < 1)
+ throw "Markup validation: thead row count.\nResizableTableColumns requires that the table head element has at least one(1) table row element. Current count: ".concat(theadRowCount);
+ var headerCellsCount = 0;
+ var invalidHeaderCellsCount = 0;
+ for (var index = 0; index < firstRow.childNodes.length; index++) {
+ var element = firstRow.childNodes[index];
+ if (element.nodeName === 'TH') {
+ headerCellsCount++;
+ }
+ else if (element.nodeName === 'TD') {
+ invalidHeaderCellsCount++;
+ }
+ }
+ if (headerCellsCount < 1)
+ throw "Markup validation: thead first row cells count.\nResizableTableColumns requires that the table head's first row element has at least one(1) table header cell element. Current count: ".concat(headerCellsCount);
+ if (invalidHeaderCellsCount !== 0)
+ throw "Markup validation: thead first row invalid.\nResizableTableColumns requires that the table head's first row element has no(0) table cell(TD) elements. Current count: ".concat(invalidHeaderCellsCount);
+ };
+ ResizableTableColumns.prototype.wrapTable = function () {
+ if (this.wrapper)
+ return;
+ this.wrapper = this.ownerDocument.createElement('div');
+ this.wrapper.classList.add(ResizableConstants.classes.wrapper);
+ var tableOriginalParent = this.table.parentNode;
+ tableOriginalParent.insertBefore(this.wrapper, this.table);
+ tableOriginalParent.removeChild(this.table);
+ this.wrapper.appendChild(this.table);
+ this.table.classList.add(ResizableConstants.classes.table);
+ };
+ ResizableTableColumns.prototype.unwrapTable = function () {
+ this.table.classList.remove(ResizableConstants.classes.table);
+ if (!this.wrapper)
+ return;
+ var tableOriginalParent = this.wrapper.parentNode;
+ tableOriginalParent.insertBefore(this.table, this.wrapper);
+ tableOriginalParent.removeChild(this.wrapper);
+ this.wrapper = null;
+ };
+ ResizableTableColumns.prototype.assignTableHeaders = function () {
+ var tableHeader;
+ var firstTableRow;
+ for (var index = 0; index < this.table.childNodes.length; index++) {
+ var element = this.table.childNodes[index];
+ if (element.nodeName === 'THEAD') {
+ tableHeader = element;
+ break;
+ }
+ }
+ if (!tableHeader)
+ return;
+ for (var index = 0; index < tableHeader.childNodes.length; index++) {
+ var element = tableHeader.childNodes[index];
+ if (element.nodeName === 'TR') {
+ firstTableRow = element;
+ break;
+ }
+ }
+ if (!firstTableRow)
+ return;
+ for (var index = 0; index < firstTableRow.childNodes.length; index++) {
+ var element = firstTableRow.childNodes[index];
+ if (element.nodeName === 'TH') {
+ this.tableHeaders.push(element);
+ }
+ }
+ };
+ ResizableTableColumns.prototype.storeOriginalWidths = function () {
+ var _this = this;
+ this.tableHeaders
+ .forEach(function (el) {
+ _this.originalWidths.push({
+ el: el,
+ detail: el.style.width
+ });
+ });
+ this.originalWidths.push({
+ el: this.table,
+ detail: this.table.style.width
+ });
+ };
+ ResizableTableColumns.prototype.restoreOriginalWidths = function () {
+ this.originalWidths
+ .forEach(function (itm) {
+ itm.el.style.width = itm.detail;
+ });
+ };
+ ResizableTableColumns.prototype.setHeaderWidths = function () {
+ var _this = this;
+ this.tableHeaders
+ .forEach(function (el) {
+ var width = el.offsetWidth;
+ var constrainedWidth = _this.constrainWidth(el, width);
+ if (typeof _this.options.maxInitialWidthHint === 'number') {
+ constrainedWidth = Math.min(constrainedWidth, _this.options.maxInitialWidthHint);
+ }
+ _this.updateWidth(el, constrainedWidth, true, false);
+ });
+ };
+ ResizableTableColumns.prototype.constrainWidth = function (el, width) {
+ var result = width;
+ result = Math.max(result, this.options.minWidth || -Infinity);
+ result = Math.min(result, this.options.maxWidth || +Infinity);
+ return result;
+ };
+ ResizableTableColumns.prototype.createDragHandles = function () {
+ var _this = this;
+ var _a;
+ if (this.dragHandlesContainer != null)
+ throw 'Drag handlers already created. Call if you wish to recreate them';
+ this.dragHandlesContainer = this.ownerDocument.createElement('div');
+ (_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.insertBefore(this.dragHandlesContainer, this.table);
+ this.dragHandlesContainer.classList.add(ResizableConstants.classes.handleContainer);
+ this.getResizableHeaders()
+ .forEach(function () {
+ var _a;
+ var handler = _this.ownerDocument.createElement('div');
+ handler.classList.add(ResizableConstants.classes.handle);
+ (_a = _this.dragHandlesContainer) === null || _a === void 0 ? void 0 : _a.appendChild(handler);
+ });
+ ResizableConstants.events.pointerDown
+ .forEach(function (evt, evtIdx) {
+ var _a;
+ (_a = _this.dragHandlesContainer) === null || _a === void 0 ? void 0 : _a.addEventListener(evt, _this.onPointerDownRef, false);
+ });
+ };
+ ResizableTableColumns.prototype.destroyDragHandles = function () {
+ var _this = this;
+ var _a, _b;
+ if (this.dragHandlesContainer !== null) {
+ ResizableConstants.events.pointerDown
+ .forEach(function (evt, evtIdx) {
+ var _a;
+ (_a = _this.dragHandlesContainer) === null || _a === void 0 ? void 0 : _a.removeEventListener(evt, _this.onPointerDownRef, false);
+ });
+ (_b = (_a = this.dragHandlesContainer) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.removeChild(this.dragHandlesContainer);
+ }
+ };
+ ResizableTableColumns.prototype.getDragHandlers = function () {
+ var nodes = this.dragHandlesContainer == null
+ ? null
+ : this.dragHandlesContainer.querySelectorAll(".".concat(ResizableConstants.classes.handle));
+ return nodes
+ ? Array.prototype.slice.call(nodes).filter(function (el) { return el.nodeName === 'DIV'; })
+ : new Array();
+ };
+ ResizableTableColumns.prototype.restoreColumnWidths = function () {
+ if (!this.options.store)
+ return;
+ var tableId = ResizableTableColumns.generateTableId(this.table);
+ if (tableId.length === 0)
+ return;
+ var data = this.options.store.get(tableId);
+ if (!data)
+ return;
+ this.getResizableHeaders()
+ .forEach(function (el) {
+ var width = data.columns[ResizableTableColumns.generateColumnId(el)];
+ if (typeof width !== 'undefined') {
+ ResizableTableColumns.setWidth(el, width);
+ }
+ });
+ if (typeof data.table !== 'undefined') {
+ ResizableTableColumns.setWidth(this.table, data.table);
+ }
+ };
+ ResizableTableColumns.prototype.checkTableWidth = function () {
+ var _a;
+ var wrapperWidth = this.wrapper.clientWidth;
+ var tableWidth = this.table.offsetWidth;
+ var difference = wrapperWidth - tableWidth;
+ if (difference <= 0)
+ return;
+ var resizableWidth = 0;
+ var addedWidth = 0;
+ var headersDetails = [];
+ this.tableHeaders
+ .forEach(function (el, idx) {
+ if (el.hasAttribute(ResizableConstants.attributes.dataResizable)) {
+ var detail = {
+ el: el,
+ detail: el.offsetWidth
+ };
+ headersDetails.push(detail);
+ resizableWidth += detail.detail;
+ }
+ });
+ var leftToAdd = 0;
+ var lastResizableCell = null;
+ var currentDetail;
+ while ((currentDetail = headersDetails.shift())) {
+ leftToAdd = difference - addedWidth;
+ lastResizableCell = currentDetail.el;
+ var extraWidth = Math.floor((currentDetail.detail / resizableWidth) * difference);
+ extraWidth = Math.min(extraWidth, leftToAdd);
+ var newWidth = this.updateWidth(currentDetail.el, currentDetail.detail + extraWidth, false, true);
+ addedWidth += (newWidth - currentDetail.detail);
+ if (addedWidth >= difference)
+ break;
+ }
+ leftToAdd = difference - addedWidth;
+ if (leftToAdd > 0) {
+ var lastCell = ((_a = headersDetails[0]) === null || _a === void 0 ? void 0 : _a.el) || lastResizableCell || this.tableHeaders[this.tableHeaders.length - 1];
+ var lastCellWidth = lastCell.offsetWidth;
+ this.updateWidth(lastCell, lastCellWidth, true, true);
+ }
+ ResizableTableColumns.setWidth(this.table, wrapperWidth);
+ };
+ ResizableTableColumns.prototype.syncHandleWidths = function () {
+ var _this = this;
+ var tableWidth = this.table.clientWidth;
+ ResizableTableColumns.setWidth(this.dragHandlesContainer, tableWidth);
+ this.dragHandlesContainer.style.minWidth = "".concat(tableWidth, "px");
+ var headers = this.getResizableHeaders();
+ this.getDragHandlers()
+ .forEach(function (el, idx) {
+ var height = (_this.options.resizeFromBody ? _this.table : _this.table.tHead).clientHeight;
+ if (idx < headers.length) {
+ var th = headers[idx];
+ var left = th.offsetWidth;
+ left += ResizableTableColumns.getOffset(th).left;
+ left -= ResizableTableColumns.getOffset(_this.dragHandlesContainer).left;
+ el.style.left = "".concat(left, "px");
+ el.style.height = "".concat(height, "px");
+ }
+ });
+ };
+ ResizableTableColumns.prototype.getResizableHeaders = function () {
+ return this.tableHeaders
+ .filter(function (el, idx) {
+ return el.hasAttribute(ResizableConstants.attributes.dataResizable);
+ });
+ };
+ ResizableTableColumns.prototype.handlePointerDown = function (event) {
+ this.handlePointerUp();
+ var target = event ? event.target : null;
+ if (target == null)
+ return;
+ if (target.nodeName !== 'DIV' || !target.classList.contains(ResizableConstants.classes.handle))
+ return;
+ if (typeof event.button === 'number' && event.button !== 0)
+ return; // this is not a left click
+ var dragHandler = target;
+ var gripIndex = this.getDragHandlers().indexOf(dragHandler);
+ var resizableHeaders = this.getResizableHeaders();
+ if (gripIndex >= resizableHeaders.length)
+ return;
+ var millisecondsNow = (new Date()).getTime();
+ var isDoubleClick = (millisecondsNow - this.lastPointerDown) < this.options.doubleClickDelay;
+ var column = resizableHeaders[gripIndex];
+ var columnWidth = column.offsetWidth;
+ var widths = {
+ column: columnWidth,
+ table: this.table.offsetWidth
+ };
+ var eventData = new ResizableEventData(column, dragHandler);
+ eventData.pointer = {
+ x: ResizableTableColumns.getPointerX(event),
+ isDoubleClick: isDoubleClick
+ };
+ eventData.originalWidths = widths;
+ eventData.newWidths = widths;
+ this.detachHandlers(); //make sure we do not have extra handlers
+ this.attachHandlers();
+ this.table.classList.add(ResizableConstants.classes.tableResizing);
+ this.wrapper.classList.add(ResizableConstants.classes.tableResizing);
+ dragHandler.classList.add(ResizableConstants.classes.columnResizing);
+ column.classList.add(ResizableConstants.classes.columnResizing);
+ this.lastPointerDown = millisecondsNow;
+ this.eventData = eventData;
+ var eventToDispatch = new CustomEvent(ResizableConstants.events.eventResizeStart, {
+ detail: {
+ column: column,
+ columnWidth: columnWidth,
+ table: this.table,
+ tableWidth: this.table.clientWidth
+ }
+ });
+ this.table.dispatchEvent(eventToDispatch);
+ event.preventDefault();
+ };
+ ResizableTableColumns.prototype.handlePointerMove = function (event) {
+ if (!this.eventData || !event)
+ return;
+ var difference = (ResizableTableColumns.getPointerX(event) || 0) - (this.eventData.pointer.x || 0);
+ if (difference === 0) {
+ return;
+ }
+ var tableWidth = this.eventData.originalWidths.table + difference;
+ var columnWidth = this.constrainWidth(this.eventData.column, this.eventData.originalWidths.column + difference);
+ ResizableTableColumns.setWidth(this.table, tableWidth);
+ ResizableTableColumns.setWidth(this.eventData.column, columnWidth);
+ this.eventData.newWidths = {
+ column: columnWidth,
+ table: tableWidth
+ };
+ var eventToDispatch = new CustomEvent(ResizableConstants.events.eventResize, {
+ detail: {
+ column: this.eventData.column,
+ columnWidth: columnWidth,
+ table: this.table,
+ tableWidth: tableWidth
+ }
+ });
+ this.table.dispatchEvent(eventToDispatch);
+ };
+ ResizableTableColumns.prototype.handlePointerUp = function () {
+ this.detachHandlers();
+ if (!this.eventData)
+ return;
+ if (this.eventData.pointer.isDoubleClick) {
+ this.handleDoubleClick();
+ }
+ this.table.classList.remove(ResizableConstants.classes.tableResizing);
+ this.wrapper.classList.remove(ResizableConstants.classes.tableResizing);
+ this.eventData.dragHandler.classList.remove(ResizableConstants.classes.columnResizing);
+ this.eventData.column.classList.remove(ResizableConstants.classes.columnResizing);
+ this.checkTableWidth();
+ this.syncHandleWidths();
+ this.refreshWrapperStyle();
+ this.saveColumnWidths();
+ var widths = this.eventData.newWidths || this.eventData.originalWidths;
+ var eventToDispatch = new CustomEvent(ResizableConstants.events.eventResizeStop, {
+ detail: {
+ column: this.eventData.column,
+ columnWidth: widths.column,
+ table: this.table,
+ tableWidth: widths.table
+ }
+ });
+ this.table.dispatchEvent(eventToDispatch);
+ this.eventData = null;
+ };
+ ResizableTableColumns.prototype.handleDoubleClick = function () {
+ if (!this.eventData || !this.eventData.column)
+ return;
+ var column = this.eventData.column;
+ var colIndex = this.tableHeaders.indexOf(column);
+ var maxWidth = 0;
+ var indicesToSkip = [];
+ this.tableHeaders
+ .forEach(function (el, idx) {
+ if (!el.hasAttribute(ResizableConstants.attributes.dataResizable)) {
+ indicesToSkip.push(idx);
+ }
+ });
+ var span = this.ownerDocument.createElement('span');
+ span.style.position = 'absolute';
+ span.style.left = '-99999px';
+ span.style.top = '-99999px';
+ span.style.visibility = 'hidden';
+ this.ownerDocument.body.appendChild(span);
+ var rows = this.table.querySelectorAll('tr');
+ for (var rowIndex = 0; rowIndex < rows.length; rowIndex++) {
+ var element = rows[rowIndex];
+ var cells = element.querySelectorAll('td, th');
+ var currentIndex = 0;
+ for (var cellIndex = 0; cellIndex < cells.length; cellIndex++) {
+ var cell = cells[cellIndex];
+ var colSpan = 1;
+ if (cell.hasAttribute('colspan')) {
+ var colSpanString = cell.getAttribute('colspan') || '1';
+ var parsed = parseInt(colSpanString);
+ if (!isNaN(parsed)) {
+ colSpan = parsed;
+ }
+ else {
+ colSpan = 1;
+ }
+ }
+ if (indicesToSkip.indexOf(cellIndex) === -1
+ && colSpan === 1
+ && currentIndex === colIndex) {
+ maxWidth = Math.max(maxWidth, ResizableTableColumns.getTextWidth(cell, span));
+ break;
+ }
+ currentIndex += colSpan;
+ }
+ }
+ this.ownerDocument.body.removeChild(span);
+ var difference = maxWidth - column.offsetWidth;
+ if (difference === 0) {
+ return;
+ }
+ var tableWidth = this.eventData.originalWidths.table + difference;
+ var columnWidth = this.constrainWidth(this.eventData.column, this.eventData.originalWidths.column + difference);
+ ResizableTableColumns.setWidth(this.table, tableWidth);
+ ResizableTableColumns.setWidth(this.eventData.column, columnWidth);
+ this.eventData.newWidths = {
+ column: columnWidth,
+ table: tableWidth,
+ };
+ var eventToDispatch = new CustomEvent(ResizableConstants.events.eventResize, {
+ detail: {
+ column: this.eventData.column,
+ columnWidth: columnWidth,
+ table: this.table,
+ tableWidth: tableWidth
+ }
+ });
+ this.table.dispatchEvent(eventToDispatch);
+ this.checkTableWidth();
+ this.syncHandleWidths();
+ this.saveColumnWidths();
+ };
+ ResizableTableColumns.prototype.attachHandlers = function () {
+ var _this = this;
+ ResizableConstants.events.pointerMove
+ .forEach(function (evt, evtIdx) {
+ _this.ownerDocument.addEventListener(evt, _this.onPointerMoveRef, false);
+ });
+ ResizableConstants.events.pointerUp
+ .forEach(function (evt, evtIdx) {
+ _this.ownerDocument.addEventListener(evt, _this.onPointerUpRef, false);
+ });
+ };
+ ResizableTableColumns.prototype.detachHandlers = function () {
+ var _this = this;
+ ResizableConstants.events.pointerMove
+ .forEach(function (evt, evtIdx) {
+ _this.ownerDocument.removeEventListener(evt, _this.onPointerMoveRef, false);
+ });
+ ResizableConstants.events.pointerUp
+ .forEach(function (evt, evtIdx) {
+ _this.ownerDocument.removeEventListener(evt, _this.onPointerUpRef, false);
+ });
+ };
+ ResizableTableColumns.prototype.refreshWrapperStyle = function () {
+ if (this.wrapper == null)
+ return;
+ var original = this.wrapper.style.overflowX;
+ this.wrapper.style.overflowX = 'hidden';
+ this.wrapper.style.overflowX = original;
+ };
+ ResizableTableColumns.prototype.saveColumnWidths = function () {
+ if (!this.options.store)
+ return;
+ var tableId = ResizableTableColumns.generateTableId(this.table);
+ if (tableId.length === 0)
+ return;
+ var data = {
+ table: this.table.offsetWidth,
+ columns: {}
+ };
+ this.getResizableHeaders()
+ .forEach(function (el) {
+ data.columns[ResizableTableColumns.generateColumnId(el)] = el.offsetWidth;
+ });
+ this.options.store.set(tableId, data);
+ };
+ ResizableTableColumns.prototype.createHandlerReferences = function () {
+ var _this = this;
+ if (!this.onPointerDownRef) {
+ this.onPointerDownRef = ResizableTableColumns.debounce(function (evt) {
+ _this.handlePointerDown(evt);
+ }, 100, true);
+ }
+ if (!this.onPointerMoveRef) {
+ this.onPointerMoveRef = ResizableTableColumns.debounce(function (evt) {
+ _this.handlePointerMove(evt);
+ }, 5, false);
+ }
+ if (!this.onPointerUpRef) {
+ this.onPointerUpRef = ResizableTableColumns.debounce(function (evt) {
+ _this.handlePointerUp();
+ }, 100, true);
+ }
+ };
+ ResizableTableColumns.prototype.registerWindowResizeHandler = function () {
+ var win = this.ownerDocument.defaultView;
+ if (ResizableTableColumns.windowResizeHandlerRef)
+ return;
+ ResizableTableColumns.windowResizeHandlerRef = ResizableTableColumns.debounce(ResizableTableColumns.onWindowResize, 50, false);
+ ResizableConstants.events.windowResize
+ .forEach(function (evt, idx) {
+ win === null || win === void 0 ? void 0 : win.addEventListener(evt, ResizableTableColumns.windowResizeHandlerRef, false);
+ });
+ };
+ ResizableTableColumns.prototype.handleWindowResize = function () {
+ this.checkTableWidth();
+ this.syncHandleWidths();
+ this.saveColumnWidths();
+ };
+ ResizableTableColumns.prototype.updateWidth = function (cell, suggestedWidth, skipConstrainCheck, skipTableResize) {
+ var originalCellWidth = cell.offsetWidth;
+ var columnWidth = skipConstrainCheck
+ ? suggestedWidth
+ : this.constrainWidth(cell, suggestedWidth);
+ ResizableTableColumns.setWidth(cell, columnWidth);
+ if (!skipTableResize) {
+ var difference = columnWidth - originalCellWidth;
+ var tableWidth = this.table.offsetWidth + difference;
+ ResizableTableColumns.setWidth(this.table, tableWidth);
+ }
+ return columnWidth;
+ };
+ ResizableTableColumns.onWindowResize = function (event) {
+ var win = event ? event.target : null;
+ if (win == null)
+ return;
+ var tables = win.document.querySelectorAll(".".concat(ResizableConstants.classes.table));
+ for (var index = 0; index < tables.length; index++) {
+ var table = tables[index];
+ if (typeof table[ResizableConstants.dataPropertyName] !== 'object')
+ continue;
+ table[ResizableConstants.dataPropertyName].handleWindowResize();
+ }
+ };
+ ResizableTableColumns.generateColumnId = function (el) {
+ var columnId = (el.getAttribute(ResizableConstants.attributes.dataResizable) || '')
+ .trim()
+ .replace(/\./g, '_');
+ return columnId;
+ };
+ ResizableTableColumns.generateTableId = function (table) {
+ var tableId = (table.getAttribute(ResizableConstants.attributes.dataResizableTable) || '')
+ .trim()
+ .replace(/\./g, '_');
+ return tableId.length
+ ? "rtc/".concat(tableId)
+ : tableId;
+ };
+ ResizableTableColumns.setWidth = function (element, width) {
+ var strWidth = width.toFixed(2);
+ strWidth = width > 0 ? strWidth : '0';
+ element.style.width = "".concat(strWidth, "px");
+ };
+ ResizableTableColumns.getInstanceId = function () {
+ return ResizableTableColumns.instancesCount++;
+ };
+ ResizableTableColumns.getPointerX = function (event) {
+ if (event.type.indexOf('touch') === 0) {
+ var tEvent = event;
+ if (tEvent.touches && tEvent.touches.length) {
+ return tEvent.touches[0].pageX;
+ }
+ if (tEvent.changedTouches && tEvent.changedTouches.length) {
+ return tEvent.changedTouches[0].pageX;
+ }
+ }
+ return event.pageX;
+ };
+ ResizableTableColumns.getTextWidth = function (contentElement, measurementElement) {
+ var _a, _b;
+ if (!contentElement || !measurementElement)
+ return 0;
+ var text = ((_a = contentElement.textContent) === null || _a === void 0 ? void 0 : _a.trim().replace(/\s/g, ' ')) + ' '; //add extra space to ensure we are not add the `...`
+ var styles = (_b = contentElement.ownerDocument.defaultView) === null || _b === void 0 ? void 0 : _b.getComputedStyle(contentElement);
+ ['fontFamily', 'fontSize', 'fontWeight', 'padding', 'border', 'boxSizing']
+ .forEach(function (prop) {
+ measurementElement.style[prop] = styles[prop];
+ });
+ measurementElement.innerHTML = text;
+ return measurementElement.offsetWidth;
+ };
+ ResizableTableColumns.getOffset = function (el) {
+ if (!el)
+ return { top: 0, left: 0 };
+ var rect = el.getBoundingClientRect();
+ return {
+ top: rect.top + el.ownerDocument.body.scrollTop,
+ left: rect.left + el.ownerDocument.body.scrollLeft
+ };
+ };
+ ResizableTableColumns.instancesCount = 0;
+ ResizableTableColumns.windowResizeHandlerRef = null;
+ ResizableTableColumns.debounce = function (func, wait, immediate) {
+ var timeout = null;
+ var debounced = function () {
+ var args = [];
+ for (var _i = 0; _i < arguments.length; _i++) {
+ args[_i] = arguments[_i];
+ }
+ var later = function () {
+ timeout = null;
+ if (!immediate) {
+ func.apply(void 0, args);
+ }
+ };
+ var callNow = immediate && !timeout;
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ timeout = setTimeout(later, wait);
+ if (callNow) {
+ func.apply(void 0, args);
+ }
+ };
+ return debounced;
+ };
+ return ResizableTableColumns;
+ }());
+
+ exports.PointerData = PointerData;
+ exports.ResizableConstants = ResizableConstants;
+ exports.ResizableEventData = ResizableEventData;
+ exports.ResizableOptions = ResizableOptions;
+ exports.ResizableTableColumns = ResizableTableColumns;
+ exports.Utilities = Utilities;
+ exports.UtilitiesDOM = UtilitiesDOM;
+ exports.WidthsData = WidthsData;
+
+}));
+(function (window, ResizableTableColumns, undefined) {
+ var store = window.store && window.store.enabled ? window.store : null;
+ var els = document.querySelectorAll('table.data');
+ for (var index = 0; index < els.length; index++) {
+ var table = els[index];
+ if (table['rtc_data_object']) {
+ continue;
+ }
+ var options = {
+ store: store
+ };
+ if (table.querySelectorAll('thead > tr').length > 1) {
+ options.resizeFromBody = false;
+ }
+ new ResizableTableColumns(els[index], options);
+ }
+})(window, window.validide_resizableTableColumns.ResizableTableColumns, void (0));
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/public/js/empty.js b/public/js/empty.js
new file mode 100644
index 0000000..f973e71
--- /dev/null
+++ b/public/js/empty.js
@@ -0,0 +1,6 @@
+/* ------------------------------------------------------------
+ * Name : front.js
+ * Desc : Front Javascrip
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
diff --git a/public/js/front.js b/public/js/front.js
new file mode 100644
index 0000000..6bea8a4
--- /dev/null
+++ b/public/js/front.js
@@ -0,0 +1,93 @@
+/* ------------------------------------------------------------
+ * Name : front.js
+ * Desc : Front Javascrip
+ * Created : 2016/9/11 Tri-aBility by Junheum,Choi
+ * Updated :
+ ------------------------------------------------------------ */
+
+function trim(str){
+ return this.replace(/(^\s*)|(\s*$)/gi, "");
+}//
+
+function bookmarksite(title,url) {
+ if (window.sidebar) // firefox
+ window.sidebar.addPanel(title, url, "");
+ else if(window.opera && window.print){ // opera
+ var elem = document.createElement('a');
+ elem.setAttribute('href',url);
+ elem.setAttribute('title',title);
+ elem.setAttribute('rel','sidebar');
+ elem.click();
+ }
+ else if(document.all) // ie
+ window.external.AddFavorite(url, title);
+}//
+
+function captcha_refresh(refresh_url) {
+ $.ajax({
+ type: 'POST',
+ url: refresh_url,
+ success: function(data, status, xhr){
+ if(data)
+ $('#captcha_span').html(data);
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ console.log(jqXHR.responseText);
+ console.log(textStatus+'=>'+errorThrown);
+ }
+ });//ajax
+}//
+
+function is_NumericKey(evt,obj){
+ var charCode = (evt.which) ? evt.which : event.keyCode;
+ switch(charCode){
+ case 48://0
+ case 49://1
+ case 50://2
+ case 51://3
+ case 52://4
+ case 53://5
+ case 54://6
+ case 55://7
+ case 56://8
+ case 57://9
+ case 96://KeyPad:0
+ case 97://KeyPad:1
+ case 98://KeyPad:2
+ case 99://KeyPad:3
+ case 100://KeyPad:4
+ case 101://KeyPad:5
+ case 102://KeyPad:6
+ case 103://KeyPad:7
+ case 104://KeyPad:8
+ case 105://KeyPad:9
+ break;
+ default:
+ alert('숫자만 가능합니다['+charCode+']');
+ obj.value = obj.value.substring(0,obj.value.length-1);
+ break;
+ }
+}
+function is_NumericType(data){
+ if(!data.match(/^[0-9]+$/)){
+ throw (new Error('숫자가 아닌값['+data+']이 있습니다'));
+ }
+ return true;
+}//
+function change_CurrencyFormat(obj,currencies){
+ //var currencies = document.getElementsByClassName("currency");
+ var total_currency = 0;
+ for(i=0; i'+errorThrown);
+ }
+ });//ajax
+}//
+
+function is_NumericKey(evt,obj){
+ var charCode = (evt.which) ? evt.which : event.keyCode;
+ switch(charCode){
+ case 48://0
+ case 49://1
+ case 50://2
+ case 51://3
+ case 52://4
+ case 53://5
+ case 54://6
+ case 55://7
+ case 56://8
+ case 57://9
+ case 96://KeyPad:0
+ case 97://KeyPad:1
+ case 98://KeyPad:2
+ case 99://KeyPad:3
+ case 100://KeyPad:4
+ case 101://KeyPad:5
+ case 102://KeyPad:6
+ case 103://KeyPad:7
+ case 104://KeyPad:8
+ case 105://KeyPad:9
+ break;
+ default:
+ alert('숫자만 가능합니다['+charCode+']');
+ obj.value = obj.value.substring(0,obj.value.length-1);
+ break;
+ }
+}
+function is_NumericType(data){
+ if(!data.match(/^[0-9]+$/)){
+ throw (new Error('숫자가 아닌값['+data+']이 있습니다'));
+ }
+ return true;
+}//
+function change_CurrencyFormat(obj,currencies){
+ //var currencies = document.getElementsByClassName("currency");
+ var total_currency = 0;
+ for(i=0; i
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..9e60f97
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/public/test.php b/public/test.php
new file mode 100644
index 0000000..04180a1
--- /dev/null
+++ b/public/test.php
@@ -0,0 +1,3 @@
+ $fileName
+ ));
+}
diff --git a/spark b/spark
new file mode 100644
index 0000000..992d044
--- /dev/null
+++ b/spark
@@ -0,0 +1,84 @@
+#!/usr/bin/env php
+
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+/*
+ * --------------------------------------------------------------------
+ * CODEIGNITER COMMAND-LINE TOOLS
+ * --------------------------------------------------------------------
+ * The main entry point into the CLI system and allows you to run
+ * commands and perform maintenance on your application.
+ */
+
+/*
+ *---------------------------------------------------------------
+ * CHECK SERVER API
+ *---------------------------------------------------------------
+ */
+
+// Refuse to run when called from php-cgi
+if (str_starts_with(PHP_SAPI, 'cgi')) {
+ exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n");
+}
+
+/*
+ *---------------------------------------------------------------
+ * CHECK PHP VERSION
+ *---------------------------------------------------------------
+ */
+
+$minPhpVersion = '8.1'; // If you update this, don't forget to update `public/index.php`.
+if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
+ $message = sprintf(
+ 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
+ $minPhpVersion,
+ PHP_VERSION
+ );
+
+ exit($message);
+}
+
+// We want errors to be shown when using it from the CLI.
+error_reporting(E_ALL);
+ini_set('display_errors', '1');
+
+/*
+ *---------------------------------------------------------------
+ * SET THE CURRENT DIRECTORY
+ *---------------------------------------------------------------
+ */
+
+// Path to the front controller
+define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
+
+// Ensure the current directory is pointing to the front controller's directory
+chdir(FCPATH);
+
+/*
+ *---------------------------------------------------------------
+ * BOOTSTRAP THE APPLICATION
+ *---------------------------------------------------------------
+ * This process sets up the path constants, loads and registers
+ * our autoloader, along with Composer's, loads our constants
+ * and fires up an environment-specific bootstrapping.
+ */
+
+// LOAD OUR PATHS CONFIG FILE
+// This is the line that might need to be changed, depending on your folder structure.
+require FCPATH . '../app/Config/Paths.php';
+// ^^^ Change this line if you move your application folder
+
+$paths = new Config\Paths();
+
+// LOAD THE FRAMEWORK BOOTSTRAP FILE
+require $paths->systemDirectory . '/Boot.php';
+
+exit(CodeIgniter\Boot::bootSpark($paths));
diff --git a/tests/.htaccess b/tests/.htaccess
new file mode 100644
index 0000000..3462048
--- /dev/null
+++ b/tests/.htaccess
@@ -0,0 +1,6 @@
+
+ Require all denied
+
+
+ Deny from all
+
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..fc40e44
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,118 @@
+# Running Application Tests
+
+This is the quick-start to CodeIgniter testing. Its intent is to describe what
+it takes to set up your application and get it ready to run unit tests.
+It is not intended to be a full description of the test features that you can
+use to test your application. Those details can be found in the documentation.
+
+## Resources
+
+* [CodeIgniter 4 User Guide on Testing](https://codeigniter.com/user_guide/testing/index.html)
+* [PHPUnit docs](https://phpunit.de/documentation.html)
+* [Any tutorials on Unit testing in CI4?](https://forum.codeigniter.com/showthread.php?tid=81830)
+
+## Requirements
+
+It is recommended to use the latest version of PHPUnit. At the time of this
+writing, we are running version 9.x. Support for this has been built into the
+**composer.json** file that ships with CodeIgniter and can easily be installed
+via [Composer](https://getcomposer.org/) if you don't already have it installed globally.
+
+```console
+> composer install
+```
+
+If running under macOS or Linux, you can create a symbolic link to make running tests a touch nicer.
+
+```console
+> ln -s ./vendor/bin/phpunit ./phpunit
+```
+
+You also need to install [XDebug](https://xdebug.org/docs/install) in order
+for code coverage to be calculated successfully. After installing `XDebug`, you must add `xdebug.mode=coverage` in the **php.ini** file to enable code coverage.
+
+## Setting Up
+
+A number of the tests use a running database.
+In order to set up the database edit the details for the `tests` group in
+**app/Config/Database.php** or **.env**.
+Make sure that you provide a database engine that is currently running on your machine.
+More details on a test database setup are in the
+[Testing Your Database](https://codeigniter.com/user_guide/testing/database.html) section of the documentation.
+
+## Running the tests
+
+The entire test suite can be run by simply typing one command-line command from the main directory.
+
+```console
+> ./phpunit
+```
+
+If you are using Windows, use the following command.
+
+```console
+> vendor\bin\phpunit
+```
+
+You can limit tests to those within a single test directory by specifying the
+directory name after phpunit.
+
+```console
+> ./phpunit app/Models
+```
+
+## Generating Code Coverage
+
+To generate coverage information, including HTML reports you can view in your browser,
+you can use the following command:
+
+```console
+> ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m
+```
+
+This runs all of the tests again collecting information about how many lines,
+functions, and files are tested. It also reports the percentage of the code that is covered by tests.
+It is collected in two formats: a simple text file that provides an overview as well
+as a comprehensive collection of HTML files that show the status of every line of code in the project.
+
+The text file can be found at **tests/coverage.txt**.
+The HTML files can be viewed by opening **tests/coverage/index.html** in your favorite browser.
+
+## PHPUnit XML Configuration
+
+The repository has a ``phpunit.xml.dist`` file in the project root that's used for
+PHPUnit configuration. This is used to provide a default configuration if you
+do not have your own configuration file in the project root.
+
+The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml``
+(which is git ignored), and to tailor it as you see fit.
+For instance, you might wish to exclude database tests, or automatically generate
+HTML code coverage reports.
+
+## Test Cases
+
+Every test needs a *test case*, or class that your tests extend. CodeIgniter 4
+provides one class that you may use directly:
+* `CodeIgniter\Test\CIUnitTestCase`
+
+Most of the time you will want to write your own test cases that extend `CIUnitTestCase`
+to hold functions and services common to your test suites.
+
+## Creating Tests
+
+All tests go in the **tests/** directory. Each test file is a class that extends a
+**Test Case** (see above) and contains methods for the individual tests. These method
+names must start with the word "test" and should have descriptive names for precisely what
+they are testing:
+`testUserCanModifyFile()` `testOutputColorMatchesInput()` `testIsLoggedInFailsWithInvalidUser()`
+
+Writing tests is an art, and there are many resources available to help learn how.
+Review the links above and always pay attention to your code coverage.
+
+### Database Tests
+
+Tests can include migrating, seeding, and testing against a mock or live database.
+Be sure to modify the test case (or create your own) to point to your seed and migrations
+and include any additional steps to be run before tests in the `setUp()` method.
+See [Testing Your Database](https://codeigniter.com/user_guide/testing/database.html)
+for details.
diff --git a/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php
new file mode 100644
index 0000000..a73356d
--- /dev/null
+++ b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php
@@ -0,0 +1,37 @@
+forge->addField('id');
+ $this->forge->addField([
+ 'name' => ['type' => 'varchar', 'constraint' => 31],
+ 'uid' => ['type' => 'varchar', 'constraint' => 31],
+ 'class' => ['type' => 'varchar', 'constraint' => 63],
+ 'icon' => ['type' => 'varchar', 'constraint' => 31],
+ 'summary' => ['type' => 'varchar', 'constraint' => 255],
+ 'created_at' => ['type' => 'datetime', 'null' => true],
+ 'updated_at' => ['type' => 'datetime', 'null' => true],
+ 'deleted_at' => ['type' => 'datetime', 'null' => true],
+ ]);
+
+ $this->forge->addKey('name');
+ $this->forge->addKey('uid');
+ $this->forge->addKey(['deleted_at', 'id']);
+ $this->forge->addKey('created_at');
+
+ $this->forge->createTable('factories');
+ }
+
+ public function down(): void
+ {
+ $this->forge->dropTable('factories');
+ }
+}
diff --git a/tests/_support/Database/Seeds/ExampleSeeder.php b/tests/_support/Database/Seeds/ExampleSeeder.php
new file mode 100644
index 0000000..619fc27
--- /dev/null
+++ b/tests/_support/Database/Seeds/ExampleSeeder.php
@@ -0,0 +1,41 @@
+ 'Test Factory',
+ 'uid' => 'test001',
+ 'class' => 'Factories\Tests\NewFactory',
+ 'icon' => 'fas fa-puzzle-piece',
+ 'summary' => 'Longer sample text for testing',
+ ],
+ [
+ 'name' => 'Widget Factory',
+ 'uid' => 'widget',
+ 'class' => 'Factories\Tests\WidgetPlant',
+ 'icon' => 'fas fa-puzzle-piece',
+ 'summary' => 'Create widgets in your factory',
+ ],
+ [
+ 'name' => 'Evil Factory',
+ 'uid' => 'evil-maker',
+ 'class' => 'Factories\Evil\MyFactory',
+ 'icon' => 'fas fa-book-dead',
+ 'summary' => 'Abandon all hope, ye who enter here',
+ ],
+ ];
+
+ $builder = $this->db->table('factories');
+
+ foreach ($factories as $factory) {
+ $builder->insert($factory);
+ }
+ }
+}
diff --git a/tests/_support/Libraries/ConfigReader.php b/tests/_support/Libraries/ConfigReader.php
new file mode 100644
index 0000000..40d057a
--- /dev/null
+++ b/tests/_support/Libraries/ConfigReader.php
@@ -0,0 +1,17 @@
+findAll();
+
+ // Make sure the count is as expected
+ $this->assertCount(3, $objects);
+ }
+
+ public function testSoftDeleteLeavesRow(): void
+ {
+ $model = new ExampleModel();
+ $this->setPrivateProperty($model, 'useSoftDeletes', true);
+ $this->setPrivateProperty($model, 'tempUseSoftDeletes', true);
+
+ /** @var stdClass $object */
+ $object = $model->first();
+ $model->delete($object->id);
+
+ // The model should no longer find it
+ $this->assertNull($model->find($object->id));
+
+ // ... but it should still be in the database
+ $result = $model->builder()->where('id', $object->id)->get()->getResult();
+
+ $this->assertCount(1, $result);
+ }
+}
diff --git a/tests/index.html b/tests/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/tests/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/tests/session/ExampleSessionTest.php b/tests/session/ExampleSessionTest.php
new file mode 100644
index 0000000..6ada0c5
--- /dev/null
+++ b/tests/session/ExampleSessionTest.php
@@ -0,0 +1,18 @@
+set('logged_in', 123);
+ $this->assertSame(123, $session->get('logged_in'));
+ }
+}
diff --git a/tests/unit/HealthTest.php b/tests/unit/HealthTest.php
new file mode 100644
index 0000000..25f229b
--- /dev/null
+++ b/tests/unit/HealthTest.php
@@ -0,0 +1,50 @@
+assertTrue(defined('APPPATH'));
+ }
+
+ public function testBaseUrlHasBeenSet(): void
+ {
+ $validation = Services::validation();
+
+ $env = false;
+
+ // Check the baseURL in .env
+ if (is_file(HOMEPATH . '.env')) {
+ $env = preg_grep('/^app\.baseURL = ./', file(HOMEPATH . '.env')) !== false;
+ }
+
+ if ($env) {
+ // BaseURL in .env is a valid URL?
+ // phpunit.xml.dist sets app.baseURL in $_SERVER
+ // So if you set app.baseURL in .env, it takes precedence
+ $config = new App();
+ $this->assertTrue(
+ $validation->check($config->baseURL, 'valid_url'),
+ 'baseURL "' . $config->baseURL . '" in .env is not valid URL'
+ );
+ }
+
+ // Get the baseURL in app/Config/App.php
+ // You can't use Config\App, because phpunit.xml.dist sets app.baseURL
+ $reader = new ConfigReader();
+
+ // BaseURL in app/Config/App.php is a valid URL?
+ $this->assertTrue(
+ $validation->check($reader->baseURL, 'valid_url'),
+ 'baseURL "' . $reader->baseURL . '" in app/Config/App.php is not valid URL'
+ );
+ }
+}
diff --git a/writable/.htaccess b/writable/.htaccess
new file mode 100644
index 0000000..a94c5b0
--- /dev/null
+++ b/writable/.htaccess
@@ -0,0 +1,12 @@
+
+ Require all denied
+
+
+ Deny from all
+
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteRule ^(.*)$ index.php/$1 [L]
+
\ No newline at end of file
diff --git a/writable/cache/index.html b/writable/cache/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/writable/cache/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/writable/excel/index.html b/writable/excel/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/writable/excel/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/writable/index.html b/writable/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/writable/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/writable/logs/index.html b/writable/logs/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/writable/logs/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+
diff --git a/writable/uploads/index.html b/writable/uploads/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/writable/uploads/index.html
@@ -0,0 +1,11 @@
+
+
+
+ 403 Forbidden
+
+
+
+Directory access is forbidden.
+
+
+