KIM-JINO5 5 ヶ月 前
コミット
103bd4d357
100 ファイル変更12163 行追加0 行削除
  1. 61 0
      .env
  2. 57 0
      .gitignore
  3. 66 0
      README.md
  4. 32 0
      app/Console/Kernel.php
  5. 50 0
      app/Exceptions/Handler.php
  6. 440 0
      app/Helpers/common.php
  7. 429 0
      app/Helpers/common.php~
  8. 183 0
      app/Helpers/string.php
  9. 50 0
      app/Http/Controllers/Account/CertifyController.php
  10. 84 0
      app/Http/Controllers/Account/CommentController.php
  11. 137 0
      app/Http/Controllers/Account/EmailController.php
  12. 68 0
      app/Http/Controllers/Account/LeaveController.php
  13. 37 0
      app/Http/Controllers/Account/LoginLogController.php
  14. 152 0
      app/Http/Controllers/Account/ModifyController.php
  15. 82 0
      app/Http/Controllers/Account/PasswordCampaignController.php
  16. 78 0
      app/Http/Controllers/Account/PasswordChangeController.php
  17. 87 0
      app/Http/Controllers/Account/PostController.php
  18. 32 0
      app/Http/Controllers/Account/ProfileController.php
  19. 68 0
      app/Http/Controllers/Admin/AjaxController.php
  20. 110 0
      app/Http/Controllers/Admin/Board/Blame/CommentController.php
  21. 111 0
      app/Http/Controllers/Admin/Board/Blame/PostController.php
  22. 158 0
      app/Http/Controllers/Admin/Board/Board/AuthorityController.php
  23. 152 0
      app/Http/Controllers/Admin/Board/Board/CategoryController.php
  24. 185 0
      app/Http/Controllers/Admin/Board/Board/CommentController.php
  25. 186 0
      app/Http/Controllers/Admin/Board/Board/ExpController.php
  26. 171 0
      app/Http/Controllers/Admin/Board/Board/GeneralController.php
  27. 298 0
      app/Http/Controllers/Admin/Board/Board/ListController.php
  28. 123 0
      app/Http/Controllers/Admin/Board/Board/ManagerController.php
  29. 300 0
      app/Http/Controllers/Admin/Board/Board/NotifyController.php
  30. 190 0
      app/Http/Controllers/Admin/Board/Board/PostController.php
  31. 188 0
      app/Http/Controllers/Admin/Board/Board/ViewController.php
  32. 197 0
      app/Http/Controllers/Admin/Board/Board/WriteController.php
  33. 87 0
      app/Http/Controllers/Admin/Board/CommentController.php
  34. 133 0
      app/Http/Controllers/Admin/Board/File/DownloadController.php
  35. 111 0
      app/Http/Controllers/Admin/Board/File/UploadController.php
  36. 267 0
      app/Http/Controllers/Admin/Board/Group/ListController.php
  37. 101 0
      app/Http/Controllers/Admin/Board/Group/ManagerController.php
  38. 83 0
      app/Http/Controllers/Admin/Board/History/CommentController.php
  39. 83 0
      app/Http/Controllers/Admin/Board/History/PostController.php
  40. 123 0
      app/Http/Controllers/Admin/Board/ImageController.php
  41. 103 0
      app/Http/Controllers/Admin/Board/Like/CommentController.php
  42. 112 0
      app/Http/Controllers/Admin/Board/Like/PostController.php
  43. 84 0
      app/Http/Controllers/Admin/Board/Link/ListController.php
  44. 89 0
      app/Http/Controllers/Admin/Board/Link/LogController.php
  45. 93 0
      app/Http/Controllers/Admin/Board/PostController.php
  46. 87 0
      app/Http/Controllers/Admin/Board/TagController.php
  47. 127 0
      app/Http/Controllers/Admin/Board/Trash/CommentController.php
  48. 127 0
      app/Http/Controllers/Admin/Board/Trash/PostController.php
  49. 112 0
      app/Http/Controllers/Admin/Config/Form/EmailController.php
  50. 70 0
      app/Http/Controllers/Admin/Config/Form/NoteController.php
  51. 70 0
      app/Http/Controllers/Admin/Config/Form/SmsController.php
  52. 58 0
      app/Http/Controllers/Admin/Config/Form/TelegramController.php
  53. 84 0
      app/Http/Controllers/Admin/Config/Layout/LogoController.php
  54. 64 0
      app/Http/Controllers/Admin/Config/Layout/MetaController.php
  55. 59 0
      app/Http/Controllers/Admin/Config/OptimizeController.php
  56. 86 0
      app/Http/Controllers/Admin/Config/Register/BasicController.php
  57. 58 0
      app/Http/Controllers/Admin/Config/Register/LoginController.php
  58. 52 0
      app/Http/Controllers/Admin/Config/Register/ModifyController.php
  59. 84 0
      app/Http/Controllers/Admin/Config/Register/NotifyController.php
  60. 60 0
      app/Http/Controllers/Admin/Config/Register/TossController.php
  61. 359 0
      app/Http/Controllers/Admin/Config/ServerController.php
  62. 60 0
      app/Http/Controllers/Admin/Config/Setting/AccessController.php
  63. 88 0
      app/Http/Controllers/Admin/Config/Setting/BasicController.php
  64. 93 0
      app/Http/Controllers/Admin/Config/Setting/CompanyController.php
  65. 62 0
      app/Http/Controllers/Admin/Config/Setting/ExpController.php
  66. 58 0
      app/Http/Controllers/Admin/Config/Setting/GeneralController.php
  67. 73 0
      app/Http/Controllers/Admin/Config/Setting/NoteController.php
  68. 60 0
      app/Http/Controllers/Admin/Config/Setting/NotifyController.php
  69. 61 0
      app/Http/Controllers/Admin/Config/Test/EmailController.php
  70. 70 0
      app/Http/Controllers/Admin/Config/Test/SmsController.php
  71. 86 0
      app/Http/Controllers/Admin/Config/Test/TelegramController.php
  72. 151 0
      app/Http/Controllers/Admin/Page/Banner/GroupController.php
  73. 286 0
      app/Http/Controllers/Admin/Page/Banner/ListController.php
  74. 205 0
      app/Http/Controllers/Admin/Page/DocumentController.php
  75. 187 0
      app/Http/Controllers/Admin/Page/MenuController.php
  76. 249 0
      app/Http/Controllers/Admin/Page/PopupController.php
  77. 42 0
      app/Http/Controllers/Admin/Popup/SmsController.php
  78. 89 0
      app/Http/Controllers/Admin/Popup/UserController.php
  79. 161 0
      app/Http/Controllers/Admin/Sms/Book/ListController.php
  80. 155 0
      app/Http/Controllers/Admin/Sms/Book/UserController.php
  81. 69 0
      app/Http/Controllers/Admin/Sms/ConfigController.php
  82. 157 0
      app/Http/Controllers/Admin/Sms/FavoriteController.php
  83. 65 0
      app/Http/Controllers/Admin/Sms/HistoryController.php
  84. 68 0
      app/Http/Controllers/Admin/Sms/ResultController.php
  85. 166 0
      app/Http/Controllers/Admin/Sms/SendController.php
  86. 70 0
      app/Http/Controllers/Admin/User/Dormant/ConfigController.php
  87. 60 0
      app/Http/Controllers/Admin/User/Dormant/Form/EmailController.php
  88. 54 0
      app/Http/Controllers/Admin/User/Dormant/Form/SmsController.php
  89. 169 0
      app/Http/Controllers/Admin/User/Dormant/ListController.php
  90. 80 0
      app/Http/Controllers/Admin/User/Dormant/NotifyController.php
  91. 154 0
      app/Http/Controllers/Admin/User/GroupController.php
  92. 356 0
      app/Http/Controllers/Admin/User/ListController.php
  93. 64 0
      app/Http/Controllers/Admin/User/Log/EmailController.php
  94. 65 0
      app/Http/Controllers/Admin/User/Log/Login/LogController.php
  95. 164 0
      app/Http/Controllers/Admin/User/Log/Login/StatController.php
  96. 64 0
      app/Http/Controllers/Admin/User/Log/NameController.php
  97. 113 0
      app/Http/Controllers/AdminController.php
  98. 296 0
      app/Http/Controllers/ApiController.php
  99. 40 0
      app/Http/Controllers/Auth/ConfirmPasswordController.php
  100. 65 0
      app/Http/Controllers/Auth/ForgotAccountController.php

+ 61 - 0
.env

@@ -0,0 +1,61 @@
+APP_NAME=BLOG
+APP_ENV=local
+APP_KEY=base64:AUqIa1L0F99tQlxZNeSKS2cr0Fbm6Wfisd4D6y0plC0=
+APP_DEBUG=true
+APP_URL=https://local-blog.web.or.kr
+
+LOG_CHANNEL=stack
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=mysql
+DB_HOST=db.web.or.kr
+DB_PORT=3306
+DB_DATABASE=web
+DB_USERNAME=admin
+DB_PASSWORD=bluescreen!!
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=redis
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=redis
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+MEMCACHED_HOST=
+
+REDIS_HOST=redis
+REDIS_PASSWORD=bluescreen!!
+REDIS_PORT=6379
+REDIS_DB=0
+REDIS_CACHE_DB=0
+
+MAIL_MAILER=smtp
+MAIL_HOST=${SMTP_DOMAIN}
+MAIL_PORT=${SMTP_PORT}
+MAIL_USERNAME=${SMTP_USER}
+MAIL_PASSWORD=${SMTP_PASS}
+MAIL_ENCRYPTION=ssl
+MAIL_FROM_ADDRESS=${SMTP_USER}
+MAIL_FROM_NAME="${APP_NAME}"
+MAIL_SMTP_TIMEOUT=30
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_HOST=
+PUSHER_PORT=443
+PUSHER_SCHEME=https
+PUSHER_APP_CLUSTER=mt1
+
+VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+VITE_PUSHER_HOST="${PUSHER_HOST}"
+VITE_PUSHER_PORT="${PUSHER_PORT}"
+VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
+VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

+ 57 - 0
.gitignore

@@ -0,0 +1,57 @@
+# =========================
+# Laravel / PHP
+# =========================
+/vendor/
+!.env.example
+.phpunit.result.cache
+.php_cs.cache
+
+# =========================
+# Logs & Cache
+# =========================
+/storage/*.key
+/storage/debugbar/
+/storage/logs/
+/storage/framework/cache/
+/storage/framework/sessions/
+/storage/framework/views/
+/storage/framework/testing/
+
+# =========================
+# Node / Frontend
+# =========================
+/node_modules/
+/public/hot
+/public/storage
+/public/build
+/npm-debug.log*
+/yarn-error.log*
+
+# =========================
+# IDE / OS
+# =========================
+.idea/
+.vscode/
+*.swp
+*.swo
+*.DS_Store
+Thumbs.db
+
+# =========================
+# Testing / Coverage
+# =========================
+/coverage/
+.phpunit.cache
+
+# =========================
+# Docker
+# =========================
+.docker/
+docker-compose.override.yml
+
+# =========================
+# Misc
+# =========================
+*.log
+*.bak
+*.tmp

+ 66 - 0
README.md

@@ -0,0 +1,66 @@
+<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
+
+<p align="center">
+<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
+</p>
+
+## About Laravel
+
+Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
+
+- [Simple, fast routing engine](https://laravel.com/docs/routing).
+- [Powerful dependency injection container](https://laravel.com/docs/container).
+- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
+- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
+- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
+- [Robust background job processing](https://laravel.com/docs/queues).
+- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
+
+Laravel is accessible, powerful, and provides tools required for large, robust applications.
+
+## Learning Laravel
+
+Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
+
+You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
+
+If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
+
+## Laravel Sponsors
+
+We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
+
+### Premium Partners
+
+- **[Vehikl](https://vehikl.com/)**
+- **[Tighten Co.](https://tighten.co)**
+- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
+- **[64 Robots](https://64robots.com)**
+- **[Cubet Techno Labs](https://cubettech.com)**
+- **[Cyber-Duck](https://cyber-duck.co.uk)**
+- **[Many](https://www.many.co.uk)**
+- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
+- **[DevSquad](https://devsquad.com)**
+- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
+- **[OP.GG](https://op.gg)**
+- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
+- **[Lendio](https://lendio.com)**
+
+## Contributing
+
+Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
+
+## Code of Conduct
+
+In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
+
+## Security Vulnerabilities
+
+If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
+
+## License
+
+The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

+ 32 - 0
app/Console/Kernel.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Console;
+
+use Illuminate\Console\Scheduling\Schedule;
+use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+
+class Kernel extends ConsoleKernel
+{
+    /**
+     * Define the application's command schedule.
+     *
+     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
+     * @return void
+     */
+    protected function schedule(Schedule $schedule)
+    {
+        // $schedule->command('inspire')->hourly();
+    }
+
+    /**
+     * Register the commands for the application.
+     *
+     * @return void
+     */
+    protected function commands()
+    {
+        $this->load(__DIR__.'/Commands');
+
+        require base_path('routes/console.php');
+    }
+}

+ 50 - 0
app/Exceptions/Handler.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Throwable;
+
+class Handler extends ExceptionHandler
+{
+    /**
+     * A list of exception types with their corresponding custom log levels.
+     *
+     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
+     */
+    protected $levels = [
+        //
+    ];
+
+    /**
+     * A list of the exception types that are not reported.
+     *
+     * @var array<int, class-string<\Throwable>>
+     */
+    protected $dontReport = [
+        //
+    ];
+
+    /**
+     * A list of the inputs that are never flashed to the session on validation exceptions.
+     *
+     * @var array<int, string>
+     */
+    protected $dontFlash = [
+        'current_password',
+        'password',
+        'password_confirmation',
+    ];
+
+    /**
+     * Register the exception handling callbacks for the application.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        $this->reportable(function (Throwable $e) {
+            //
+        });
+    }
+}

+ 440 - 0
app/Helpers/common.php

@@ -0,0 +1,440 @@
+<?php
+
+// 전체 배열의 키를 대문자, 소문자로 변경해서 반환한다.
+function arrayChangeKeyCaseRecursive($arr = [], $case = CASE_LOWER): string
+{
+    return array_map(function ($item) use ($case) {
+        if (is_array($item)){
+            $item = arrayChangeKeyCaseRecursive($item, $case);
+        }
+        return $item;
+    }, array_change_key_case($arr, $case));
+}
+
+// Request Url
+function currentURL(): string
+{
+    $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http");
+    $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
+    $query = (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '');
+    return sprintf("%s://%s%s", $protocol, $host, $query);
+}
+
+// 데이터 단위 변환
+function byteFormat($num, $precision = 1): string
+{
+    if ($num >= 1000000000000) {
+        $num = round($num / 1099511627776, $precision);
+        $unit = 'TB';
+    } elseif ($num >= 1000000000) {
+        $num = round($num / 1073741824, $precision);
+        $unit = 'GB';
+    } elseif ($num >= 1000000) {
+        $num = round($num / 1048576, $precision);
+        $unit = 'MB';
+    } elseif ($num >= 1000) {
+        $num = round($num / 1024, $precision);
+        $unit = 'KB';
+    } else {
+        $unit = 'Bytes';
+        return number_format($num) . ' ' . $unit;
+    }
+    return number_format($num, $precision) . ' ' . $unit;
+}
+
+// 데이터 단위 지정 변환
+function byteToFormat($bytes = 0, $unit = "", $decimals = 0): string
+{
+    $units = ['B' => 0, 'KB' => 1, 'MB' => 2, 'GB' => 3, 'TB' => 4, 'PB' => 5, 'EB' => 6, 'ZB' => 7, 'YB' => 8];
+    $value = 0;
+    if ($bytes > 0) {
+        if (!array_key_exists($unit, $units)) {
+            $pow = floor(log($bytes) / log(1024));
+            $unit = array_search($pow, $units);
+        }
+        $value = ($bytes / pow(1024, floor($units[$unit])));
+    }
+    if (!is_numeric($decimals) || $decimals < 0) {
+        $decimals = 2;
+    }
+    return sprintf('%.' . $decimals . 'f ' . $unit, $value);
+}
+
+// 상태 보기
+function show($varName): string
+{
+    switch ($result = get_cfg_var($varName)) {
+        case 0:
+            return '<span class="text-danger">Off</span>';
+        case 1:
+            return '<span class="text-success">On</span>';
+        default:
+            return $result;
+    }
+}
+
+// 리눅스 시스템 보기 - 파일권한
+function isFun($funName = ""): string
+{
+    if (!$funName || trim($funName) == '' || preg_match('~[^a-z0-9\_]+~i', $funName, $tmp)) {
+        return '알수없음';
+    } else {
+        return (false !== function_exists($funName)) ? '<span class="text-success">사용가능</span>' : '<span class="text-danger">Г—</span>';
+    }
+}
+
+// 리눅스 시스템 보기 - CPU 모델
+function GetCoreInformation(): array
+{
+    $data = @file('/proc/stat');
+    $cores = [];
+
+    if($data) {
+        foreach ($data as $line) {
+            if (preg_match('/^cpu[0-9]/', $line)) {
+                $info = explode(' ', $line);
+                $cores[] = ['user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7]];
+            }
+        }
+    }
+
+    return $cores;
+}
+
+// 리눅스 시스템 보기 CPU 속도
+function GetCpuPercentages($stat1 = [], $stat2 = []): array
+{
+    $cpus = [];
+    if (count($stat1) !== count($stat2)) {
+        return $cpus;
+    }
+
+    for ($i = 0, $l = count($stat1); $i < $l; $i++) {
+        $dif = [];
+        $dif['user'] = ($stat2[$i]['user'] - $stat1[$i]['user']);
+        $dif['nice'] = ($stat2[$i]['nice'] - $stat1[$i]['nice']);
+        $dif['sys'] = ($stat2[$i]['sys'] - $stat1[$i]['sys']);
+        $dif['idle'] = ($stat2[$i]['idle'] - $stat1[$i]['idle']);
+        $dif['iowait'] = ($stat2[$i]['iowait'] - $stat1[$i]['iowait']);
+        $dif['irq'] = ($stat2[$i]['irq'] - $stat1[$i]['irq']);
+        $dif['softirq'] = ($stat2[$i]['softirq'] - $stat1[$i]['softirq']);
+        $total = array_sum($dif);
+        $cpu = [];
+
+        foreach ($dif as $x => $y) {
+            $cpu[$x] = round($y / $total * 100, 2);
+        }
+        $cpus['cpu' . $i] = $cpu;
+    }
+    return $cpus;
+}
+
+// 현재 URL 이 대상 URL 과 같다면 active 상태
+function activeUrl($segment = ''): string
+{
+    return (request()->is($segment) ? 'active' : '');
+}
+
+// 관리자 설정값 조회
+function configs($item = '', $default = ''): string|int|null
+{
+    $config = cache('config-meta');
+
+    if($item) {
+        $ret = (property_exists($config, $item) ? $config->{$item} : $default);
+    }else{
+        $ret = $config;
+    }
+
+    return $ret;
+}
+
+// 게시판 에디터 출력
+function htmlEditor($name = '', $content = '', $className = '', $isDhtmlEditor = true, $emoticonYN = true, $placeholder = "", $rows = 0, $id = ''): string
+{
+    // TINYMCE 에디터 추가
+    $html = "";
+
+    if ($isDhtmlEditor && !defined('LOAD_DHTML_EDITOR_JS'))
+    {
+        // tinymce iframe 허용
+        $whiteIframe = configs('white_iframe');
+        $whiteIframe = preg_replace("/[\r|\n|\r\n]+/", ",", $whiteIframe);
+        $whiteIframe = preg_replace("/\s+/", "", $whiteIframe);
+
+        $html .= PHP_EOL . '<script type="module">';
+
+        // 아이프레임 주소 허용여부 결정
+        $html .= sprintf('var whiteIframe = "%s";', $whiteIframe);
+
+        // 이모티콘 사용여부
+        $html .= sprintf('var useEmoticon = "%s";', ($emoticonYN ? 'Y' : 'N'));
+
+        $html .= '</script>';
+
+        define('LOAD_DHTML_EDITOR_JS', true);
+    }
+
+    $html .= '<textarea';
+    $html .= sprintf(' class="tinymce-editor %s"', $className);
+
+    if($name) {
+        $html .= sprintf(' name="%s"', $name);
+    }
+
+    if($id) {
+        $html .= sprintf(' id="%s"', $id);
+    }
+
+    if($placeholder) {
+        $html .= sprintf(' placeholder="%s"', $placeholder);
+    }
+
+    if ($rows) {
+        $html .= sprintf(' rows="%s"', $rows);
+    }
+
+    $html .= ('>' . htmlspecialchars($content) . '</textarea>');
+
+    return $html;
+}
+
+// 관리자 Form Submit 정보 attributes 와 맵핑
+function mapAdminAttr(array $rules, array $posts): array
+{
+    foreach(array_keys($rules) as $field) {
+        if(array_key_exists($field, $posts)) {
+            continue;
+        }
+        $posts[$field] = 0;
+    }
+    return $posts;
+}
+
+// FreeBSD 값 조회
+function getKey($keyName): bool
+{
+    return doCommand('sysctl', "-n $keyName");
+}
+
+// FreeBSD 시스템 명령 실행
+function doCommand($commandName, $args): bool
+{
+    $buffer = "";
+    if (false === ($command = findCommand($commandName))) {
+        return false;
+    }
+    if ($fp = @popen("$command $args", 'r')) {
+        while (!@feof($fp)) {
+            $buffer .= @fgets($fp, 4096);
+        }
+        return trim($buffer);
+    }
+    return false;
+}
+
+// FreeBSD 명령어 지정 조회
+function findCommand($commandName)
+{
+    $path = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin', '/usr/local/sbin'];
+    foreach ($path as $p) {
+        if (@is_executable("$p/$commandName")) {
+            return "$p/$commandName";
+        }
+    }
+    return false;
+}
+
+// 날짜와 시간에 <br/>, \n 추가
+function dateBr($date = null, $default = ''): string
+{
+    return ($date ? date('Y-m-d <\b\r /> H:i:s', strtotime($date)) : $default);
+}
+
+// Alert 띄우기
+function alert(string $msg, string|null $url = "", bool $redirect = true): Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $msg = preg_replace('/(\r\n|\r|\n)/', '\n', addslashes($msg)); // 줄바꿈 되도록 출력
+    $msg = strip_tags(trim($msg), '<br/>'); // 태그 제거, 공백 제거
+    $msg = preg_replace('/<br[^>]*>/i', '\n\n', $msg); // br 태그를 \n로 개행 alert 에서 다음줄로 하기 위함.
+    if ($redirect) {
+        if ($url) {
+            return redirect($url)->withErrors($msg)->withInput();
+        } else {
+            return back()->withErrors($msg)->withInput();
+        }
+    }else{
+        return response(
+            sprintf('<script>alert("%s");</script>', $msg)
+        , 200, ['Content-Type', 'text/javascript']);
+    }
+}
+
+// 로그인 확인
+function loginCheck(string $callback = null): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "if (confirm(\"로그인 후 이용하실 수 있습니다.\\n로그인 화면으로 이동하시겠습니까?\")) {";
+    $s .= "location.href = \"" . route('login') . "?callback=" . urlencode(url($_SERVER['REQUEST_URI']) ?? $callback) . "\";";
+    $s .= '} else if(document.referrer) { history.go(-1); ';
+    $s .= '} else { window.close(); }';
+    $s .= '</script>';
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+// Alert 후 창 닫음
+function alertClose(string $msg): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "alert(\"$msg\");";
+    $s .= "self.close();";
+    $s .= "window.close();";
+    $s .= "</script>";
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+// 성인인증 알림
+function confirmToAdult(string $callback = null): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "if (confirm(\"해당 상품은 성인인증이 필요합니다.\\n로그인 화면으로 이동하시겠습니까?\")) {";
+    $s .= "location.href = \"" . route('login') . "?callback=" . urlencode(url($_SERVER['REQUEST_URI']) ?? $callback) . "\";";
+    if(DEVICE_TYPE != DEVICE_TYPE_1) { // 모바일
+        $s .= '} else { history.back(); }';
+    }else{ // PC
+        $s .= '} else { window.close(); }';
+    }
+    $s .= "</script>";
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+
+// Send socket
+function getSock(string $url): string
+{
+    // host 와 uri 를 분리
+    $host = "";
+    $get = "";
+
+    if (preg_match("/http:\/\/([a-zA-Z0-9_\-\.]+)([^<]*)/", $url, $res)) {
+        $host = $res[1];
+        $get = $res[2];
+    }
+
+    // 80번 포트로 소캣접속 시도
+    $fp = fsockopen($host, 80, $n, $s, 30);
+    if (empty($fp)) {
+        die($s . ' (' . $n . ")\n");
+    } else {
+        fputs($fp, "GET $get HTTP/1.0\r\n");
+        fputs($fp, "Host: $host\r\n");
+        fputs($fp, "\r\n");
+
+        $header = '';
+        // header 와 content 를 분리한다.
+        while (trim($buffer = fgets($fp, 1024)) !== '') {
+            $header .= $buffer;
+        }
+        while (!feof($fp)) {
+            $buffer .= fgets($fp, 1024);
+        }
+    }
+    fclose($fp);
+
+    // content 만 return 한다.
+    return $buffer;
+}
+
+// 휴대폰 번호 조회
+function getTelNumber($phone, $hyphen = 1): string
+{
+    if ($hyphen) {
+        $preg = "$1-$2-$3";
+    } else {
+        $preg = "$1$2$3";
+    }
+
+    $phone = str_replace('-', '', trim($phone));
+    return preg_replace(
+        "/^(01[016789])([0-9]{3,4})([0-9]{4})$/",
+        $preg,
+        $phone
+    );
+}
+
+// 수행 측정시간
+function getTime(): float
+{
+    $t = explode(' ', microtime());
+    return (float)$t[0] + (float)$t[1];
+}
+
+/**
+ * iframe tag 조회
+ */
+function getIframeTag($string = '', $onlySrc = false)
+{
+    preg_match('/<iframe.*src=\"(.*)\".*><\/iframe>/isU', $string, $matches);
+    if ($onlySrc) {
+        return (isset($matches[1])) ? $matches[1] : ""; // the src part. (http://www.youtube.com/embed/IIYeKGNNNf4?rel=0)
+    } else {
+        return (isset($matches[0])) ? $matches[0] : ""; // only the <iframe ...></iframe> part
+    }
+}
+
+/*
+ * 이미지 인지 여부
+ */
+function isImage($path = ''): int
+{
+    return intval(in_array(getimagesize($path)[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP]));
+}
+
+/*
+ * 문자 검색 후 강조
+ */
+function highlight($search, $str): string
+{
+    return (!is_null($search)) ? preg_replace( '#' . preg_quote($search,'#') . '#iu', '<span style="background: #ff9632;color: #000;">$0</span>', $str) : $str;
+}
+
+/*
+ * 모든 공백 제거
+ */
+function aTrim($str): string
+{
+    return preg_replace('/\s+/', '', $str);
+}
+
+/*
+ * 목록 시작 번호
+ * currentItems: same as limit
+ * currentPage: floor(start / limit)
+ * totalPages: ceil(totalItems / limit)
+ * last: totalPages * limit
+ * previous: (currentPage-1) * limit
+ * next: (currentPage+1) * limit
+ */
+function listNum(int $total, int $page, int $perPage): int
+{
+    return (($listNum = ($total - ($page - 1) * $perPage)) > 0 ? $listNum : $total);
+}
+
+/*
+ * 단말기 구분으로 레이아웃 처리
+ */
+function layout(string $viewPath): string
+{
+    return (DEVICE_TYPE != DEVICE_TYPE_1 ? ('mobile.' . $viewPath) : $viewPath);
+}
+
+/*
+ * 서버 호스트
+ */
+function getServerHost(): string
+{
+    return match (getenv('DEVELOPER_ENV')) {
+        'local' => LOCAL_HOST,
+        'dev' => DEV_HOST
+    };
+}

+ 429 - 0
app/Helpers/common.php~

@@ -0,0 +1,429 @@
+<?php
+
+// 전체 배열의 키를 대문자, 소문자로 변경해서 반환한다.
+function arrayChangeKeyCaseRecursive($arr = [], $case = CASE_LOWER): string
+{
+    return array_map(function ($item) use ($case) {
+        if (is_array($item)){
+            $item = arrayChangeKeyCaseRecursive($item, $case);
+        }
+        return $item;
+    }, array_change_key_case($arr, $case));
+}
+
+// Request Url
+function currentURL(): string
+{
+    $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http");
+    $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
+    $query = (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '');
+    return sprintf("%s://%s%s", $protocol, $host, $query);
+}
+
+// 데이터 단위 변환
+function byteFormat($num, $precision = 1): string
+{
+    if ($num >= 1000000000000) {
+        $num = round($num / 1099511627776, $precision);
+        $unit = 'TB';
+    } elseif ($num >= 1000000000) {
+        $num = round($num / 1073741824, $precision);
+        $unit = 'GB';
+    } elseif ($num >= 1000000) {
+        $num = round($num / 1048576, $precision);
+        $unit = 'MB';
+    } elseif ($num >= 1000) {
+        $num = round($num / 1024, $precision);
+        $unit = 'KB';
+    } else {
+        $unit = 'Bytes';
+        return number_format($num) . ' ' . $unit;
+    }
+    return number_format($num, $precision) . ' ' . $unit;
+}
+
+// 데이터 단위 지정 변환
+function byteToFormat($bytes = 0, $unit = "", $decimals = 0): string
+{
+    $units = ['B' => 0, 'KB' => 1, 'MB' => 2, 'GB' => 3, 'TB' => 4, 'PB' => 5, 'EB' => 6, 'ZB' => 7, 'YB' => 8];
+    $value = 0;
+    if ($bytes > 0) {
+        if (!array_key_exists($unit, $units)) {
+            $pow = floor(log($bytes) / log(1024));
+            $unit = array_search($pow, $units);
+        }
+        $value = ($bytes / pow(1024, floor($units[$unit])));
+    }
+    if (!is_numeric($decimals) || $decimals < 0) {
+        $decimals = 2;
+    }
+    return sprintf('%.' . $decimals . 'f ' . $unit, $value);
+}
+
+// 상태 보기
+function show($varName): string
+{
+    switch ($result = get_cfg_var($varName)) {
+        case 0:
+            return '<span class="text-danger">Off</span>';
+        case 1:
+            return '<span class="text-success">On</span>';
+        default:
+            return $result;
+    }
+}
+
+// 리눅스 시스템 보기 - 파일권한
+function isFun($funName = ""): string
+{
+    if (!$funName || trim($funName) == '' || preg_match('~[^a-z0-9\_]+~i', $funName, $tmp)) {
+        return '알수없음';
+    } else {
+        return (false !== function_exists($funName)) ? '<span class="text-success">사용가능</span>' : '<span class="text-danger">Г—</span>';
+    }
+}
+
+// 리눅스 시스템 보기 - CPU 모델
+function GetCoreInformation(): array
+{
+    $data = @file('/proc/stat');
+    $cores = [];
+
+    if($data) {
+        foreach ($data as $line) {
+            if (preg_match('/^cpu[0-9]/', $line)) {
+                $info = explode(' ', $line);
+                $cores[] = ['user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7]];
+            }
+        }
+    }
+
+    return $cores;
+}
+
+// 리눅스 시스템 보기 CPU 속도
+function GetCpuPercentages($stat1 = [], $stat2 = []): array
+{
+    $cpus = [];
+    if (count($stat1) !== count($stat2)) {
+        return $cpus;
+    }
+
+    for ($i = 0, $l = count($stat1); $i < $l; $i++) {
+        $dif = [];
+        $dif['user'] = ($stat2[$i]['user'] - $stat1[$i]['user']);
+        $dif['nice'] = ($stat2[$i]['nice'] - $stat1[$i]['nice']);
+        $dif['sys'] = ($stat2[$i]['sys'] - $stat1[$i]['sys']);
+        $dif['idle'] = ($stat2[$i]['idle'] - $stat1[$i]['idle']);
+        $dif['iowait'] = ($stat2[$i]['iowait'] - $stat1[$i]['iowait']);
+        $dif['irq'] = ($stat2[$i]['irq'] - $stat1[$i]['irq']);
+        $dif['softirq'] = ($stat2[$i]['softirq'] - $stat1[$i]['softirq']);
+        $total = array_sum($dif);
+        $cpu = [];
+
+        foreach ($dif as $x => $y) {
+            $cpu[$x] = round($y / $total * 100, 2);
+        }
+        $cpus['cpu' . $i] = $cpu;
+    }
+    return $cpus;
+}
+
+// 현재 URL 이 대상 URL 과 같다면 active 상태
+function activeUrl($segment = ''): string
+{
+    return (request()->is($segment) ? 'active' : '');
+}
+
+// 관리자 설정값 조회
+function configs($item = '', $default = ''): string|int|null
+{
+    $config = cache('config-meta');
+
+    if($item) {
+        $ret = (property_exists($config, $item) ? $config->{$item} : $default);
+    }else{
+        $ret = $config;
+    }
+
+    return $ret;
+}
+
+// 게시판 에디터 출력
+function htmlEditor($name = '', $content = '', $className = '', $isDhtmlEditor = true, $emoticonYN = true, $placeholder = "", $rows = 0, $id = ''): string
+{
+    // TINYMCE 에디터 추가
+    $html = "";
+
+    if ($isDhtmlEditor && !defined('LOAD_DHTML_EDITOR_JS'))
+    {
+        // tinymce iframe 허용
+        $whiteIframe = configs('white_iframe');
+        $whiteIframe = preg_replace("/[\r|\n|\r\n]+/", ",", $whiteIframe);
+        $whiteIframe = preg_replace("/\s+/", "", $whiteIframe);
+
+        $html .= PHP_EOL . '<script type="module">';
+
+        // 아이프레임 주소 허용여부 결정
+        $html .= sprintf('var whiteIframe = "%s";', $whiteIframe);
+
+        // 이모티콘 사용여부
+        $html .= sprintf('var useEmoticon = "%s";', ($emoticonYN ? 'Y' : 'N'));
+
+        $html .= '</script>';
+
+        define('LOAD_DHTML_EDITOR_JS', true);
+    }
+
+    $html .= '<textarea';
+    $html .= sprintf(' class="tinymce-editor %s"', $className);
+
+    if($name) {
+        $html .= sprintf(' name="%s"', $name);
+    }
+
+    if($id) {
+        $html .= sprintf(' id="%s"', $id);
+    }
+
+    if($placeholder) {
+        $html .= sprintf(' placeholder="%s"', $placeholder);
+    }
+
+    if ($rows) {
+        $html .= sprintf(' rows="%s"', $rows);
+    }
+
+    $html .= ('>' . htmlspecialchars($content) . '</textarea>');
+
+    return $html;
+}
+
+// 관리자 Form Submit 정보 attributes 와 맵핑
+function mapAdminAttr(array $rules, array $posts): array
+{
+    foreach(array_keys($rules) as $field) {
+        if(array_key_exists($field, $posts)) {
+            continue;
+        }
+        $posts[$field] = 0;
+    }
+    return $posts;
+}
+
+// FreeBSD 값 조회
+function getKey($keyName): bool
+{
+    return doCommand('sysctl', "-n $keyName");
+}
+
+// FreeBSD 시스템 명령 실행
+function doCommand($commandName, $args): bool
+{
+    $buffer = "";
+    if (false === ($command = findCommand($commandName))) {
+        return false;
+    }
+    if ($fp = @popen("$command $args", 'r')) {
+        while (!@feof($fp)) {
+            $buffer .= @fgets($fp, 4096);
+        }
+        return trim($buffer);
+    }
+    return false;
+}
+
+// FreeBSD 명령어 지정 조회
+function findCommand($commandName)
+{
+    $path = ['/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin', '/usr/local/sbin'];
+    foreach ($path as $p) {
+        if (@is_executable("$p/$commandName")) {
+            return "$p/$commandName";
+        }
+    }
+    return false;
+}
+
+// 날짜와 시간에 <br/>, \n 추가
+function dateBr($date = null, $default = ''): string
+{
+    return ($date ? date('Y-m-d <\b\r /> H:i:s', strtotime($date)) : $default);
+}
+
+// Alert 띄우기
+function alert(string $msg, string|null $url = "", bool $redirect = true): Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse|Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $msg = preg_replace('/(\r\n|\r|\n)/', '\n', addslashes($msg)); // 줄바꿈 되도록 출력
+    $msg = strip_tags(trim($msg), '<br/>'); // 태그 제거, 공백 제거
+    $msg = preg_replace('/<br[^>]*>/i', '\n\n', $msg); // br 태그를 \n로 개행 alert 에서 다음줄로 하기 위함.
+    if ($redirect) {
+        if ($url) {
+            return redirect($url)->withErrors($msg)->withInput();
+        } else {
+            return back()->withErrors($msg)->withInput();
+        }
+    }else{
+        return response(
+            sprintf('<script>alert("%s");</script>', $msg)
+        , 200, ['Content-Type', 'text/javascript']);
+    }
+}
+
+// 로그인 확인
+function loginCheck(string $callback = null): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "if (confirm(\"로그인 후 이용하실 수 있습니다.\\n로그인 화면으로 이동하시겠습니까?\")) {";
+    $s .= "location.href = \"" . route('login') . "?callback=" . urlencode(url($_SERVER['REQUEST_URI']) ?? $callback) . "\";";
+    $s .= '} else if(document.referrer) { history.go(-1); console.log(2); ';
+    $s .= '} else { window.close(); console.log(1); }';
+    $s .= '</script>';
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+// Alert 후 창 닫음
+function alertClose(string $msg): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "alert(\"$msg\");";
+    $s .= "self.close();";
+    $s .= "window.close();";
+    $s .= "</script>";
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+// 성인인증 알림
+function confirmToAdult(string $callback = null): Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+{
+    $s = "<script>";
+    $s .= "if (confirm(\"해당 상품은 성인인증이 필요합니다.\\n로그인 화면으로 이동하시겠습니까?\")) {";
+    $s .= "location.href = \"" . route('login') . "?callback=" . urlencode(url($_SERVER['REQUEST_URI']) ?? $callback) . "\";";
+    if(DEVICE_TYPE != DEVICE_TYPE_1) { // 모바일
+        $s .= '} else { history.back(); }';
+    }else{ // PC
+        $s .= '} else { window.close(); }';
+    }
+    $s .= "</script>";
+    return response($s, 200, ['Content-Type', 'text/javascript']);
+}
+
+
+// Send socket
+function getSock(string $url): string
+{
+    // host 와 uri 를 분리
+    $host = "";
+    $get = "";
+
+    if (preg_match("/http:\/\/([a-zA-Z0-9_\-\.]+)([^<]*)/", $url, $res)) {
+        $host = $res[1];
+        $get = $res[2];
+    }
+
+    // 80번 포트로 소캣접속 시도
+    $fp = fsockopen($host, 80, $n, $s, 30);
+    if (empty($fp)) {
+        die($s . ' (' . $n . ")\n");
+    } else {
+        fputs($fp, "GET $get HTTP/1.0\r\n");
+        fputs($fp, "Host: $host\r\n");
+        fputs($fp, "\r\n");
+
+        $header = '';
+        // header 와 content 를 분리한다.
+        while (trim($buffer = fgets($fp, 1024)) !== '') {
+            $header .= $buffer;
+        }
+        while (!feof($fp)) {
+            $buffer .= fgets($fp, 1024);
+        }
+    }
+    fclose($fp);
+
+    // content 만 return 한다.
+    return $buffer;
+}
+
+// 휴대폰 번호 조회
+function getTelNumber($phone, $hyphen = 1): string
+{
+    if ($hyphen) {
+        $preg = "$1-$2-$3";
+    } else {
+        $preg = "$1$2$3";
+    }
+
+    $phone = str_replace('-', '', trim($phone));
+    return preg_replace(
+        "/^(01[016789])([0-9]{3,4})([0-9]{4})$/",
+        $preg,
+        $phone
+    );
+}
+
+// 수행 측정시간
+function getTime(): float
+{
+    $t = explode(' ', microtime());
+    return (float)$t[0] + (float)$t[1];
+}
+
+/**
+ * iframe tag 조회
+ */
+function getIframeTag($string = '', $onlySrc = false)
+{
+    preg_match('/<iframe.*src=\"(.*)\".*><\/iframe>/isU', $string, $matches);
+    if ($onlySrc) {
+        return (isset($matches[1])) ? $matches[1] : ""; // the src part. (http://www.youtube.com/embed/IIYeKGNNNf4?rel=0)
+    } else {
+        return (isset($matches[0])) ? $matches[0] : ""; // only the <iframe ...></iframe> part
+    }
+}
+
+/*
+ * 이미지 인지 여부
+ */
+function isImage($path = ''): int
+{
+    return intval(in_array(getimagesize($path)[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP]));
+}
+
+/*
+ * 문자 검색 후 강조
+ */
+function highlight($search, $str): string
+{
+    return (!is_null($search)) ? preg_replace( '#' . preg_quote($search,'#') . '#iu', '<span style="background: #ff9632;color: #000;">$0</span>', $str) : $str;
+}
+
+/*
+ * 모든 공백 제거
+ */
+function aTrim($str): string
+{
+    return preg_replace('/\s+/', '', $str);
+}
+
+/*
+ * 목록 시작 번호
+ * currentItems: same as limit
+ * currentPage: floor(start / limit)
+ * totalPages: ceil(totalItems / limit)
+ * last: totalPages * limit
+ * previous: (currentPage-1) * limit
+ * next: (currentPage+1) * limit
+ */
+function listNum(int $total, int $page, int $perPage): int
+{
+    return (($listNum = ($total - ($page - 1) * $perPage)) > 0 ? $listNum : $total);
+}
+
+/*
+ * 단말기 구분으로 레이아웃 처리
+ */
+function layout(string $viewPath): string
+{
+    return (DEVICE_TYPE != DEVICE_TYPE_1 ? ('mobile.' . $viewPath) : $viewPath);
+}

+ 183 - 0
app/Helpers/string.php

@@ -0,0 +1,183 @@
+<?php
+/**
+ * User: 김국현
+ * Date: 2021-04-04
+ * Time: 오후 11:01
+ */
+
+// 문자열이 한글, 영문, 숫자, 특수문자로 구성되어 있는지 검사
+function findRegexString(string $str, int $options): bool
+{
+    if (empty($str)) {
+        return false;
+    }
+
+    $s = '';
+    for ($i = 0; $i < strlen($str); $i++) {
+        $c = $str[$i];
+        $oc = ord($c);
+
+        // 한글
+        if ($oc >= 0xA0 && $oc <= 0xFF) {
+            if (strtoupper(env('APP_CHARSET', 'UTF-8')) === 'UTF-8') {
+                if ($options & _HANGUL_) {
+                    $s .= $c . $str[$i + 1] . $str[$i + 2];
+                }
+                $i += 2;
+            } else {
+                // 한글은 2바이트 이므로 문자하나를 건너뜀
+                $i++;
+                if ($options & _HANGUL_) {
+                    $s .= $c . $str[$i];
+                }
+            }
+        } elseif ($oc >= 0x30 && $oc <= 0x39) { // 숫자
+            if ($options & _NUMERIC_) {
+                $s .= $c;
+            }
+        } elseif ($oc >= 0x41 && $oc <= 0x5A) { // 영대문자
+            if (($options & _ALPHABETIC_) OR ($options & _ALPHAUPPER_)) {
+                $s .= $c;
+            }
+        } elseif ($oc >= 0x61 && $oc <= 0x7A) { // 영소문자
+            if (($options & _ALPHABETIC_) OR ($options & _ALPHALOWER_)) {
+                $s .= $c;
+            }
+        } elseif ($oc >= 0x20) { // 공백
+            if ($options & _SPACE_) {
+                $s .= $c;
+            }
+        } elseif ($oc >= 0x5F) { // `_` 언더바
+
+            if ($options & _UNDER_) {
+                $s .= $c;
+            }
+        } else {
+            if ($options & _SPECIAL_) {
+                $s .= $c;
+            }
+        }
+    }
+    // 넘어온 값과 비교하여 같으면 참, 틀리면 거짓
+    return ($str === $s);
+}
+
+// 문자열에 정규식을 적용하여 배열로 만든다.
+function countOccurrences(string $str, string $exp): int
+{
+    preg_match_all($exp, $str, $match);
+    return count($match[0]);
+}
+
+// 문자열에 소문자 개수가 몇개인지
+function countLowercase(string $str): int
+{
+    return countOccurrences($str, '/[a-z]/');
+}
+
+// 문자열에 대문자 개수가 몇개인지
+function countUppercase(string $str): int
+{
+    return countOccurrences($str, '/[A-Z]/');
+}
+
+// 문자열에 숫자 개수가 몇개인지
+function countNumbers(string $str): int
+{
+    return countOccurrences($str, '/[0-9]/');
+}
+
+// 문자열에 특수문자 개수가 몇개인지
+function countSpecialChars(string $str): int
+{
+    return countOccurrences($str, '/[!@#$%^&*()]/');
+}
+
+// 원하는 문자열을 원하는 길에 맞는지 확인해서 조정하는 기능을 합니다.
+function cutChar(string $word, int $cut): string
+{
+    $word = substr($word, 0, $cut); // 필요한 길이만큼 취함.
+    for ($k = $cut - 1; $k > 1; $k--) {
+        if (ord(substr($word, $k, 1)) < 128) {
+            break; // 한글값은 160 이상.
+        }
+    }
+    return substr($word, 0, $cut - ($cut - $k + 1) % 2);
+}
+
+// UTF-8 자르기
+function cutCharUtf8(string $word, int $cut, string $mask = '...'): string
+{
+    if($cut <= 0) {
+        return $word;
+    }
+
+    preg_match_all('/[\xE0-\xFF][\x80-\xFF]{2}|./', $word, $match); // target for BMP
+
+    $m = $match[0];
+    $len = strlen($word); // length of source string
+    if ($len <= $cut) {
+        return $word;
+    }
+
+    $ret = [];
+    $count = 0;
+    for ($i = 0; $i < $cut; $i++) {
+        $count += (strlen($m[$i]) > 1) ? 2 : 1;
+        if ($count > $cut) {
+            break;
+        }
+        $ret[] = $m[$i];
+    }
+
+    return (join('', $ret) . $mask);
+}
+
+// base encode 확인
+function isBase64($s = ""): bool
+{
+    return (bool)preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $s);
+}
+
+// 절대 경로 삭제 (../../../)
+function canonicalizePath($path): string
+{
+    $path = array_filter(explode(DIRECTORY_SEPARATOR, $path));
+    $stack = [];
+    foreach ($path as $seg) {
+        if ($seg == '..') {
+            array_pop($stack);
+            continue;
+        }
+
+        if ($seg == '.') {
+            continue;
+        }
+
+        $stack[] = $seg;
+    }
+
+    return (DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $stack));
+}
+
+// alert 에서 php 개행이 되도록 예외처리 한다.
+function nl2nr(string $s): string
+{
+    $s = preg_replace('/(\r\n|\r|\n)/', '\n', addslashes($s)); // 줄바꿈 되도록 출력
+    $s = strip_tags(trim($s), '<br/>'); // 태그 제거, 공백 제거
+    return preg_replace('/<br[^>]*>/i', '\n\n', $s); // br 태그를 \n로 개행 alert 에서 다음줄로 하기 위함.
+}
+
+// 휴대전화번호에 하이픈(-) 넣기
+function addHyphen($tel): string
+{
+    $tel = preg_replace("/[^0-9]/", "", $tel);    // 숫자 이외 제거
+    if (substr($tel, 0, 2) == '02') {
+        return preg_replace("/([0-9]{2})([0-9]{3,4})([0-9]{4})$/", "\\1-\\2-\\3", $tel);
+    } else if (strlen($tel) == '8' && (substr($tel, 0, 2) == '15' || substr($tel, 0, 2) == '16' || substr($tel, 0, 2) == '18')) {
+        // 지능망 번호이면
+        return preg_replace("/([0-9]{4})([0-9]{4})$/", "\\1-\\2", $tel);
+    } else {
+        return preg_replace("/([0-9]{3})([0-9]{3,4})([0-9]{4})$/", "\\1-\\2-\\3", $tel);
+    }
+}

+ 50 - 0
app/Http/Controllers/Account/CertifyController.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+
+class CertifyController extends Controller
+{
+    use CommonTrait;
+
+    public function __construct()
+    {
+        $this->middleware(['front', 'auth']);
+    }
+
+    /**
+     * 비밀번호 확인
+     * @method GET
+     * @see /account/certify
+     */
+    public function index(Request $request)
+    {
+        $request->session()->reflash();
+        $callbackURL = $request->session()->get('url.intended', session('url'));
+
+        return view(layout('account.certify'), [
+            'callbackURL' => $callbackURL,
+            'menuID' => 'CERTIFY'
+        ]);
+    }
+
+    /**
+     * 비밀번호 인증
+     * @method POST
+     * @see /account/certify
+     */
+    public function update(Request $request)
+    {
+        $certified = $this->passwordAuthed($request->post('password'));
+        if ($certified) {
+            $request->session()->flash('is-certified', 1);
+            $callbackUrl = urldecode($request->post('callback_url'));
+            return redirect($callbackUrl);
+        } else {
+            return back()->withErrors(['password' => '비밀번호가 일치하지 않습니다.'])->withInput();
+        }
+    }
+}

+ 84 - 0
app/Http/Controllers/Account/CommentController.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Services\UserService;
+use App\Services\CommentService;
+use App\Models\DTO\SearchData;
+use App\Models\DTO\ResponseData;
+use Exception;
+
+class CommentController extends Controller
+{
+    use CommonTrait;
+
+    private UserService $userService;
+    private CommentService $commentService;
+
+    public function __construct(
+        UserService $userService, CommentService $commentService
+    ) {
+        $this->middleware(['front', 'auth']);
+
+        $this->userService = $userService;
+        $this->commentService = $commentService;
+    }
+
+    /**
+     * 작성 댓글
+     * @method GET|POST
+	 * @see /account/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->path = route('account.comment');
+
+        // 내가 작성한 게시글
+        $comments = $this->commentService->userList(UID, $params);
+
+        return view(layout('account.comment'), [
+            'comments' => $comments,
+            'params' => $params,
+            'menuID' => 'COMMENT'
+        ]);
+    }
+
+    /**
+     * 댓글 삭제
+     * @method DELETE
+     * @see /account/comment/delete
+     */
+    public function delete(Request $request, ResponseData $response): ResponseData
+    {
+        try {
+
+            $posts = $request->validate([
+                'keys.*' => 'required|numeric|exists:tb_comment,id',
+            ]);
+
+            if($posts['keys']) {
+
+                $user = $request->user();
+                foreach($posts['keys'] as $key) {
+                    $comment = $this->commentService->commentModel->findOrNew($key);
+
+                    if(!$comment->exists) {
+                        continue;
+                    }
+
+                    if(!$comment->remove($comment, $user)) {
+                        throw new Exception($key . '번 댓글은 삭제할 수 없습니다.');
+                    }
+                }
+            }
+        }catch(Exception $e) {
+            $response = $response::fromException($e);
+        }
+
+        return $response;
+    }
+}

+ 137 - 0
app/Http/Controllers/Account/EmailController.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Mail;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\UserEmailLog;
+use App\Models\DTO\ResponseData;
+use App\Mail\VerifyCode;
+use Exception;
+
+class EmailController extends Controller
+{
+    private User $userModel;
+    private UserEmailLog $userEmailLogModel;
+
+    public function __construct(User $user, UserEmailLog $userEmailLogModel)
+    {
+        $this->middleware('auth');
+        $this->middleware('throttle:verify')->only('index');
+
+        $this->userModel = $user;
+        $this->userEmailLogModel = $userEmailLogModel;
+    }
+
+    /**
+     * 회원 이메일 수정
+     * @method GET
+     * @see /account/email
+     */
+    public function index(Request $request, ResponseData $response): ResponseData
+    {
+        try{
+
+            $rules = [
+                'email' => 'required|email|unique:users,email'
+            ];
+
+            $attributes = [
+                'email' => '이메일'
+            ];
+
+            $messages = [
+                'email.required' => '이메일을 입력해 주세요.',
+                'email.email' => '이메일 형식이 옳지 않습니다.',
+                'email.unique' => '이미 사용 중인 이메일 입니다.'
+            ];
+
+            $posts = $this->validate($request, $rules, $messages, $attributes);
+
+            if($this->userEmailLogModel->isUpdateAble(UID)) {
+                throw new Exception(
+                    sprintf('이메일 변경은 %d일 지난 후 가능합니다.', $this->userEmailLogModel->getDayLeft(UID))
+                );
+            }
+
+            // 이메일 인증번호/남은시간 생성
+            $verifyCode = rand(100000, 999999);
+            $verifyExpiresAt = now()->addMinutes(VERIFY_EXPIRES_AT)->diffInSeconds(now());
+
+            // 이메일 인증번호 발송
+            [$name] = explode('@', $posts['email']);
+
+            $sender = (object)[
+                'name' => $name,
+                'email' => $posts['email'],
+            ];
+            Mail::to($sender)->send(new VerifyCode($verifyCode, $posts['email']));
+
+            // 쿠키 생성
+            $sender->verifyCode = $verifyCode;
+            $sender = serialize($sender);
+            $cookie = cookie('tmpVerifyData', $sender, VERIFY_EXPIRES_AT);
+            $response->cookie($cookie);
+
+            // 이메일 인증 화면
+            $response->view = view(layout('account.email'), [
+                'user' => $request->user(),
+                'menuID' => 'MODIFY',
+                'verifyExpiresAt' => $verifyExpiresAt,
+            ])->render();
+
+        }catch(Exception $e) {
+            $response = $response::fromException($e);
+        }
+
+        return $response;
+    }
+
+    /**
+     * 회원 정보 수정 처리
+     * @method POST
+     * @see /account/email/update
+     */
+    public function update(Request $request, ResponseData $response): ResponseData
+    {
+        try{
+
+            $rules = [
+                'code' => 'required|integer'
+            ];
+
+            $attributes = [
+                'code' => '인증번호'
+            ];
+
+            $messages = [
+                'code.required' => '인증번호를 입력해 주세요.',
+                'code.integer' => '인증번호 형식이 옳지 않습니다.'
+            ];
+
+            $posts = $this->validate($request, $rules, $messages, $attributes);
+            $user = $request->user();
+            $tmpVerifyData = unserialize($request->cookie('tmpVerifyData'));
+
+            if($posts['code'] != $tmpVerifyData->verifyCode) {
+                throw new Exception('인증번호가 일치하지 않습니다.');
+            }
+
+            $this->userModel->info()->generateEmailVerified(
+                $tmpVerifyData->email
+            );
+
+            // 이메일 변경 확인
+            if($user->email != $tmpVerifyData->email) {
+                $this->userEmailLogModel->setLog($user, $tmpVerifyData->email);
+            }
+
+        }catch(Exception $e) {
+            $response = $response::fromException($e);
+        }
+
+        return $response;
+    }
+}

+ 68 - 0
app/Http/Controllers/Account/LeaveController.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Models\User;
+
+class LeaveController extends Controller
+{
+    use CommonTrait;
+
+    private User $userModel;
+
+    public function __construct(User $user)
+    {
+        $this->middleware(['front', 'auth']);
+        $this->userModel = $user;
+    }
+
+    /**
+     * 회원탈퇴
+     * @method GET
+     * @see /account/leave
+     */
+    public function index()
+    {
+        return view(layout('account.leave'), [
+            'user' => $this->userModel->info(),
+            'menuID' => 'LEAVE'
+        ]);
+    }
+
+    /**
+     * 회원 탈퇴 처리
+     * @method POST
+     * @see /account/leave/update
+     */
+    public function update(Request $request)
+    {
+        $rules = [
+            'password' => 'required|confirmed',
+            'withdrawal' => 'required|in:0,1'
+        ];
+
+        $attributes = [
+            'password' => '비밀번호',
+            'password_confirmation' => '비밀번호 확인',
+            'withdrawal' => '안내 동의'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        // 비밀번호 확인
+        $certified = $this->passwordAuthed($posts['password']);
+        if(!$certified) {
+            return back()->withErrors('비밀번호가 일치하지 않습니다.');
+        }
+
+        $user = $request->user();
+        $user->is_withdraw = 1;
+        $user->deleted_at = now();
+        $user->save();
+
+        return redirect()->route('logout')->with('message', '회원탈퇴가 완료되었습니다.');
+    }
+}

+ 37 - 0
app/Http/Controllers/Account/LoginLogController.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Services\UserService;
+use App\Models\DTO\SearchData;
+
+class LoginLogController extends Controller
+{
+    private UserService $userService;
+
+    public function __construct(UserService $userService)
+    {
+        $this->middleware(['front', 'auth']);
+        $this->userService = $userService;
+    }
+
+    /**
+     * 로그인 기록
+     * @method GET|POST
+     * @see /account/logingLog
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->startDate = ($request->post('start_date') ?? date('Y-m-d', strtotime('-7 day')));
+        $params->endDate = ($request->post('end_date') ?? now()->format('Y-m-d'));
+
+        return view(layout('account.loginLog'), [
+            'loginLog' => $this->userService->loginLog(UID, $params),
+            'params' => $params,
+            'menuID' => 'LOGINLOG'
+        ]);
+    }
+}

+ 152 - 0
app/Http/Controllers/Account/ModifyController.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Validation\Rule;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Models\User;
+use App\Models\UserNameLog;
+use App\Models\UserEmailLog;
+use App\Models\FileLib;
+use App\Rules\AllowNickname;
+
+class ModifyController extends Controller
+{
+    use CommonTrait;
+
+    private User $userModel;
+    private UserNameLog $userNameLogModel;
+    private UserEmailLog $userEmailLogModel;
+
+    public function __construct(
+        User $user,
+        UserNameLog $UserNameLogModel,
+        UserEmailLog $userEmailLogModel
+    ) {
+        $this->middleware(['front', 'auth']);
+        $this->userModel = $user;
+        $this->userNameLogModel = $UserNameLogModel;
+        $this->userEmailLogModel = $userEmailLogModel;
+    }
+
+    /**
+     * 회원 정보 수정
+     * @method GET
+     * @see /account/modify
+     */
+    public function index(Request $request)
+    {
+        $this->isCertified($request);
+        $request->session()->reflash();
+
+        $changeNicknameDayLeft = $this->userNameLogModel->getDayLeft(UID);
+        $changeEmailDayLeft = $this->userEmailLogModel->getDayLeft(UID);
+        $userThumbWidth = config('user_thumb_width', THUMB_MAX_WIDTH);
+        $userThumbHeight = config('user_thumb_height', THUMB_MAX_HEIGHT);
+        $useUserThumb = config('use_user_thumb', 0);
+
+        return view(layout('account.modify'), [
+            'user' => $request->user(),
+            'changeNicknameDayLeft' => $changeNicknameDayLeft,
+            'changeEmailDayLeft' => $changeEmailDayLeft,
+            'userThumbWidth' => $userThumbWidth,
+            'userThumbHeight' => $userThumbHeight,
+            'useUserThumb' => $useUserThumb,
+            'menuID' => 'MODIFY'
+        ]);
+    }
+
+    /**
+     * 회원 정보 수정 처리
+     * @method POST
+     * @see /account/modify
+     */
+    public function update(Request $request, FileLib $fileLib)
+    {
+        $request->session()->reflash();
+
+        $rules = [
+            'nickname' => ['required', 'string', 'min:2', 'max:20',
+                new AllowNickname, Rule::unique('users', 'nickname')->ignore(UID, 'id')],
+            'thumb_img' => 'nullable|mimes:jpg,jpeg,gif,png|max:3192',
+            'today_message' => ['nullable', 'string', 'max:40'],
+            'about_me' => ['nullable', 'string', 'max:500'],
+            'is_open_profile' => 'nullable|numeric|in:0,1',
+            'receive_email' => 'nullable|numeric|in:0,1',
+            'receive_sms' => 'nullable|numeric|in:0,1'
+        ];
+
+        $attributes = [
+            'nickname' => '닉네임',
+            'thumb_img' => '프로필 이미지',
+            'today_message' => '오늘의 한마디',
+            'about_me' => '자기소개',
+            'is_open_profile' => '정보 공개 여부',
+            'receive_email' => '수신여부 - 이메일(Email)',
+            'receive_sms' => '수신여부 - 이메일(Sms)'
+        ];
+
+        $messages = [
+            'profile.max' => '첨부 가능한 이미지의 최대 용량은 3MB 입니다.'
+        ];
+
+        $posts = $this->validate($request, $rules, $messages, $attributes);
+        $user = $request->user();
+
+        if(!$this->userNameLogModel->isUpdateAble($user->id)) {
+            $posts['nickname'] = $user->nickname;
+        }
+        if(!$request->has('is_open_profile')) {
+            $posts['is_open_profile'] = 0;
+        }
+        if(!$request->has('receive_email')) {
+            $posts['receive_email'] = 0;
+        }
+        if(!$request->has('receive_sms')) {
+            $posts['receive_sms'] = 0;
+        }
+
+        $saveData = [
+            'nickname' => $posts['nickname'],
+            'today_message' => $posts['today_message'],
+            'about_me' => $posts['about_me'],
+            'is_open_profile' => $posts['is_open_profile'],
+            'receive_email' => $posts['receive_email'],
+            'receive_sms' => $posts['receive_sms']
+        ];
+
+        // 파일 삭제
+        if($request->get('thumb_img_del'))
+        {
+            $thumbPath = public_path($request->user()->thumb);
+            if(file_exists($thumbPath)) {
+                if(unlink($thumbPath)) {
+                    $saveData['thumb'] = null;
+                }
+            }
+
+        }else if($request->hasFile('thumb_img')) {
+
+            // 파일 저장
+            $thumb = $request->file('thumb_img');
+            $savePath = $thumb->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_USER_THUMB . DIRECTORY_SEPARATOR . $user->id);
+            $storagePath = Storage::path($savePath);
+            $saveData['thumb'] = Storage::url($savePath);
+
+            // 이미지 크기 조정
+            $fileLib->resize($storagePath, THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT);
+        }
+
+        // 이름 변경 확인
+        if($user->nickname != $posts['nickname']) {
+            $this->userNameLogModel->setLog($user, $posts['nickname']);
+        }
+
+        $this->userModel->updater(UID, $saveData);
+
+        return redirect()->route('account.profile')->withErrors('회원 정보가 수정되었습니다.');
+    }
+}

+ 82 - 0
app/Http/Controllers/Account/PasswordCampaignController.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Rules\NumberLength;
+use App\Rules\SpecialCharLength;
+use App\Rules\UppercaseLength;
+use App\Models\User;
+
+class PasswordCampaignController extends Controller
+{
+    use CommonTrait;
+
+    private User $userModel;
+
+    public function __construct(User $user)
+    {
+        $this->middleware(['front', 'auth']);
+        $this->userModel = $user;
+    }
+
+    /**
+     * 정기 비밀번호 변경 안내
+     * @method GET
+     * @see /account/password/campaign
+     */
+    public function index()
+    {
+        return view(layout('account.passwordCampaign'), [
+            'changePasswordDay' => config('change_password_day', 0),
+            'menuID' => 'PASSWORD_CAMPAIGN'
+        ]);
+    }
+
+    /**
+     * 정기 비밀번호 변경 안내
+     * @method POST
+     * @see /account/password/campaign
+     */
+    public function update(Request $request)
+    {
+        $rules = [
+            'password' => 'required|string',
+            'new_password' => [
+                'required',
+                'string',
+                'confirmed',
+                new NumberLength,
+                new SpecialCharLength,
+                new UppercaseLength
+            ],
+        ];
+
+        $attributes = [
+            'password' => '현재 비밀번호',
+            'new_password' => '새 비밀번호',
+            'new_password_confirmation' => '새 비밀번호 확인'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $certified = $this->passwordAuthed($posts['password']);
+        if(!$certified) {
+            return back()->withErrors('현재 비밀번호가 일치하지 않습니다.');
+        }
+
+        // 동일 비밀번호 여부 확인
+        if ($posts['new_password'] == $posts['password']) {
+            return back()->withErrors('이전 비밀번호는 사용할 수 없습니다.');
+        }
+
+        $this->userModel->updater(UID, [
+            'password' => $this->passwordMake($posts['new_password']),
+            'password_updated_at' => now()
+        ]);
+
+        return redirect()->route('account.profile')->withErrors('비밀번호가 변경되었습니다.');
+    }
+}

+ 78 - 0
app/Http/Controllers/Account/PasswordChangeController.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Rules\NumberLength;
+use App\Rules\SpecialCharLength;
+use App\Rules\UppercaseLength;
+use App\Models\User;
+
+class PasswordChangeController extends Controller
+{
+    use CommonTrait;
+
+    private User $userModel;
+
+    public function __construct(User $user)
+    {
+        $this->middleware(['front', 'auth']);
+        $this->userModel = $user;
+    }
+
+    /**
+     * 비밀번호 변경
+     * @method GET
+     * @see /account/password
+     */
+    public function index(Request $request)
+    {
+        $this->isCertified($request);
+        $request->session()->reflash();
+
+        return view(layout('account.passwordChange'), [
+            'menuID' => 'PASSWORD'
+        ]);
+    }
+
+    /**
+     * 비밀번호 변경 처리
+     * @method POST
+     * @see /account/password/change
+     */
+    public function update(Request $request)
+    {
+        $request->session()->reflash();
+
+        $rules = [
+            'password' => [
+                'required',
+                'confirmed',
+                new NumberLength,
+                new SpecialCharLength,
+                new UppercaseLength
+            ],
+        ];
+
+        $attributes = [
+            'new_password' => '새 비밀번호',
+            'new_password_confirmation' => '새 비밀번호 확인'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        // 동일 비밀번호 여부 확인
+        if ($this->passwordAuthed($posts['password'])) {
+            return back()->withErrors('이전 비밀번호는 사용할 수 없습니다.');
+        }
+
+        $this->userModel->updater(UID, [
+            'password' => bcrypt($posts['password']),
+            'password_updated_at' => now()
+        ]);
+
+        return redirect()->route('account.profile')->withErrors('비밀번호가 변경되었습니다.');
+    }
+}

+ 87 - 0
app/Http/Controllers/Account/PostController.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Services\UserService;
+use App\Services\BoardService;
+use App\Services\PostService;
+use App\Models\DTO\SearchData;
+use App\Models\DTO\ResponseData;
+use Exception;
+
+class PostController extends Controller
+{
+    use CommonTrait;
+
+    private UserService $userService;
+    private BoardService $boardService;
+    private PostService $postService;
+
+    public function __construct(
+        UserService $userService, BoardService $boardService, PostService $postService
+    ) {
+        $this->middleware(['front', 'auth']);
+
+        $this->userService = $userService;
+        $this->boardService = $boardService;
+        $this->postService = $postService;
+    }
+
+    /**
+     * 작성 게시글
+     * @method GET|POST
+	 * @see /account/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->path = route('account.post');
+
+        // 내가 작성한 게시글
+        $posts = $this->boardService->userPosts(UID, $params);
+
+        return view(layout('account.post'), [
+            'posts' => $posts,
+            'params' => $params,
+            'menuID' => 'POST'
+        ]);
+    }
+
+    /**
+     * 게시글 삭제
+     * @method DELETE
+     * @see /account/post/delete
+     */
+    public function delete(Request $request, ResponseData $response): ResponseData
+    {
+        try {
+
+            $posts = $request->validate([
+                'keys.*' => 'required|numeric|exists:tb_post,id',
+            ]);
+
+            if($posts['keys']) {
+
+                $user = $request->user();
+                foreach($posts['keys'] as $key) {
+                    $post = $this->postService->postModel->findOrNew($key);
+
+                    if(!$post->exists) {
+                        continue;
+                    }
+
+                    if(!$post->remove($post, $user)) {
+                        throw new Exception($key . '번 게시글은 삭제할 수 없습니다.');
+                    }
+                }
+            }
+        }catch(Exception $e) {
+            $response = $response::fromException($e);
+        }
+
+        return $response;
+    }
+}

+ 32 - 0
app/Http/Controllers/Account/ProfileController.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Controllers\Account;
+
+use App\Http\Controllers\Controller;
+use App\Models\User;
+
+class ProfileController extends Controller
+{
+    private User $userModel;
+
+    public function __construct(User $userModel)
+    {
+        $this->middleware(['front', 'auth']);
+        $this->userModel = $userModel;
+    }
+
+    /**
+     * 내 정보
+     * @method GET
+     * @see /account
+     */
+    public function index()
+    {
+        $member = $this->userModel->info();
+
+        return view(layout('account.profile'), [
+            'member' => $member,
+            'menuID' => 'PROFILE'
+        ]);
+    }
+}

+ 68 - 0
app/Http/Controllers/Admin/AjaxController.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+use App\Http\Controllers\Controller;
+use App\Models\PostBlame;
+use App\Models\CommentBlame;
+
+class AjaxController extends Controller
+{
+    /**
+     * 게시판 신고처리 결과 등록
+     */
+    public function postBlameResultUpdate(Request $request)
+    {
+        $rules = [
+            'post_blame_id' => 'required|exists:tb_post_blame,id',
+            'status' => 'required|numeric|in:0,1',
+            'memo' => 'required',
+        ];
+
+        $attributes = [
+            'post_blame_id' => '신고 PK',
+            'status' => '처리 상태',
+            'memo' => '처리 내용',
+        ];
+
+        $validator = Validator::make($request->all(), $rules, [], $attributes);
+        if ($validator->fails()) {
+            return json_encode(['success' => 0, 'message' => $validator->errors()->first()]);
+        }
+        $posts = $validator->valid();
+
+        if((new PostBlame)->postBlameResultUpdate($posts['post_blame_id'], $posts['status'], $posts['memo'])) {
+            return json_encode(['success' => 1, 'message' => '신고 결과를 변경하였습니다.']);
+        }
+    }
+
+    /**
+     * 댓글 신고처리 결과 등록
+     */
+    public function commentBlameResultUpdate(Request $request)
+    {
+        $rules = [
+            'comment_blame_id' => 'required|exists:tb_comment_blame,id',
+            'status' => 'required|numeric|in:0,1',
+            'memo' => 'required',
+        ];
+
+        $attributes = [
+            'comment_blame_id' => '신고 PK',
+            'status' => '처리 상태',
+            'memo' => '처리 내용',
+        ];
+
+        $validator = Validator::make($request->all(), $rules, [], $attributes);
+        if ($validator->fails()) {
+            return json_encode(['success' => 0, 'message' => $validator->errors()->first()]);
+        }
+        $posts = $validator->valid();
+
+        if((new CommentBlame)->commentBlameResultUpdate($posts['comment_blame_id'], $posts['status'], $posts['memo'])) {
+            return json_encode(['success' => 1, 'message' => '신고 결과를 변경하였습니다.']);
+        }
+    }
+}

+ 110 - 0
app/Http/Controllers/Admin/Board/Blame/CommentController.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Blame;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\Comment;
+use App\Models\CommentBlame;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class CommentController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private CommentBlame $commentBlameModel;
+
+    public function __construct(Board $board, CommentBlame $commentBlame)
+    {
+        $this->boardModel = $board;
+        $this->commentBlameModel = $commentBlame;
+    }
+
+    /**
+     * 댓글 > 신고 관리 조회
+     * @method GET
+     * @see /admin/board/blame/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->post('board_id');
+
+        $blameData = $this->commentBlameModel->data($params);
+
+        if ($blameData->rows > 0) {
+            $num = listNum($blameData->total, $params->page, $params->perPage);
+            foreach ($blameData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->editURL = route('admin.board.blame.comment.edit', $row->id);
+                $row->typeStr = MAP_BLAME_TYPE[$row->type];
+                $row->reason = ($row->reason ?? '-');
+
+                $blameData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.blame.comment.index', [
+            'blameData' => $blameData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 댓글 > 신고 관리 삭제
+     * @method DELETE
+     * @see /admin/board/blame/comment/destroy
+     */
+    public function destroy(Request $request, Comment $commentModel)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $commentBlameID) {
+                    $commentBlame = $this->commentBlameModel->findOrNew($commentBlameID);
+
+                    if (!$commentBlame->exists) {
+                        throw new Exception($commentBlameID . '번 신고글은 존재하지 않습니다.');
+                    }
+
+                    if (!$commentBlame->delete()) {
+                        return back()->withErrors($commentBlameID . "번 신고글을 삭제할 수 없습니다.")->withInput();
+                    }
+
+                    // 신고 수 감소
+                    $commentModel->decreaseBlame($commentBlameID);
+                }
+            }
+
+            $message = '신고 기록이 삭제되었습니다.';
+            return redirect()->route('admin.board.comment.blame.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 댓글 > 신고 상세 보기
+     * @method GET
+     * @see /admin/board/blame/comment/{pk}/edit
+     */
+    public function edit(int $commentBlameID)
+    {
+        $blame = $this->commentBlameModel->findOrFail($commentBlameID);
+        $blame->device = $this->device($blame->user_agent);
+        $blame->platform = $this->platform($blame->user_agent);
+        $blame->browser = $this->browser($blame->user_agent);
+
+        return view('admin.board.blame.comment.edit', [
+            'blame' => $blame
+        ]);
+    }
+}

+ 111 - 0
app/Http/Controllers/Admin/Board/Blame/PostController.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Blame;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\Post;
+use App\Models\PostBlame;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class PostController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostBlame $postBlameModel;
+
+    public function __construct(Board $board, PostBlame $postBlame)
+    {
+        $this->boardModel = $board;
+        $this->postBlameModel = $postBlame;
+    }
+
+    /**
+     * 게시판 > 신고 관리 조회
+     * @method GET
+     * @see /admin/board/blame/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->post('board_id');
+
+        $blameData = $this->postBlameModel->data($params);
+
+        if ($blameData->rows > 0) {
+            $num = listNum($blameData->total, $params->page, $params->perPage);
+            foreach ($blameData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->editURL = route('admin.board.blame.post.edit', $row->id);
+                $row->typeStr = MAP_BLAME_TYPE[$row->type];
+                $row->reason = ($row->reason ?? '-');
+
+                $blameData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.blame.post.index', [
+            'blameData' => $blameData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 신고 관리 삭제
+     * @method DELETE
+     * @see /admin/board/blame/destroy
+     */
+    public function destroy(Request $request, Post $postModel)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postBlameID) {
+                    $postBlame = $this->postBlameModel->findOrNew($postBlameID);
+
+                    if(!$postBlame->exists) {
+                        throw new Exception($postBlameID . '번 신고글은 존재하지 않습니다.');
+                    }
+
+                    if(!$postBlame->delete()) {
+                        throw new Exception($postBlameID . "번 신고글을 삭제할 수 없습니다.");
+                    }
+
+                    // 신고 수 감소
+                    $postModel->decreaseBlame($postBlame->post_id);
+
+                }
+            }
+
+            $message = '신고 기록이 삭제되었습니다.';
+            return redirect()->route('admin.board.blame.post.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 게시판 > 신고 상세 보기
+     * @method GET
+     * @see /admin/board/blame/post/{pk}/edit
+     */
+    public function edit(int $postBlameID)
+    {
+        $blame = $this->postBlameModel->findOrFail($postBlameID);
+        $blame->device = $this->device($blame->user_agent);
+        $blame->platform = $this->platform($blame->user_agent);
+        $blame->browser = $this->browser($blame->user_agent);
+
+        return view('admin.board.blame.post.edit', [
+            'blame' => $blame
+        ]);
+    }
+}

+ 158 - 0
app/Http/Controllers/Admin/Board/Board/AuthorityController.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class AuthorityController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 권한
+     * @method GET
+     * @see /admin/board/board/authority/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.authority', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.authority.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all(),
+
+            // 권한 종류
+            'authorityType' => [
+                '' => '모든 사용자',
+                '1' => '로그인 사용자',
+                '100' => '관리자',
+            ]
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 권한 저장
+     * @method POST
+     * @see /admin/board/board/authority/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'access_post_list' => 'nullable|numeric',
+            'access_post_view' => 'nullable|numeric',
+            'access_post_write' => 'nullable|numeric',
+            'access_comment_list' => 'nullable|numeric',
+            'access_comment_write' => 'nullable|numeric',
+            'access_image_upload' => 'nullable|numeric',
+            'access_file_download' => 'nullable|numeric',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'access_post_list' => '목록',
+            'access_post_view' => '글 열람',
+            'access_post_write' => '글 작성',
+            'access_comment_list' => '댓글 목록',
+            'access_comment_write' => '댓글 작성',
+            'access_image_upload' => '이미지 첨부',
+            'access_file_download' => '파일 다운로드',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [];
+
+        $selectArr = [
+            'access_post_list',
+            'access_post_view',
+            'access_post_write',
+            'access_comment_list',
+            'access_comment_write',
+            'access_image_upload',
+            'access_file_download'
+        ];
+
+        foreach($selectArr as $name) {
+            $metaData[$name] = $posts[$name];
+        }
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            $groupKeys = $posts['grp'] ?? [];
+            $allKeys = $posts['all'] ?? [];
+
+            if ($groupKeys) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    foreach ($groupKeys as $gk => $gv) {
+                        $groupData[$gk] = $metaData[$gk];
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+
+            if ($allKeys) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    foreach ($allKeys as $ak => $av) {
+                        $allData[$ak] = $metaData[$ak];
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 권한 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.authority.show', $boardID)->with('message', $message);
+    }
+}

+ 152 - 0
app/Http/Controllers/Admin/Board/Board/CategoryController.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use App\Models\BoardCategory;
+
+class CategoryController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+    private BoardCategory $boardCategoryModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta, BoardCategory $boardCategory)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+        $this->boardCategoryModel = $boardCategory;
+    }
+
+    /**
+     * 게시판 수정 - 분류
+     * @method GET
+     * @see /admin/board/board/category/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.category', [
+            'boardID' => $boardID,
+
+            // 게시판 분류
+            'boardCategoryData' => $this->boardCategoryModel->getCategoryList($boardID),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 분류 등록 저장
+     * @method POST
+     * @see /admin/board/board/category/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'name' => 'required|string',
+            'depth' => 'required|numeric|min:0',
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 수정 - 분류',
+            'category_id' => '분류 PK',
+            'parent_id' => '분류 부모 PK',
+            'name' => '분류 명',
+            'depth' => '분류 깊이',
+        ];
+
+        $type = $request->post('type');
+        if($type == 'modify') { // 수정
+            $rules['board_category_id'] = 'required|numeric|exists:tb_board_category,id';
+            $rules['parent_id'] = 'numeric|nullable|exists:tb_board_category,parent_id';
+        }else{ // 입력
+            $rules['parent_id'] = 'required|numeric|min:0';
+        }
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+        $parentID = ($posts['parent_id'] ?? null);
+        $today = now();
+
+        $saveData = [
+            'board_id' => $boardID,
+            'parent_id' => $parentID,
+            'name' => $posts['name'],
+            'depth' => $posts['depth']
+        ];
+
+        if($type == 'add') {
+            $code = $this->boardCategoryModel->nextKey($parentID, $boardID);
+            $saveData['code'] = $code;
+            $saveData['created_at'] = $today;
+            $this->boardCategoryModel->register($saveData);
+            $message = '게시판 분류가 새로 등록되었습니다.';
+        }else{
+            $saveData['id'] = $posts['board_category_id'];
+            $saveData['updated_at'] = $today;
+            $this->boardCategoryModel->updater($saveData);
+            $message = '게시판 분류가 수정 되었습니다.';
+        }
+
+        return redirect()->route('admin.board.board.category.show', $boardID)->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 분류 삭제
+     * @method DELETE
+     * @see /admin/board/board/category/{pk}/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $boardID = $request->post('board_id');
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $boardCategoryID) {
+                $this->boardCategoryModel->remove($boardCategoryID);
+            }
+        }
+
+        $message = '게시판 분류 정보가 삭제되었습니다.';
+        return redirect()->route('admin.board.board.category.show', $boardID)->with('message', $message);
+    }
+
+    /**
+     * 분류 한 단계 위로
+     */
+    public function up(int $boardCategoryID)
+    {
+        $boardID = $this->boardCategoryModel->find($boardCategoryID)->getOriginal('board_id');
+        if($boardID) {
+            $this->boardCategoryModel->orderUp($boardCategoryID);
+            return redirect()->route('admin.board.board.category.show', $boardID);
+        }else{
+            return back();
+        }
+    }
+
+    /**
+     * 분류 한 단계 아래로
+     */
+    public function down(int $boardCategoryID)
+    {
+        $boardID = $this->boardCategoryModel->find($boardCategoryID)->getOriginal('board_id');
+        if($boardID) {
+            $this->boardCategoryModel->orderDown($boardCategoryID);
+            return redirect()->route('admin.board.board.category.show', $boardID);
+        }else{
+            return back();
+        }
+    }
+}

+ 185 - 0
app/Http/Controllers/Admin/Board/Board/CommentController.php

@@ -0,0 +1,185 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class CommentController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 댓글
+     * @method GET
+     * @see /admin/board/board/comment/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.comment', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.comment.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 작성 저장
+     * @method POST
+     * @see /admin/board/board/comment/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'use_comment' => 'required|numeric',
+            'comment_per_page' => 'required|numeric|min:0|max:20',
+            'comment_page_count' => 'required|numeric|min:0|max:10',
+            'use_comment_like' => 'required|numeric',
+            'use_comment_dislike' => 'required|numeric',
+            'show_user_thumb_in_comment' => 'required|numeric|in:0,1',
+            'show_user_icon_in_comment' => 'required|numeric|in:0,1',
+            'update_order_on_comment' => 'required|numeric',
+            'comment_default_content' => 'string|nullable|max:1000',
+            'comment_min_length' => 'required|numeric|min:0',
+            'comment_max_length' => 'required|numeric|min:0|max:99999999',
+            'use_comment_secret' => 'nullable|numeric|in:1,2',
+            'use_comment_secret_selected' => 'required|numeric|in:0,1',
+            'show_comment_ip' => 'nullable|numeric',
+            'use_comment_blame' => 'required|numeric|in:0,1',
+            'comment_blame_blind_count' => 'required|numeric|min:0',
+            'protect_delete_comment' => 'required|numeric|in:0,1',
+            'protect_update_comment' => 'required|numeric|in:0,1',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'use_comment' => '댓글 사용 여부',
+            'comment_per_page' => '댓글 목록 수',
+            'comment_page_count' => '댓글 페이지 수',
+            'use_comment_like' => '댓글 추천 기능',
+            'use_comment_dislike' => '댓글 비추천 기능',
+            'show_user_thumb_in_comment' => '회원 프로필 이미지',
+            'show_user_icon_in_comment' => '회원 아이콘 이미지',
+            'update_order_on_comment' => '댓글 작성시 글 수정 시각 갱신',
+            'comment_default_content' => '댓글 기본 내용',
+            'comment_min_length' => '최소 글수 제한',
+            'comment_max_length' => '최대 글수 제한',
+            'use_comment_secret' => '비밀글 사용',
+            'use_comment_secret_selected' => '비밀글 기본 선택',
+            'show_comment_ip' => 'IP 보이기',
+            'use_comment_blame' => '댓글 신고 기능',
+            'comment_blame_blind_count' => '댓글 신고 시 숨김',
+            'protect_delete_comment' => '댓글 보호 기능 (삭제 시)',
+            'protect_update_comment' => '댓글 보호 기능 (수정 시)',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'use_comment', 'comment_per_page', 'comment_page_count', 'use_comment_like', 'use_comment_dislike',
+            'show_user_thumb_in_comment', 'show_user_icon_in_comment', 'update_order_on_comment',
+            'comment_default_content', 'comment_min_length', 'comment_max_length',
+            'use_comment_secret', 'use_comment_secret_selected', 'show_comment_ip',
+            'use_comment_blame', 'comment_blame_blind_count', 'protect_delete_comment', 'protect_update_comment'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [
+            'use_comment' => $posts['use_comment'],
+            'comment_per_page' => $posts['comment_per_page'],
+            'comment_page_count' => $posts['comment_page_count'],
+            'use_comment_like' => $posts['use_comment_like'],
+            'use_comment_dislike' => $posts['use_comment_dislike'],
+            'show_user_thumb_in_comment' => $posts['show_user_thumb_in_comment'],
+            'show_user_icon_in_comment' => $posts['show_user_icon_in_comment'],
+            'update_order_on_comment' => $posts['update_order_on_comment'],
+            'comment_default_content' => $posts['comment_default_content'],
+            'comment_min_length' => $posts['comment_min_length'],
+            'comment_max_length' => $posts['comment_max_length'],
+            'use_comment_secret' => $posts['use_comment_secret'],
+            'use_comment_secret_selected' => $posts['use_comment_secret_selected'],
+            'show_comment_ip' => $posts['show_comment_ip'],
+            'use_comment_blame' => $posts['use_comment_blame'],
+            'comment_blame_blind_count' => $posts['comment_blame_blind_count'],
+            'protect_delete_comment' => $posts['protect_delete_comment'],
+            'protect_update_comment' => $posts['protect_update_comment']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 댓글 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.comment.show', $boardID)->with('message', $message);
+    }
+}

+ 186 - 0
app/Http/Controllers/Admin/Board/Board/ExpController.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class ExpController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 경험치
+     * @method GET
+     * @see /admin/board/board/exp/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.exp', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.exp.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 경험치 저장
+     * @method POST
+     * @see /admin/board/board/exp/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'use_exp' => 'required|numeric|in:0,1',
+            'use_exp_info' => 'required|numeric|in:0,1',
+            'exp_write' => 'required|numeric',
+            'exp_comment' => 'required|numeric',
+            'exp_post_delete' => 'required|numeric',
+            'exp_admin_post_delete' => 'required|numeric',
+            'exp_comment_delete' => 'required|numeric',
+            'exp_admin_comment_delete' => 'required|numeric',
+            'exp_file_upload' => 'required|numeric',
+            'exp_file_download' => 'required|numeric',
+            'exp_file_download_uploader' => 'required|numeric',
+            'exp_read' => 'required|numeric',
+            'exp_post_like' => 'required|numeric',
+            'exp_post_dislike' => 'required|numeric',
+            'exp_post_liked' => 'required|numeric',
+            'exp_post_disliked' => 'required|numeric',
+            'exp_comment_like' => 'required|numeric',
+            'exp_comment_dislike' => 'required|numeric',
+            'exp_comment_liked' => 'required|numeric',
+            'exp_comment_disliked' => 'required|numeric'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'use_exp' => '경험치 기능 사용',
+            'use_exp_info' => '경험치 안내',
+            'exp_write' => '원글 작성',
+            'exp_comment' => '댓글 작성',
+            'exp_post_delete' => '작성자 본인이 원글 삭제',
+            'exp_admin_post_delete' => '관리자가 원글 삭제',
+            'exp_comment_delete' => '작성자 본인이 댓글 삭제',
+            'exp_admin_comment_delete' => '관리자가 댓글 삭제',
+            'exp_file_upload' => '파일 업로드',
+            'exp_file_download' => '파일 다운로드',
+            'exp_file_download_uploader' => '파일 다운로드 시 업로더에게',
+            'exp_read' => '게시글 조회',
+            'exp_post_like' => '원글 추천함',
+            'exp_post_dislike' => '원글 비추천함',
+            'exp_post_liked' => '원글 추천 받음',
+            'exp_post_disliked' => '원글 비추천 받음',
+            'exp_comment_like' => '댓글 추천함',
+            'exp_comment_dislike' => '댓글 비추천함',
+            'exp_comment_liked' => '댓글 추천 받음',
+            'exp_comment_disliked' => '댓글 비추천 받음'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'use_exp', 'use_exp_info', 'exp_write', 'exp_comment', 'exp_post_delete', 'exp_admin_post_delete',
+            'exp_comment_delete', 'exp_admin_comment_delete', 'exp_file_upload', 'exp_file_download',
+            'exp_file_download_uploader', 'exp_read', 'exp_post_like', 'exp_post_dislike', 'exp_post_liked',
+            'exp_post_disliked', 'exp_comment_like', 'exp_comment_dislike', 'exp_comment_liked', 'exp_comment_disliked'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [
+            'use_exp' => $posts['use_exp'],
+            'use_exp_info' => $posts['use_exp_info'],
+            'exp_write' => $posts['exp_write'],
+            'exp_comment' => $posts['exp_comment'],
+            'exp_post_delete' => $posts['exp_post_delete'],
+            'exp_admin_post_delete' => $posts['exp_admin_post_delete'],
+            'exp_comment_delete' => $posts['exp_comment_delete'],
+            'exp_admin_comment_delete' => $posts['exp_admin_comment_delete'],
+            'exp_file_upload' => $posts['exp_file_upload'],
+            'exp_file_download' => $posts['exp_file_download'],
+            'exp_file_download_uploader' => $posts['exp_file_download_uploader'],
+            'exp_read' => $posts['exp_read'],
+            'exp_post_like' => $posts['exp_post_like'],
+            'exp_post_dislike' => $posts['exp_post_dislike'],
+            'exp_post_liked' => $posts['exp_post_liked'],
+            'exp_post_disliked' => $posts['exp_post_disliked'],
+            'exp_comment_like' => $posts['exp_comment_like'],
+            'exp_comment_dislike' => $posts['exp_comment_dislike'],
+            'exp_comment_liked' => $posts['exp_comment_liked'],
+            'exp_comment_disliked' => $posts['exp_comment_disliked']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 경험치 정보가 등록되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.exp.show', $boardID)->with('message', $message);
+    }
+}

+ 171 - 0
app/Http/Controllers/Admin/Board/Board/GeneralController.php

@@ -0,0 +1,171 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class GeneralController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 일반
+     * @method GET
+     * @see /admin/board/board/general/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.general', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.general.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 일반 저장
+     * @method POST
+     * @see /admin/board/board/general/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'block_delete' => 'required|numeric|in:0,1',
+            'protect_post_day' => 'required|numeric|min:0|max:365',
+            'protect_comment_day' => 'required|numeric|min:0|max:365',
+            'protect_delete_post' => 'required|numeric|in:0,1',
+            'protect_update_post' => 'required|numeric|in:0,1',
+            'use_category' => 'required|numeric|in:0,1',
+            'use_personal' => 'required|numeric|in:0,1',
+            'use_anonymous' => 'required|numeric|in:0,1',
+            'anonymous_except_admin' => 'required|numeric|in:0,1',
+            'anonymous_name' => 'required|string|max:100',
+            'use_download_log' => 'required|numeric|in:0,1',
+            'use_post_history' => 'required|numeric|in:0,1',
+            'use_link_click_log' => 'required|numeric|in:0,1',
+            'use_comment_history' => 'required|numeric|in:0,1',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'block_delete' => '관리자만 삭제',
+            'protect_post_day' => '게시글 수정/삭제 금지 기간',
+            'protect_comment_day' => '댓글 수정/삭제 금지 기간',
+            'protect_delete_post' => '게시글 보호 기능 (삭제 시)',
+            'protect_update_post' => '게시글 보호 기능 (수정 시)',
+            'use_category' => '분류 기능',
+            'use_personal' => '1:1 게시판',
+            'use_anonymous' => '익명 게시판',
+            'anonymous_except_admin' => '관리자 익명 제외',
+            'anonymous_name' => '익명 이름',
+            'use_download_log' => '다운로드 기록',
+            'use_post_history' => '게시물 변경 기록',
+            'use_link_click_log' => 'Link 클릭 기록',
+            'use_comment_history' => '댓글 변경 기록',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'block_delete', 'protect_post_day', 'protect_comment_day', 'protect_delete_post', 'protect_update_post',
+            'use_category', 'use_personal', 'use_anonymous', 'anonymous_except_admin', 'anonymous_name',
+            'use_download_log', 'use_post_history', 'use_link_click_log', 'use_comment_history'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [
+            'block_delete' => $posts['block_delete'],
+            'protect_post_day' => $posts['protect_post_day'],
+            'protect_comment_day' => $posts['protect_comment_day'],
+            'protect_delete_post' => $posts['protect_delete_post'],
+            'protect_update_post' => $posts['protect_update_post'],
+            'use_category' => $posts['use_category'],
+            'use_personal' => $posts['use_personal'],
+            'use_anonymous' => $posts['use_anonymous'],
+            'anonymous_except_admin' => $posts['anonymous_except_admin'],
+            'anonymous_name' => $posts['anonymous_name'],
+            'use_download_log' => $posts['use_download_log'],
+            'use_post_history' => $posts['use_post_history'],
+            'use_link_click_log' => $posts['use_link_click_log'],
+            'use_comment_history' => $posts['use_comment_history']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 일반 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.general.show', $boardID)->with('message', $message);
+    }
+}

+ 298 - 0
app/Http/Controllers/Admin/Board/Board/ListController.php

@@ -0,0 +1,298 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use Illuminate\Support\Facades\Validator;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use App\Models\BoardGroup;
+use App\Models\DTO\SearchData;
+use App\Models\DTO\ResponseData;
+use Exception;
+
+class ListController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+    private BoardGroup $boardGroupModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta, BoardGroup $boardGroup)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+        $this->boardGroupModel = $boardGroup;
+    }
+
+    /**
+     * 게시판 관리
+     * @method GET
+     * @see /admin/board/board/list
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $boardData = $this->boardModel->data($params);
+
+        if ($boardData->rows > 0) {
+            $num = listNum($boardData->total,$params->page, $params->perPage);
+            foreach ($boardData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postRows = number_format($row->postRows);
+                $row->commentRows = number_format($row->commentRows);
+                $row->editURL = route('admin.board.board.list.show', $row->id);
+
+                $boardData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.board.list', [
+            'boardGroupData' => $this->boardGroupModel->all(),
+            'boardData' => $boardData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 기본
+     * @method GET
+     * @see /admin/board/board/list/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.basic', [
+            'actionURL' => route('admin.board.board.list.update', $boardID),
+            'boardData' => $this->boardModel->find($boardID),
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+            'boardAllData' => $this->boardModel->all(), // 모든 게시판
+            'boardGroupData' => $this->boardGroupModel->all(), // 게시판 그룹
+            'boardID' => $boardID
+        ]);
+    }
+
+    /**
+     * 게시판 등록
+     * @method GET
+     * @see /admin/board/board/create
+     */
+    public function create()
+    {
+        return view('admin.board.board.basic', [
+            'actionURL' => route('admin.board.board.list.store'),
+            'boardData' => [],
+            'boardMetaData' => [],
+            'boardAllData' => $this->boardModel->all(), // 모든 게시판
+            'boardGroupData' => $this->boardGroupModel->all(), // 게시판 그룹
+            'boardID' => null
+        ]);
+    }
+
+    /**
+     * 게시판 등록 저장
+     * @method POST
+     * @see /admin/board/board/list
+     */
+    public function store(Request $request, ResponseData $response)
+    {
+        $rules = [
+            'code' => [
+                'required',
+                'string',
+                'min:3',
+                'max:50',
+                'alpha_dash',
+            ],
+            'name' => 'required|max:255',
+            'board_group_id' => 'required|exists:tb_board_group,id',
+            'pc_header_content' => 'string|nullable|max:4000',
+            'pc_footer_content' => 'string|nullable|max:4000',
+            'mobile_header_content' => 'string|nullable|max:4000',
+            'mobile_footer_content' => 'string|nullable|max:4000',
+            'sort' => 'required|numeric',
+            'is_search' => 'required|numeric|in:0,1',
+            'is_display' => 'required|numeric|in:0,1',
+            'use_inform_modal' => 'required|numeric|in:0,1',
+            'inform_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'code' => '주소',
+            'name' => '제목',
+            'board_group_id' => '그룹 PK',
+            'pc_header_content' => 'PC 상단 내용',
+            'pc_footer_content' => 'PC 하단 내용',
+            'mobile_header_content' => '모바일 상단 내용',
+            'mobile_footer_content' => '모바일 하단 내용',
+            'sort' => '순서',
+            'is_search' => '검색 여부',
+            'is_display' => '사용 여부',
+            'use_inform_modal' => '이용안내 표시',
+            'inform_content' => '이용안내 내용'
+        ];
+
+        $this->validate($request, $rules, [], $attributes);
+
+        $result = $this->boardModel->register($request, $response);
+
+        if(!$result->success) {
+            return back()->withErrors($result->message)->withInput();
+        }
+
+        $message = '게시판이 등록되었습니다.';
+        return redirect()->route('admin.board.board.list.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 수정 저장
+     * @method PUT
+     * @see /admin/board/board/list/{pk}
+     */
+    public function update(Request $request, ResponseData $response)
+    {
+        $boardID = $request->post('board_id');
+
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'code' => [
+                'required',
+                'string',
+                'min:3',
+                'max:50',
+                'alpha_dash',
+                Rule::unique('tb_board', 'code')->ignore($boardID, 'id')
+            ],
+            'name' => 'required|max:255',
+            'board_group_id' => 'required|exists:tb_board_group,id',
+            'pc_header_content' => 'string|nullable',
+            'pc_footer_content' => 'string|nullable',
+            'mobile_header_content' => 'string|nullable',
+            'mobile_footer_content' => 'string|nullable',
+            'sort' => 'required|numeric',
+            'is_search' => 'required|numeric|in:0,1',
+            'is_display' => 'required|numeric|in:0,1',
+            'use_inform_modal' => 'required|numeric|in:0,1',
+            'inform_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'board_id' => 'PK',
+            'code' => '주소',
+            'name' => '제목',
+            'board_group_id' => '그룹 PK',
+            'pc_header_content' => 'PC 상단 내용',
+            'pc_footer_content' => 'PC 하단 내용',
+            'mobile_header_content' => '모바일 상단 내용',
+            'mobile_footer_content' => '모바일 하단 내용',
+            'sort' => '순서',
+            'is_search' => '검색 여부',
+            'is_display' => '사용 여부',
+            'use_inform_modal' => '이용안내 표시',
+            'inform_content' => '이용안내 내용'
+        ];
+
+        $this->validate($request, $rules, [], $attributes);
+
+        $result = $this->boardModel->updater($boardID, $request, $response);
+
+        if(!$result->success) {
+            return back()->withErrors($result->message)->withInput();
+        }
+
+        $message = '게시판이 수정되었습니다.';
+        return redirect()->route('admin.board.board.list.show', $boardID)->with('message', $message);
+    }
+
+    /**
+     * 게시판 선택 수정
+     * @method PUT
+     * @see /admin/board/board/list
+     */
+    public function listUpdate(Request $request)
+    {
+        $posts = $request->all();
+
+        if($posts['chk']) {
+            foreach($posts['chk'] as $boardID) {
+                $updateData = [
+                    'code' => ($posts['code'][$boardID] ?? ''),
+                    'name' => ($posts['name'][$boardID] ?? ''),
+                    'board_group_id' => ($posts['board_group_id'][$boardID] ?? ''),
+                    'sort' => ($posts['sort'][$boardID] ?? '0'),
+                    'is_search' => ($posts['is_search'][$boardID] ?? '0'),
+                    'is_display' => ($posts['is_display'][$boardID] ?? '0'),
+                ];
+
+                $rules = [
+                    'code' => [
+                        'required',
+                        'string',
+                        'min:3',
+                        'max:50',
+                        'alpha_dash',
+                        Rule::unique('tb_board', 'code')->ignore($boardID, 'id')
+                    ],
+                    'name' => 'required|max:255',
+                    'board_group_id' => 'required|exists:tb_board_group,id',
+                    'sort' => 'required|numeric',
+                    'is_search' => 'required|numeric|in:0,1',
+                    'is_display' => 'required|numeric|in:0,1'
+                ];
+
+                $attributes = [
+                    'code' => '주소',
+                    'name' => '제목',
+                    'board_group_id' => '그룹 PK',
+                    'sort' => '순서',
+                    'is_search' => '검색 여부',
+                    'is_display' => '사용 여부'
+                ];
+
+                $validator = Validator::make($updateData, $rules, [], $attributes);
+                if($validator->fails()) {
+                    return back()->withErrors($validator)->withInput();
+                }
+
+                $this->boardModel->find($boardID)->update($updateData);
+            }
+        }
+
+        $message = '게시판 정보가 변경되었습니다.';
+        return redirect()->route('admin.board.board.list.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 삭제
+     * @method DELETE
+     * @see /admin/board/board/list/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try{
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $boardID) {
+                    $board = $this->boardModel->findOrNew($boardID);
+
+                    if (!$board->exists) {
+                        throw new Exception($boardID . '번 게시판은 존재하지 않습니다.');
+                    }
+
+                    if ($board->post->count() > 0) {
+                        return back()->withErrors($boardID . "번 게시판은 삭제할 수 없습니다.")->withInput();
+                    }
+
+                    $this->boardModel->remove($boardID);
+                }
+            }
+
+            $message = '게시판 정보가 삭제되었습니다.';
+            return redirect()->route('admin.board.board.list.index')->with('message', $message);
+        }catch(Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 123 - 0
app/Http/Controllers/Admin/Board/Board/ManagerController.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\Board;
+use App\Models\BoardAdmin;
+use Exception;
+
+class ManagerController extends Controller
+{
+    private Board $boardModel;
+    private BoardAdmin $boardAdminModel;
+    private User $userModel;
+
+    public function __construct(Board $board, BoardAdmin $boardAdmin, User $user)
+    {
+        $this->boardModel = $board;
+        $this->boardAdminModel = $boardAdmin;
+        $this->userModel = $user;
+    }
+
+    /**
+     * 게시판 수정 - 관리자
+     * @method GET
+     * @see /admin/board/board/manager/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.manager', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.manager.store'),
+
+            // 게시판 관리자 목록
+            'boardAdminData' => $this->boardAdminModel->data(),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 관리자 등록 저장
+     * @method POST
+     * @see /admin/board/board/manager/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'sid' => 'required|exists:users,sid'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 수정 - 관리자',
+            'sid' => '회원 ID'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $boardID = $posts['board_id'];
+        $sid = $posts['sid'];
+
+        $user = $this->userModel->findByUserID($sid);
+
+        // 이미 등록된 관리자인지 확인
+        $isBoardAdmin = $this->boardAdminModel->where([
+            ['board_id', $boardID],
+            ['user_id', $user->id]
+        ])->count();
+
+        if ($isBoardAdmin) {
+            return back()->withErrors(['sid' => "{$sid}는 이미 등록된 관리자 입니다."])->withInput();
+        }
+
+        $this->boardAdminModel->insert([
+            'board_id' => $boardID,
+            'user_id' => $user->id,
+            'created_at' => now()
+        ]);
+
+        $message = '게시판 관리자가 새로 등록되었습니다.';
+        return redirect()->route('admin.board.board.manager.show', $boardID)->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 관리자 삭제
+     * @method DELETE
+     * @see /admin/board/board/manager/{pk}/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $boardID = $request->post('board_id');
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $boardAdminID) {
+                    $boardAdmin = $this->boardAdminModel->findOrNew($boardAdminID);
+
+                    if (!$boardAdmin->exists) {
+                        throw new Exception($boardAdminID . '번 관리자는 존재하지 않습니다.');
+                    }
+
+                    if (!$boardAdmin->delete()) {
+                        throw new Exception($boardAdminID . '번 관리자는 삭제할 수 없습니다.');
+                    }
+                }
+            }
+
+            $message = '게시판 관리자 정보가 삭제되었습니다.';
+            return redirect()->route('admin.board.board.manager.show', $boardID)->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 300 - 0
app/Http/Controllers/Admin/Board/Board/NotifyController.php

@@ -0,0 +1,300 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class NotifyController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 알람
+     * @method GET
+     * @see /admin/board/board/notify/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.notify', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.notify.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 알람 저장
+     * @method POST
+     * @see /admin/board/board/notify/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'send_email_post_super_admin' => 'numeric|in:0,1',
+            'send_email_post_post_writer' => 'numeric|in:0,1',
+            'send_email_comment_super_admin' => 'numeric|in:0,1',
+            'send_email_comment_post_writer' => 'numeric|in:0,1',
+            'send_email_comment_comment_writer' => 'numeric|in:0,1',
+            'send_email_post_blame_super_admin' => 'numeric|in:0,1',
+            'send_email_post_blame_post_writer' => 'numeric|in:0,1',
+            'send_email_comment_blame_super_admin' => 'numeric|in:0,1',
+            'send_email_comment_blame_post_writer' => 'numeric|in:0,1',
+            'send_email_comment_blame_comment_writer' => 'numeric|in:0,1',
+            'send_email_post_personal_super_admin' => 'numeric|in:0,1',
+            'send_email_post_personal_post_writer' => 'numeric|in:0,1',
+            'send_email_post_personal_reply_super_admin' => 'numeric|in:0,1',
+            'send_email_post_personal_reply_post_writer' => 'numeric|in:0,1',
+            'send_email_post_personal_reply_comment_writer' => 'numeric|in:0,1',
+
+            'send_telegram_post_super_admin' => 'numeric|in:0,1',
+            'send_telegram_comment_super_admin' => 'numeric|in:0,1',
+            'send_telegram_blame_super_admin' => 'numeric|in:0,1',
+            'send_telegram_comment_blame_super_admin' => 'numeric|in:0,1',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'send_email_post_super_admin' => '이메일 발송(게시글 작성) - 최고 관리자',
+            'send_email_post_post_writer' => '이메일 발송(게시글 작성) - 게시글 작성자',
+            'send_email_comment_super_admin' => '이메일 발송(댓글 작성) - 최고 관리자',
+            'send_email_comment_post_writer' => '이메일 발송(댓글 작성) - 게시글 작성자',
+            'send_email_comment_comment_writer' => '이메일 발송(댓글) - 댓글 작성자',
+            'send_email_post_blame_super_admin' => '이메일 발송(게시글 신고) - 최고 관리자',
+            'send_email_post_blame_post_writer' => '이메일 발송(게시글 신고) - 게시글 작성자',
+            'send_email_comment_blame_super_admin' => '이메일 발송(댓글 신고) - 최고 관리자',
+            'send_email_comment_blame_post_writer' => '이메일 발송(댓글 신고) - 게시글 작성자',
+            'send_email_comment_blame_comment_writer' => '이메일 발송(댓글 신고) - 댓글 작성자',
+            'send_email_post_personal_super_admin' => '이메일 발송(1:1 문의 접수 시) - 최고관리자',
+            'send_email_post_personal_post_writer' => '이메일 발송(1:1 문의 접수 시) - 게시글 작성자',
+            'send_email_post_personal_reply_super_admin' => '이메일 발송(1:1 문의 답변 시) - 최고관리자',
+            'send_email_post_personal_reply_post_writer' => '이메일 발송(1:1 문의 답변 시) - 게시글 작성자',
+            'send_email_post_personal_reply_comment_writer' => '이메일 발송(1:1 문의 답변 시) - 댓글 작성자',
+
+            'send_telegram_post_super_admin' => '문자 발송(게시글 작성) - 최고 관리자',
+            'send_telegram_comment_super_admin' => '문자 발송(댓글 작성) - 최고 관리자',
+            'send_telegram_blame_super_admin' => '문자 발송(게시글 신고) - 최고 관리자',
+            'send_telegram_comment_blame_super_admin' => '문자 발송(댓글 신고) - 최고 관리자',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [
+            // 게시글 작성
+            'send_email_post_super_admin' => ($posts['send_email_post_super_admin'] ?? 0),
+            'send_email_post_post_writer' => ($posts['send_email_post_post_writer'] ?? 0),
+
+            // 댓글 작성
+            'send_email_comment_super_admin' => ($posts['send_email_comment_super_admin'] ?? 0),
+            'send_email_comment_post_writer' => ($posts['send_email_comment_post_writer'] ?? 0),
+            'send_email_comment_comment_writer' => ($posts['send_email_comment_comment_writer'] ?? 0),
+
+            // 게시글 신고
+            'send_email_post_blame_super_admin' => ($posts['send_email_post_blame_super_admin'] ?? 0),
+            'send_email_post_blame_post_writer' => ($posts['send_email_post_blame_post_writer'] ?? 0),
+
+            // 댓글 신고
+            'send_email_comment_blame_super_admin' => ($posts['send_email_comment_blame_super_admin'] ?? 0),
+            'send_email_comment_blame_post_writer' => ($posts['send_email_comment_blame_post_writer'] ?? 0),
+            'send_email_comment_blame_comment_writer' => ($posts['send_email_comment_blame_comment_writer'] ?? 0),
+
+            // 1:1 문의
+            'send_email_post_personal_super_admin' => ($posts['send_email_post_personal_super_admin'] ?? 0),
+            'send_email_post_personal_post_writer' => ($posts['send_email_post_personal_post_writer'] ?? 0),
+            'send_email_post_personal_reply_super_admin' => ($posts['send_email_post_personal_reply_super_admin'] ?? 0),
+            'send_email_post_personal_reply_post_writer' => ($posts['send_email_post_personal_reply_post_writer'] ?? 0),
+            'send_email_post_personal_reply_comment_writer' => ($posts['send_email_post_personal_reply_comment_writer'] ?? 0),
+
+            // 텔레그램
+            'send_telegram_post_super_admin' => ($posts['send_telegram_post_super_admin'] ?? 0),
+            'send_telegram_comment_super_admin' => ($posts['send_telegram_comment_super_admin'] ?? 0),
+            'send_telegram_blame_super_admin' => ($posts['send_telegram_blame_super_admin'] ?? 0),
+            'send_telegram_comment_blame_super_admin' => ($posts['send_telegram_comment_blame_super_admin'] ?? 0),
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            $groupKeys = $posts['grp'] ?? [];
+            $allKeys = $posts['all'] ?? [];
+
+            if ($groupKeys) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+
+                    foreach ($groupKeys as $gk => $gv) {
+                        // 게시글 작성 시
+                        if ($gk === 'send_email_post') {
+                            $groupData['send_email_post_super_admin'] = $metaData['send_email_post_super_admin'];
+                            $groupData['send_email_post_post_writer'] = $metaData['send_email_post_post_writer'];
+                        }
+                        // 댓글 작성 시
+                        if ($gk === 'send_email_comment') {
+                            $groupData['send_email_comment_super_admin'] = $metaData['send_email_comment_super_admin'];
+                            $groupData['send_email_comment_post_writer'] = $metaData['send_email_comment_post_writer'];
+                            $groupData['send_email_comment_comment_writer'] = $metaData['send_email_comment_comment_writer'];
+                        }
+                        // 게시글 신고 발생 시
+                        if ($gk === 'send_email_post_blame') {
+                            $groupData['send_email_post_blame_super_admin'] = $metaData['send_email_post_blame_super_admin'];
+                            $groupData['send_email_post_blame_post_writer'] = $metaData['send_email_post_blame_post_writer'];
+                        }
+                        // 댓글 신고 발생 시
+                        if ($gk === 'send_email_comment_blame') {
+                            $groupData['send_email_comment_blame_super_admin'] = $metaData['send_email_comment_blame_super_admin'];
+                            $groupData['send_email_comment_blame_post_writer'] = $metaData['send_email_comment_blame_post_writer'];
+                            $groupData['send_email_comment_blame_comment_writer'] = $metaData['send_email_comment_blame_comment_writer'];
+                        }
+                        // 1:1 문의 접수 시
+                        if ($gk === 'send_email_post_personal') {
+                            $groupData['send_email_post_personal_super_admin'] = $metaData['send_email_post_personal_super_admin'];
+                            $groupData['send_email_post_personal_post_writer'] = $metaData['send_email_post_personal_post_writer'];
+                        }
+                        // 1:1 문의 답변 시
+                        if ($gk === 'send_email_post_personal_reply') {
+                            $groupData['send_email_post_personal_reply_super_admin'] = $metaData['send_email_post_personal_reply_super_admin'];
+                            $groupData['send_email_post_personal_reply_post_writer'] = $metaData['send_email_post_personal_reply_post_writer'];
+                            $groupData['send_email_post_personal_reply_comment_writer'] = $metaData['send_email_post_personal_reply_comment_writer'];
+                        }
+
+                        /*
+                         * 텔레그램
+                         */
+                        // 게시글 작성 시
+                        if ($gk === 'send_telegram_post') {
+                            $groupData['send_telegram_post_super_admin'] = $metaData['send_telegram_post_super_admin'];
+                        }
+                        // 댓글 작성 시
+                        if ($gk === 'send_telegram_comment') {
+                            $groupData['send_telegram_comment_super_admin'] = $metaData['send_telegram_comment_super_admin'];
+                        }
+                        // 게시글 신고 발생 시
+                        if ($gk === 'send_telegram_blame') {
+                            $groupData['send_telegram_blame_super_admin'] = $metaData['send_telegram_blame_super_admin'];
+                        }
+                        // 댓글 신고 발생 시
+                        if ($gk === 'send_telegram_comment_blame') {
+                            $groupData['send_telegram_comment_blame_super_admin'] = $metaData['send_telegram_comment_blame_super_admin'];
+                        }
+                    }
+
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+
+            if ($allKeys) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    foreach ($allKeys as $ak => $av) {
+                        // 게시글 작성 시
+                        if ($ak === 'send_email_post') {
+                            $allData['send_email_post_super_admin'] = $metaData['send_email_post_super_admin'];
+                            $allData['send_email_post_post_writer'] = $metaData['send_email_post_post_writer'];
+                        }
+                        // 댓글 작성 시
+                        if ($ak === 'send_email_comment') {
+                            $allData['send_email_comment_super_admin'] = $metaData['send_email_comment_super_admin'];
+                            $allData['send_email_comment_post_writer'] = $metaData['send_email_comment_post_writer'];
+                            $allData['send_email_comment_comment_writer'] = $metaData['send_email_comment_comment_writer'];
+                        }
+                        // 게시글 신고 발생 시
+                        if ($ak === 'send_email_post_blame') {
+                            $allData['send_email_post_blame_super_admin'] = $metaData['send_email_post_blame_super_admin'];
+                            $allData['send_email_post_blame_post_writer'] = $metaData['send_email_post_blame_post_writer'];
+                        }
+                        // 댓글 신고 발생 시
+                        if ($ak === 'send_email_comment_blame') {
+                            $allData['send_email_comment_blame_super_admin'] = $metaData['send_email_comment_blame_super_admin'];
+                            $allData['send_email_comment_blame_post_writer'] = $metaData['send_email_comment_blame_post_writer'];
+                            $allData['send_email_comment_blame_comment_writer'] = $metaData['send_email_comment_blame_comment_writer'];
+                        }
+                        // 1:1 문의 접수 시
+                        if ($ak === 'send_email_post_personal') {
+                            $allData['send_email_post_personal_super_admin'] = $metaData['send_email_post_personal_super_admin'];
+                            $allData['send_email_post_personal_post_writer'] = $metaData['send_email_post_personal_post_writer'];
+                        }
+                        // 1:1 문의 답변 시
+                        if ($ak === 'send_email_post_personal_reply') {
+                            $allData['send_email_post_personal_reply_super_admin'] = $metaData['send_email_post_personal_reply_super_admin'];
+                            $allData['send_email_post_personal_reply_post_writer'] = $metaData['send_email_post_personal_reply_post_writer'];
+                            $allData['send_email_post_personal_reply_comment_writer'] = $metaData['send_email_post_personal_reply_comment_writer'];
+                        }
+
+                        /*
+                         * 텔레그램
+                         */
+                        // 게시글 작성 시
+                        if ($ak === 'send_telegram_post') {
+                            $allData['send_telegram_post_super_admin'] = $metaData['send_telegram_post_super_admin'];
+                        }
+                        // 댓글 작성 시
+                        if ($ak === 'send_telegram_comment') {
+                            $allData['send_telegram_comment_super_admin'] = $metaData['send_telegram_comment_super_admin'];
+                        }
+                        // 게시글 신고 발생 시
+                        if ($ak === 'send_telegram_blame') {
+                            $allData['send_telegram_blame_super_admin'] = $metaData['send_telegram_blame_super_admin'];
+                        }
+                        // 댓글 신고 발생 시
+                        if ($ak === 'send_telegram_comment_blame') {
+                            $allData['send_telegram_comment_blame_super_admin'] = $metaData['send_telegram_comment_blame_super_admin'];
+                        }
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 알람 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.notify.show', $boardID)->with('message', $message);
+    }
+}

+ 190 - 0
app/Http/Controllers/Admin/Board/Board/PostController.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class PostController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 목록
+     * @method GET
+     * @see /admin/board/board/post/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.post', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.post.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 목록 저장
+     * @method POST
+     * @see /admin/board/board/post/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'board_layout_type' => 'required|numeric|in:0,1,2',
+            'list_sort_type' => 'required|numeric|in:1,2,3,4',
+            'list_per_page' => 'required|numeric|between:0,50',
+            'list_mobile_per_page' => 'required|numeric|between:0,50',
+            'list_page_count' => 'required|numeric|between:0,50',
+            'list_page_mobile_count' => 'required|numeric|between:0,50',
+            'always_show_write_button' => 'required|in:0,1',
+            'show_list_from_view' => 'required|in:0,1',
+            'new_icon_hour' => 'required|numeric|between:0,24',
+            'hot_icon_hit' => 'required|numeric|between:0,999999',
+            'hot_icon_day' => 'required|numeric|between:0,365',
+            'subject_length' => 'required|max:255',
+            'subject_mobile_length' => 'required|max:255',
+            'except_notice' => 'required|numeric',
+            'except_speaker' => 'required|numeric',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'board_layout_type' => '게시판 종류',
+            'list_sort_type' => '기본 정렬 순',
+            'list_per_page' => 'PC - 목록 수',
+            'list_mobile_per_page' => '모바일 - 목록 수',
+            'list_page_count' => 'PC - 페이지 수',
+            'list_page_mobile_count' => '모바일 - 페이지 수',
+            'always_show_write_button' => '글쓰기 버튼 보이기',
+            'show_list_from_view' => '하단 목록 보이기',
+            'new_icon_hour' => 'NEW 아이콘 - 시간',
+            'hot_icon_hit' => 'HOT 아이콘 - 조회 수',
+            'hot_icon_day' => 'HOT 아이콘 - 일',
+            'subject_length' => 'PC - 제목',
+            'subject_mobile_length' => 'PC - 모바일',
+            'except_notice' => '공지사항 제외',
+            'except_speaker' => '전체공지 제외',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'board_layout_type', 'list_sort_type', 'list_per_page', 'list_mobile_per_page',
+            'list_page_count', 'list_page_mobile_count', 'always_show_write_button', 'show_list_from_view', 'new_icon_hour',
+            'hot_icon_hit', 'hot_icon_day', 'subject_length', 'subject_mobile_length', 'except_notice', 'except_speaker'
+        ];
+
+        $mapMobileData = [ // 모바일 값
+            'list_per_page' => 'list_mobile_per_page',
+            'list_page_count' => 'list_page_mobile_count',
+            'subject_length' => 'subject_mobile_length'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($posts['board_id']);
+
+        // 메타 정보 저장
+        $metaData = [
+            'board_layout_type' => $posts['board_layout_type'],
+            'list_sort_type' => $posts['list_sort_type'],
+            'list_per_page' => $posts['list_per_page'],
+            'list_mobile_per_page' => $posts['list_mobile_per_page'],
+            'list_page_count' => $posts['list_page_count'],
+            'list_page_mobile_count' => $posts['list_page_mobile_count'],
+            'always_show_write_button' => $posts['always_show_write_button'],
+            'show_list_from_view' => $posts['show_list_from_view'],
+            'new_icon_hour' => $posts['new_icon_hour'],
+            'hot_icon_hit' => $posts['hot_icon_hit'],
+            'hot_icon_day' => $posts['hot_icon_day'],
+            'subject_length' => $posts['subject_length'],
+            'subject_mobile_length' => $posts['subject_mobile_length'],
+            'except_notice' => $posts['except_notice'],
+            'except_speaker' => $posts['except_speaker']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+
+                    if (array_key_exists($field, $mapMobileData)) {
+                        $groupData[$mapMobileData[$field]] = $posts[$mapMobileData[$field]];
+                    }
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+
+                    if (array_key_exists($field, $mapMobileData)) {
+                        $allData[$mapMobileData[$field]] = $posts[$mapMobileData[$field]];
+                    }
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 목록 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.post.show', $boardID)->with('message', $message);
+    }
+}

+ 188 - 0
app/Http/Controllers/Admin/Board/Board/ViewController.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class ViewController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 열람
+     * @method GET
+     * @see /admin/board/board/view/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.view', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.view.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 열람 저장
+     * @method POST
+     * @see /admin/board/board/view/{pk}
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'use_bookmark' => 'required|numeric|in:0,1',
+            'use_post_like' => 'required|numeric|in:0,1',
+            'use_post_dislike' => 'required|numeric|in:0,1',
+            'use_print' => 'required|numeric|in:0,1',
+            'use_sns' => 'required|numeric|in:0,1',
+            'use_prev_next_post' => 'required|numeric|in:0,1',
+            'use_blame' => 'required|numeric|in:0,1',
+            'blame_blind_count' => 'required|numeric',
+            'show_post_ip' => 'nullable|numeric',
+            'content_target_blank' => 'required|numeric|in:0,1',
+            'use_auto_url' => 'required|numeric|in:0,1',
+            'use_copy_post_url' => 'required|numeric|in:0,1',
+            'use_url_qrcode' => 'required|numeric|in:0,1',
+            'use_attached_url_qrcode' => 'required|numeric|in:0,1',
+            'need_like_for_download' => 'required|numeric|in:0,1',
+            'need_comment_for_download' => 'required|numeric|in:0,1',
+            'show_user_thumb_in_post' => 'required|numeric|in:0,1',
+            'show_user_icon_in_post' => 'required|numeric|in:0,1',
+            'use_post_user_regdate' => 'required|numeric|in:0,1',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'use_bookmark' => '즐겨찾기 기능',
+            'use_post_like' => '추천 기능',
+            'use_post_dislike' => '비추천 기능',
+            'use_print' => '본문 인쇄 기능',
+            'use_sns' => 'SNS 보내기 기능',
+            'use_prev_next_post' => '이전글, 다음글 버튼',
+            'use_blame' => '신고 기능',
+            'blame_blind_count' => '신고 시 숨김',
+            'show_post_ip' => 'IP 보이기',
+            'content_target_blank' => 'Link 새창',
+            'use_auto_url' => '본문 URL Link 생성',
+            'use_copy_post_url' => '주소 복사 버튼',
+            'use_url_qrcode' => '글 주소 QR 코드',
+            'use_attached_url_qrcode' => '첨부된 링크 QR 코드 ',
+            'need_like_for_download' => '다운로드 제한 (추천 필수)',
+            'need_comment_for_download' => '다운로드 제한 (댓글 필수)',
+            'show_user_thumb_in_post' => '회원 프로필 이미지',
+            'show_user_icon_in_post' => '회원 아이콘 이미지',
+            'use_post_user_regdate' => '회원 가입일',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'use_bookmark', 'use_post_like', 'use_post_dislike', 'use_print', 'use_sns',
+            'use_prev_next_post', 'use_blame', 'blame_blind_count', 'show_post_ip',
+            'content_target_blank', 'use_auto_url', 'use_copy_post_url', 'use_url_qrcode',
+            'use_attached_url_qrcode', 'need_like_for_download', 'need_comment_for_download',
+            'show_user_thumb_in_post', 'show_user_icon_in_post', 'use_post_user_regdate'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($posts['board_id']);
+
+        // 메타 정보 저장
+        $metaData = [
+            'use_bookmark' => $posts['use_bookmark'],
+            'use_post_like' => $posts['use_post_like'],
+            'use_post_dislike' => $posts['use_post_dislike'],
+            'use_print' => $posts['use_print'],
+            'use_sns' => $posts['use_sns'],
+            'use_prev_next_post' => $posts['use_prev_next_post'],
+            'use_blame' => $posts['use_blame'],
+            'blame_blind_count' => $posts['blame_blind_count'],
+            'show_post_ip' => $posts['show_post_ip'],
+            'content_target_blank' => $posts['content_target_blank'],
+            'use_auto_url' => $posts['use_auto_url'],
+            'use_copy_post_url' => $posts['use_copy_post_url'],
+            'use_url_qrcode' => $posts['use_url_qrcode'],
+            'use_attached_url_qrcode' => $posts['use_attached_url_qrcode'],
+            'need_like_for_download' => $posts['need_like_for_download'],
+            'need_comment_for_download' => $posts['need_comment_for_download'],
+            'show_user_thumb_in_post' => $posts['show_user_thumb_in_post'],
+            'show_user_icon_in_post' => $posts['show_user_icon_in_post'],
+            'use_post_user_regdate' => $posts['use_post_user_regdate']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 열람 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.view.show', $boardID)->with('message', $message);
+    }
+}

+ 197 - 0
app/Http/Controllers/Admin/Board/Board/WriteController.php

@@ -0,0 +1,197 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Board;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardMeta;
+use Exception;
+
+class WriteController extends Controller
+{
+    private Board $boardModel;
+    private BoardMeta $boardMetaModel;
+
+    public function __construct(Board $board, BoardMeta $boardMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardMetaModel = $boardMeta;
+    }
+
+    /**
+     * 게시판 수정 - 작성
+     * @method GET
+     * @see /admin/board/board/write/{pk}
+     */
+    public function show(int $boardID)
+    {
+        return view('admin.board.board.write', [
+            'boardID' => $boardID,
+            'actionURL' => route('admin.board.board.write.store'),
+
+            // 게시판 정보
+            'boardData' => $this->boardModel->find($boardID),
+
+            // 게시판 메타 정보
+            'boardMetaData' => $this->boardMetaModel->getAllMeta($boardID),
+
+            // 모든 게시판
+            'boardAllData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 수정 - 작성 저장
+     * @method GET
+     * @see /admin/board/board/write/store
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_id' => 'required|numeric|exists:tb_board,id',
+            'write_header_content' => 'string|nullable',
+            'mobile_write_header_content' => 'string|nullable',
+            'post_default_subject' => 'string|nullable',
+            'post_default_content' => 'string|nullable',
+            'use_post_dhtml' => 'required|numeric|in:0,1',
+            'save_external_image' => 'required|numeric|in:0,1',
+            'post_subject_min_length' => 'required|numeric|min:0',
+            'post_subject_max_length' => 'required|numeric|min:0|max:255',
+            'post_content_min_length' => 'required|numeric|min:0',
+            'post_content_max_length' => 'required|numeric|min:0|max:4000',
+            'use_category_required' => 'nullable|numeric|in:1,0',
+            'use_post_secret' => 'nullable|numeric|in:1,2',
+            'use_post_secret_selected' => 'required|numeric|in:0,1',
+            'use_post_tag' => 'required|numeric|in:0,1',
+            'link_num' => 'required|numeric|min:0|max:10',
+            'use_upload_file' => 'required|numeric|in:0,1',
+            'upload_file_num' => 'required|numeric|min:0|max:10',
+            'upload_file_max_size' => 'required|numeric',
+            'upload_only_img_file' => 'string|numeric|in:0,1',
+            'upload_file_extension' => 'string|nullable|max:255',
+            'use_only_one_post' => 'required|numeric|in:0,1',
+            'use_post_captcha' => 'required|numeric|in:0,1',
+            'grp' => 'array',
+            'all' => 'array'
+        ];
+
+        $attributes = [
+            'board_id' => '게시판 PK',
+            'write_header_content' => '작성폼 상단 내용',
+            'mobile_write_header_content' => '모바일 작성폼 상단 내용',
+            'post_default_subject' => '글쓰기 시 기본 제목',
+            'post_default_content' => '글쓰기 시 기본 내용',
+            'use_post_dhtml' => '본문 에디터 사용',
+            'save_external_image' => '외부 이미지 가져오기',
+            'post_subject_min_length' => '최소 글수 제한 (제목)',
+            'post_subject_max_length' => '최대 글수 제한 (제목)',
+            'post_content_min_length' => '최소 글수 제한 (내용)',
+            'post_content_max_length' => '최대 글수 제한 (내용)',
+            'use_category_required' => '분류 필수 선택',
+            'use_post_secret' => '비밀글 사용',
+            'use_post_secret_selected' => '비밀글 기본 선택',
+            'use_post_tag' => '태그 사용',
+            'link_num' => 'URL Link 개수',
+            'use_upload_file' => '첨부파일 사용',
+            'upload_file_num' => '첨부파일 개수 제한',
+            'upload_file_max_size' => '첨부파일 용량 제한',
+            'upload_only_img_file' => '첨부파일 이미지 허용',
+            'upload_file_extension' => '첨부파일 허용 확장자',
+            'use_only_one_post' => '하루에 한 글만 작성',
+            'use_post_captcha' => 'Captcha 사용',
+            'grp' => '그룹 적용',
+            'all' => '전체 적용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $boardID = $posts['board_id'];
+
+        $defaultData = [ // 그룹, 전체 적용 값
+            'write_header_content', 'mobile_write_header_content', 'post_default_subject', 'post_default_content',
+            'use_post_dhtml', 'save_external_image', 'post_subject_min_length', 'post_subject_max_length',
+            'post_content_min_length', 'post_content_max_length', 'use_category_required', 'use_post_secret', 'use_post_secret_selected',
+            'use_post_tag', 'link_num', 'use_upload_file', 'upload_file_num', 'upload_file_max_size',
+            'upload_only_img_file', 'upload_file_extension', 'use_only_one_post', 'use_post_captcha'
+        ];
+
+        // 게시판 정보 조회
+        $boardData = $this->boardModel->find($boardID);
+
+        // 메타 정보 저장
+        $metaData = [
+            'write_header_content' => $posts['write_header_content'],
+            'mobile_write_header_content' => $posts['mobile_write_header_content'],
+            'post_default_subject' => $posts['post_default_subject'],
+            'post_default_content' => $posts['post_default_content'],
+            'use_post_dhtml' => $posts['use_post_dhtml'],
+            'save_external_image' => $posts['save_external_image'],
+            'post_subject_min_length' => $posts['post_subject_min_length'],
+            'post_subject_max_length' => $posts['post_subject_max_length'],
+            'post_content_min_length' => $posts['post_content_min_length'],
+            'post_content_max_length' => $posts['post_content_max_length'],
+            'use_category_required' => $posts['use_category_required'],
+            'use_post_secret' => $posts['use_post_secret'],
+            'use_post_secret_selected' => $posts['use_post_secret_selected'],
+            'link_num' => $posts['link_num'],
+            'use_post_tag' => $posts['use_post_tag'],
+            'use_upload_file' => $posts['use_upload_file'],
+            'upload_file_num' => $posts['upload_file_num'],
+            'upload_file_max_size' => $posts['upload_file_max_size'],
+            'upload_only_img_file' => $posts['upload_only_img_file'],
+            'upload_file_extension' => $posts['upload_file_extension'],
+            'use_only_one_post' => $posts['use_only_one_post'],
+            'use_post_captcha' => $posts['use_post_captcha']
+        ];
+
+        DB::beginTransaction();
+
+        try
+        {
+            $this->boardMetaModel->save($boardID, $metaData);
+
+            /*
+             * 그룹, 전체 적용
+             */
+            $groupData = []; // 그룹적용
+            $allData = []; // 전체적용
+
+            foreach ($defaultData as $field) {
+                if (isset($posts['grp'][$field])) {
+                    $groupData[$field] = $posts[$field];
+                }
+                if (isset($posts['all'][$field])) {
+                    $allData[$field] = $posts[$field];
+                }
+            }
+
+            if ($groupData) {
+                $brdGroupData = $this->boardModel->findByGroup($boardData->board_group_id);
+                foreach ($brdGroupData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $groupData);
+                }
+            }
+            if ($allData) {
+                $brdAllData = $this->boardModel->all();
+                foreach ($brdAllData as $bKey => $bVal) {
+                    if ($bVal->id === $boardID) {
+                        continue;
+                    }
+                    $this->boardMetaModel->save($bVal->id, $allData);
+                }
+            }
+
+            $message = '게시판 - 작성 정보가 저장되었습니다.';
+            DB::commit();
+        } catch (Exception $e) {
+            $message = $e->getMessage();
+            DB::rollBack();
+        }
+
+        return redirect()->route('admin.board.board.write.show', $boardID)->with('message', $message);
+    }
+}

+ 87 - 0
app/Http/Controllers/Admin/Board/CommentController.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\Comment;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class CommentController extends Controller
+{
+    private Board $boardModel;
+    private Comment $commentModel;
+
+    public function __construct(Board $board, Comment $comment)
+    {
+        $this->boardModel = $board;
+        $this->commentModel = $comment;
+    }
+
+    /**
+     * 게시판 관리 > 댓글 관리
+     * @method GET
+     * @see /admin/board/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $commentData = $this->commentModel->data($params);
+
+        if ($commentData->rows > 0) {
+            $num = listNum($commentData->total, $params->page, $params->perPage);
+            foreach ($commentData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->page = $row->findPageNumber($row->board_id, $row->post_id, $row->id);
+                $row->device = (MAP_DEVICE_ICON_TYPE[$row->device_type] ?? '');
+
+                $commentData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.comment.index', [
+            'commentData' => $commentData,
+            'boardData' => $this->boardModel->all(),
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 게시판 관리 > 댓글 삭제
+     * @method DELETE
+     * @see /admin/board/comment/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+
+                $user = $request->user();
+                foreach ($chk as $commentID) {
+                    $comment = $this->commentModel->findOrNew($commentID);
+
+                    if(!$comment->exists) {
+                        throw new Exception($commentID . '번 댓글은 존재하지 않습니다.');
+                    }
+
+                    if (!$this->commentModel->remove($comment, $user)) {
+                        throw new Exception($commentID . "번 댓글을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '댓글이 삭제되었습니다.';
+            return redirect()->route('admin.board.comment.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 133 - 0
app/Http/Controllers/Admin/Board/File/DownloadController.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\File;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\PostFile;
+use App\Models\PostFileDownLog;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class DownloadController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostFile $postFileModel;
+    private PostFileDownLog $postFileDownloadLogModel;
+
+    public function __construct(Board $board, PostFile $postFile, PostFileDownLog $postFileDownLog)
+    {
+        $this->boardModel = $board;
+        $this->postFileModel = $postFile;
+        $this->postFileDownloadLogModel = $postFileDownLog;
+    }
+
+    /**
+     * 게시판 > 첨부파일 (다운로드)
+     * @method GET
+     * @see /admin/board/file/download
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postFileLogData = $this->postFileDownloadLogModel->data($params);
+
+        if ($postFileLogData->rows > 0) {
+            $num = listNum($postFileLogData->total, $params->page, $params->perPage);
+            foreach ($postFileLogData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->fileLink = route('admin.board.file.download.show', $row->post_file_id);
+                $row->editURL = route('admin.board.file.download.edit', $row->id);
+                $row->fileSize = byteFormat($row->file_size, 2);
+                $row->fileExists = file_exists($row->file_path);
+                $row->browser = $this->browser($row->user_agent);
+                $row->platform = $this->platform($row->user_agent);
+                $row->download = number_format($row->download);
+                $row->createdAt = dateBr($row->created_at);
+
+                $postFileLogData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.file.download.index', [
+            'postFileLogData' => $postFileLogData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 첨부파일 (다운로드) 삭제
+     * @method DELETE
+     * @see /admin/board/file/download/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postFileDownLogID) {
+                    $postFileDownloadLog = $this->postFileDownloadLogModel->findOrNew($postFileDownLogID);
+
+                    if(!$postFileDownloadLog->exists) {
+                        throw new Exception($postFileDownLogID . '번 다운로드 이력은 존재하지 않습니다.');
+                    }
+
+                    if(!$postFileDownloadLog->delete()) {
+                        throw new Exception($postFileDownLogID . '번 다운로드 기록을 삭제할 수 없습니다.');
+                    }
+                }
+            }
+
+            $message = '다운로드 기록이 삭제되었습니다.';
+            return redirect()->route('admin.board.file.download.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 게시판 > 첨부파일(다운로드) 실행
+     * @method GET
+     * @see /admin/board/file/download/show/{pk}
+     */
+    public function show(int $postFileID)
+    {
+        $fileModel = $this->postFileModel->findOrFail($postFileID);
+
+        if(!file_exists($fileModel->file_path)) {
+            return back()->with('message', '해당 파일이 존재하지 않습니다.');
+        }
+
+        return response()->download($fileModel->file_path, $fileModel->origin_name);
+    }
+
+    /**
+     * 게시판 > 첨부파일(다운로드) 상세 보기
+     * @method GET
+     * @see /admin/board/file/download/{pk}/edit
+     */
+    public function edit(int $postFileDownLogID)
+    {
+        $log = $this->postFileDownloadLogModel->findOrFail($postFileDownLogID);
+        $log->device = $this->device($log->user_agent);
+        $log->platform = $this->platform($log->user_agent);
+        $log->browser = $this->browser($log->user_agent);
+        $log->fileLink = route('admin.board.file.download.show', $postFileDownLogID);
+        $log->fileSize = byteFormat($log->postFile->file_size, 2);
+        $log->download = number_format($log->postFile->download);
+
+        return view('admin.board.file.download.edit', [
+            'log' => $log
+        ]);
+    }
+}

+ 111 - 0
app/Http/Controllers/Admin/Board/File/UploadController.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\File;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\PostFile;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class UploadController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostFile $postFileModel;
+
+    public function __construct(Board $board, PostFile $postFile)
+    {
+        $this->boardModel = $board;
+        $this->postFileModel = $postFile;
+    }
+
+    /**
+     * 게시판 > 첨부파일(Upload)
+     * @method GET
+     * @see /admin/board/file/upload
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postFileData = $this->postFileModel->data($params);
+
+        if ($postFileData->rows > 0) {
+            $num = listNum($postFileData->total, $params->page, $params->perPage);
+            foreach ($postFileData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->fileLink = route('admin.board.file.download.show', $row->id);
+                $row->editURL = route('admin.board.file.upload.edit', $row->id);
+                $row->fileSize = byteFormat($row->file_size, 2);
+                $row->fileExists = file_exists($row->file_path);
+                $row->download = number_format($row->download);
+
+                $postFileData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.file.upload.index', [
+            'postFileData' => $postFileData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 첨부파일(Upload) 삭제
+     * @method DELETE
+     * @see /admin/board/file/upload/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postFileID) {
+                    $postFile = $this->postFileModel->findOrNew($postFileID);
+
+                    if(!$postFile->exists) {
+                        throw new Exception($postFileID . '번 파일은 존재하지 않습니다.');
+                    }
+
+                    if(!$this->postFileModel->remove($postFileID)) {
+                        throw new Exception($postFileID . "번 파일을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '첨부파일이 삭제되었습니다.';
+            return redirect()->route('admin.board.file.upload.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 게시판 > 첨부파일(Upload) 상세 보기
+     * @method GET
+     * @see /admin/board/file/upload/{pk}/edit
+     */
+    public function edit(int $fileID)
+    {
+        $file = $this->postFileModel->findOrFail($fileID);
+        $file->device = $this->device($file->user_agent);
+        $file->platform = $this->platform($file->user_agent);
+        $file->browser = $this->browser($file->user_agent);
+        $file->fileLink = route('admin.board.file.download.show', $file->file_id);
+        $file->fileSize = byteFormat($file->file_size, 2);
+        $file->download = number_format($file->download);
+
+        return view('admin.board.file.upload.edit', [
+            'file' => $file
+        ]);
+    }
+}

+ 267 - 0
app/Http/Controllers/Admin/Board/Group/ListController.php

@@ -0,0 +1,267 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Group;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use Illuminate\Support\Facades\Validator;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardGroup;
+use App\Models\BoardGroupMeta;
+use App\Models\DTO\SearchData;
+use App\Models\DTO\ResponseData;
+use Exception;
+
+class ListController extends Controller
+{
+    private Board $boardModel;
+    private BoardGroup $boardGroupModel;
+    private BoardGroupMeta $boardGroupMetaModel;
+
+    public function __construct(Board $board, BoardGroup $boardGroup, BoardGroupMeta $boardGroupMeta)
+    {
+        $this->boardModel = $board;
+        $this->boardGroupModel = $boardGroup;
+        $this->boardGroupMetaModel = $boardGroupMeta;
+    }
+
+    /**
+     * 게시판 그룹 관리
+     * @method GET
+     * @see /admin/board/group/list
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $boardGroupData = $this->boardGroupModel->data($params);
+
+        if ($boardGroupData->rows > 0) {
+            $num = listNum($boardGroupData->total, $params->page, $params->perPage);
+            foreach ($boardGroupData->list as $i => $row) {
+                $row->num = $num--;
+                $row->boardRows = number_format($row->boardRows);
+                $row->postRows = number_format($row->postRows);
+                $row->commentRows = number_format($row->commentRows);
+                $row->createdAt = dateBr($row->created_at, '-');
+                $row->editURL = route('admin.board.group.list.edit', $row->id);
+
+                $boardGroupData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.group.index', [
+            'boardGroupData' => $boardGroupData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 등록
+     * @method GET
+     * @see /admin/board/group/list/create
+     */
+    public function create()
+    {
+        return view('admin.board.group.write', [
+            'actionURL' => route('admin.board.group.list.store'),
+            'boardGroupData' => [],
+            'boardGroupMetaData' => [],
+            'boardGroupID' => null
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 수정
+     * @method GET
+     * @see /admin/board/group/list/{pk}/edit
+     */
+    public function edit(int $boardGroupID)
+    {
+        return view('admin.board.group.write', [
+            'actionURL' => route('admin.board.group.list.update', $boardGroupID),
+            'boardGroupData' => $this->boardGroupModel->find($boardGroupID),
+            'boardGroupMetaData' => $this->boardGroupMetaModel->getAllMeta($boardGroupID),
+            'boardGroupID' => $boardGroupID
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 등록 저장
+     * @method POST
+     * @see /admin/board/group/list
+     */
+    public function store(Request $request, ResponseData $response)
+    {
+        $rules = [
+            'code' => [
+                'required',
+                'string',
+                'min:3',
+                'max:50',
+                'alpha_dash',
+            ],
+            'name' => 'required|max:255',
+            'sort' => 'required|numeric',
+            'pc_header_content' => 'string|nullable',
+            'pc_footer_content' => 'string|nullable',
+            'mobile_header_content' => 'string|nullable',
+            'mobile_footer_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'code' => '주소',
+            'name' => '제목',
+            'sort' => '순서',
+            'pc_header_content' => 'PC 상단 내용',
+            'pc_footer_content' => 'PC 하단 내용',
+            'mobile_header_content' => '모바일 상단 내용',
+            'mobile_footer_content' => '모바일 하단 내용'
+        ];
+
+        $this->validate($request, $rules, [], $attributes);
+
+        $result = $this->boardGroupModel->register($request, $response);
+
+        if(!$result->success) {
+            return back()->withErrors($result->message)->withInput();
+        }
+
+        $message = '게시판 그룹이 등록되었습니다.';
+        return redirect()->route('admin.board.group.list.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 수정 저장
+     * @method PUT
+     * @see /admin/board/group/list/{pk}
+     */
+    public function update(Request $request, ResponseData $response)
+    {
+        $boardGroupID = $request->post('board_group_id');
+
+        $rules = [
+            'board_group_id' => 'required|numeric|exists:tb_board_group,id',
+            'code' => [
+                'required',
+                'string',
+                'min:3',
+                'max:50',
+                'alpha_dash',
+                Rule::unique('tb_board_group', 'code')->ignore($boardGroupID, 'id')
+            ],
+            'name' => 'required|max:255',
+            'sort' => 'required|numeric',
+            'pc_header_content' => 'string|nullable',
+            'pc_footer_content' => 'string|nullable',
+            'mobile_header_content' => 'string|nullable',
+            'mobile_footer_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'board_group_id' => 'PK',
+            'code' => '주소',
+            'name' => '제목',
+            'sort' => '순서',
+            'pc_header_content' => 'PC 상단 내용',
+            'pc_footer_content' => 'PC 하단 내용',
+            'mobile_header_content' => '모바일 상단 내용',
+            'mobile_footer_content' => '모바일 하단 내용'
+        ];
+
+        $this->validate($request, $rules, [], $attributes);
+
+        $result = $this->boardGroupModel->updater($boardGroupID, $request, $response);
+
+        if(!$result->success) {
+            return back()->withErrors($result->message)->withInput();
+        }
+
+        $message = '게시판 그룹이 수정되었습니다.';
+        return redirect()->route('admin.board.group.list.edit', $boardGroupID)->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 수정
+     * @method POST
+     * @see /admin/board/group/list/listUpdate
+     */
+    public function listUpdate(Request $request)
+    {
+        $posts = $request->all();
+
+        if($posts['chk']) {
+            foreach($posts['chk'] as $boardGroupID) {
+                $updateData = [
+                    'code' => ($posts['code'][$boardGroupID] ?? ''),
+                    'name' => ($posts['name'][$boardGroupID] ?? ''),
+                    'sort' => ($posts['sort'][$boardGroupID] ?? ''),
+                ];
+
+                $rules = [
+                    'code' => [
+                        'required',
+                        'string',
+                        'min:3',
+                        'max:50',
+                        'alpha_dash',
+                        Rule::unique('tb_board_group', 'code')->ignore($boardGroupID, 'id')
+                    ],
+                    'name' => 'required|max:255',
+                    'sort' => 'required|numeric'
+                ];
+
+                $attributes = [
+                    'code' => 'CODE',
+                    'name' => '제목',
+                    'sort' => '순서'
+                ];
+
+                $validator = Validator::make($updateData, $rules, [], $attributes);
+                if($validator->fails()) {
+                    return back()->withErrors($validator)->withInput();
+                }
+
+                $this->boardGroupModel->find($boardGroupID)->update($updateData);
+            }
+        }
+
+        $message = '게시판 그룹 정보가 변경되었습니다.';
+        return redirect()->route('admin.board.group.list.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 삭제
+     * @method DELETE
+     * @see /admin/board/group/list/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $boardGroupID) {
+
+                    $boardGroup = $this->boardGroupModel->findOrNew($boardGroupID);
+
+                    if (!$boardGroup->exists) {
+                        throw new Exception($boardGroupID . '번 게시판 그룹은 존재하지 않습니다.');
+                    }
+
+                    if ($boardGroup->board->count() > 0) {
+                        return back()->withErrors($boardGroupID . "번 게시판 그룹은 삭제할 수 없습니다.")->withInput();
+                    }
+
+                    $this->boardGroupModel->remove($boardGroupID);
+                }
+            }
+
+            $message = '게시판 그룹이 삭제되었습니다.';
+            return redirect()->route('admin.board.group.list.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 101 - 0
app/Http/Controllers/Admin/Board/Group/ManagerController.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Group;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\BoardGroupAdmin;
+
+class ManagerController extends Controller
+{
+    private BoardGroupAdmin $boardGroupAdminModel;
+    private User $userModel;
+
+    public function __construct(BoardGroupAdmin $boardGroupAdmin, User $user)
+    {
+        $this->boardGroupAdminModel = $boardGroupAdmin;
+        $this->userModel = $user;
+    }
+
+    /**
+     * 게시판 그룹 관리자
+     * @method GET
+     * @see /admin/board/group/manager
+     */
+    public function show(int $boardGroupID)
+    {
+        $boardGroupAdminData = $this->boardGroupAdminModel->data();
+
+        if ($boardGroupAdminData->rows > 0) {
+            foreach ($boardGroupAdminData->list as $i => $row) {
+                $boardGroupAdminData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.group.manager', [
+            'actionURL' => route('admin.board.group.manager.store'),
+            'boardGroupAdminData' => $boardGroupAdminData,
+            'boardGroupID' => $boardGroupID
+        ]);
+    }
+
+    /**
+     * 게시판 그룹 관리자 등록 저장
+     * @method POST
+     * @see /admin/board/group/manager
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'board_group_id' => 'required|numeric|exists:tb_board_group,id',
+            'sid' => 'required|exists:users,sid'
+        ];
+
+        $attributes = [
+            'board_group_id' => '게시판 그룹 PK',
+            'sid' => '회원 ID'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $boardGroupID = $posts['board_group_id'];
+        $sid = $posts['sid'];
+        $userID = $this->userModel->where('sid', $sid)->value('id');
+
+        // 이미 등록된 관리자인지 확인
+        $isBoardAdmin = $this->boardGroupAdminModel->where('user_id', $userID)->count();
+        if($isBoardAdmin) {
+            return back()->withErrors(['sid' => "{$sid}는 이미 등록된 관리자 입니다."])->withInput();
+        }
+
+        $this->boardGroupAdminModel->insert([
+            'board_group_id' => $boardGroupID,
+            'user_id' => $userID,
+            'created_at' => now()
+        ]);
+
+        $message = '게시판 그룹 관리자가 새로 등록되었습니다.';
+        return redirect()->route('admin.board.group.manager.show', $boardGroupID)->with('message', $message);
+    }
+
+    /**
+     * 게시판 그룹 관리자 삭제
+     * @method DELETE
+     * @see /admin/board/group/manager/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $boardGroupID = $request->post('board_group_id');
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $boardGroupAdminID) {
+                $this->boardGroupAdminModel->find($boardGroupAdminID)->delete();
+            }
+        }
+
+        $message = '게시판 그룹 관리자 정보가 삭제되었습니다.';
+        return redirect()->route('admin.board.group.manager.show', $boardGroupID)->with('message', $message);
+    }
+}

+ 83 - 0
app/Http/Controllers/Admin/Board/History/CommentController.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\History;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\CommentHistory;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class CommentController extends Controller
+{
+    private Board $boardModel;
+    private CommentHistory $commentHistoryModel;
+
+    public function __construct(Board $board, CommentHistory $commentHistory)
+    {
+        $this->boardModel = $board;
+        $this->commentHistoryModel = $commentHistory;
+    }
+
+    /**
+     * 게시판 > 댓글 변경 이력
+     * @method GET
+     * @see /admin/board/history/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $commentHistoryData = $this->commentHistoryModel->data($params);
+
+        if ($commentHistoryData->rows > 0) {
+            $num = listNum($commentHistoryData->total, $params->page, $params->perPage);
+            foreach ($commentHistoryData->list as $i => $row) {
+                $row->num = $num--;
+                $row->createdAt = dateBr($row->created_at, '-');
+
+                $commentHistoryData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.history.comment', [
+            'commentHistoryData' => $commentHistoryData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 댓글 변경 이력 삭제
+     * @method DELETE
+     * @see /admin/board/history/comment/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $i => $historyID) {
+
+                    $commentHistory = $this->commentHistoryModel->findOrNew($historyID);
+
+                    if(!$commentHistory->exists) {
+                        throw new Exception($i . "번 변경 이력이 존재하지 않습니다.");
+                    }
+
+                    if(!$commentHistory->delete()) {
+                        throw new Exception($i . "번 변경 이력을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '댓글 변경 이력이 삭제되었습니다.';
+            return redirect()->route('admin.board.history.comment.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 83 - 0
app/Http/Controllers/Admin/Board/History/PostController.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\History;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\PostHistory;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class PostController extends Controller
+{
+    private Board $boardModel;
+    private PostHistory $postHistoryModel;
+
+    public function __construct(Board $board, PostHistory $postHistory)
+    {
+        $this->boardModel = $board;
+        $this->postHistoryModel = $postHistory;
+    }
+
+    /**
+     * 게시판 >  게시글 변경 이력
+     * @method GET
+     * @see /admin/board/history/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postHistoryData = $this->postHistoryModel->data($params);
+
+        if ($postHistoryData->rows > 0) {
+            $num = listNum($postHistoryData->total, $params->page, $params->perPage);
+            foreach ($postHistoryData->list as $i => $row) {
+                $row->num = $num--;
+                $row->createdAt = dateBr($row->created_at, '-');
+
+                $postHistoryData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.history.post', [
+            'postHistoryData' => $postHistoryData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 게시글 변경 이력 삭제
+     * @method DELETE
+     * @see /admin/board/history/post/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $i => $historyID) {
+
+                    $postHistory = $this->postHistoryModel->findOrNew($historyID);
+
+                    if (!$postHistory->exists) {
+                        throw new Exception($i . '번 변경 이력이 존재하지 않습니다.');
+                    }
+
+                    if (!$postHistory->delete()) {
+                        throw new Exception($i . "번 변경 이력을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '게시물 변경 이력이 삭제되었습니다.';
+            return redirect()->route('admin.board.history.post.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 123 - 0
app/Http/Controllers/Admin/Board/ImageController.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\EditorImage;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class ImageController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private EditorImage $editorImageModel;
+
+    public function __construct(Board $board, EditorImage $editorImage)
+    {
+        $this->boardModel = $board;
+        $this->editorImageModel = $editorImage;
+    }
+
+    /**
+     * 게시판 > 이미지 관리
+     * @method GET
+     * @see /admin/board/image
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $editorImageData = $this->editorImageModel->data($params);
+
+        if ($editorImageData->rows > 0) {
+            $num = listNum($editorImageData->total, $params->page, $params->perPage);
+            foreach ($editorImageData->list as $i => $row) {
+                $row->num = $num--;
+                $row->fileExists = file_exists($row->file_path);
+                $row->targetStr = MAP_EDITOR_IMG_TYPE[$row->target_type];
+                $row->editURL = route('admin.board.image.edit', $row->id);
+                $row->fileLink = route('admin.board.image.show', $row->id);
+
+                $editorImageData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.image.index', [
+            'editorImageData' => $editorImageData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 게시판 > 이미지 관리 삭제
+     * @method DELETE
+     * @see /admin/board/image/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+            $fileName = $request->post('file_name');
+
+            if ($chk) {
+                foreach ($chk as $editorImageID) {
+                    $editorImage = $this->editorImageModel->findOrNew($editorImageID);
+
+                    if(!$editorImage->exists) {
+                        throw new Exception($editorImageID . '번 이미지는 존재하지 않습니다.');
+                    }
+
+                    if(!$this->editorImageModel->remove($editorImageID)) {
+                        throw new Exception($editorImageID . '번 ' . $fileName[$editorImageID] . " 파일을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '에디터 이미지가 삭제되었습니다.';
+            return redirect()->route('admin.board.image.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 게시판 > 이미지 관리 상세보기
+     * @method GET
+     * @see /admin/board/image/show/{pk}
+     */
+    public function show(int $editorImageID)
+    {
+        $imageModel = $this->editorImageModel->findOrFail($editorImageID);
+
+        if(!file_exists($imageModel->file_path)) {
+            return back()->with('message', '해당 파일이 존재하지 않습니다.');
+        }
+
+        return response()->download($imageModel->file_path, $imageModel->origin_name);
+    }
+
+    /**
+     * 게시판 > 이미지 정보 상세 보기
+     * @method GET
+     * @see /admin/board/image/{pk}/edit
+     */
+    public function edit(int $editorImageID)
+    {
+        $image = $this->editorImageModel->findOrFail($editorImageID);
+        $image->device = $this->device($image->user_agent);
+        $image->platform = $this->platform($image->user_agent);
+        $image->browser = $this->browser($image->user_agent);
+        $image->fileLink = route('admin.board.image.show', $image->id);
+        $image->fileSize = byteFormat($image->file_size, 2);
+        $image->download = number_format($image->download);
+
+        return view('admin.board.image.edit', [
+            'image' => $image
+        ]);
+    }
+}

+ 103 - 0
app/Http/Controllers/Admin/Board/Like/CommentController.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Like;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\Comment;
+use App\Models\CommentLike;
+use App\Models\DTO\SearchData;
+
+class CommentController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private CommentLike $commentLikeModel;
+
+    public function __construct(Board $board, CommentLike $commentLike)
+    {
+        $this->boardModel = $board;
+        $this->commentLikeModel = $commentLike;
+    }
+
+    /**
+     * 댓글 > 추천/비추 조회
+     * @method GET
+     * @see /admin/board/like/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->post('board_id');
+
+        $likeData = $this->commentLikeModel->data($params);
+
+        if ($likeData->rows > 0) {
+            $num = listNum($likeData->total, $params->page, $params->perPage);
+            foreach ($likeData->list as $i => $row) {
+                $row->num = $num--;
+                $row->status = ($row->type == 1 ? '추천' : '비추천');
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->editURL = route('admin.board.like.comment.edit', $row->id);
+
+                $likeData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.like.comment.index', [
+            'likeData' => $likeData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 댓글 > 추천/비추 삭제
+     * @method DELETE
+     * @see /admin/board/like/comment/destroy
+     */
+    public function destroy(Request $request, Comment $commentModel)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $i => $likeID) {
+                $like = $this->commentLikeModel->find($likeID);
+
+                // 추천 수 감소
+                if($like->type == 1) {
+                    $commentModel->decreaseLike($like->comment_id);
+                }else{
+                    $commentModel->decreaseDisLike($like->comment_id);
+                }
+
+                if(!$like->delete()) {
+                    return back()->withErrors("{$i}번 추천/비추 기록을 삭제할 수 없습니다.")->withInput();
+                }
+            }
+        }
+
+        $message = '댓글 추천/비추 기록이 삭제되었습니다.';
+        return redirect()->route('admin.board.like.comment.index')->with('message', $message);
+    }
+
+    /**
+     * 댓글 > 추천/비추천 상세 보기
+     * @method GET
+     * @see /admin/board/like/{pk}/edit
+     */
+    public function edit(int $likeID)
+    {
+        $like = $this->commentLikeModel->findOrFail($likeID);
+        $like->device = $this->device($like->user_agent);
+        $like->platform = $this->platform($like->user_agent);
+        $like->browser = $this->browser($like->user_agent);
+
+        return view('admin.board.like.comment.edit', [
+            'like' => $like
+        ]);
+    }
+}

+ 112 - 0
app/Http/Controllers/Admin/Board/Like/PostController.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Like;
+
+use Illuminate\Http\Request;
+use App\Http\Traits\AgentTrait;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\Post;
+use App\Models\PostLike;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class PostController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostLike $postLikeModel;
+
+    public function __construct(Board $board, PostLike $postLike)
+    {
+        $this->boardModel = $board;
+        $this->postLikeModel = $postLike;
+    }
+
+    /**
+     * 게시판 > 추천/비추 조회
+     * @method GET
+     * @see /admin/board/like/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->post('board_id');
+
+        $likeData = $this->postLikeModel->data($params);
+
+        if ($likeData->rows > 0) {
+            $num = listNum($likeData->total, $params->page, $params->perPage);
+            foreach ($likeData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->editURL = route('admin.board.like.post.edit', $row->id);
+
+                $likeData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.like.post.index', [
+            'likeData' => $likeData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 추천/비추 삭제
+     * @method DELETE
+     * @see /admin/board/like/post/destroy
+     */
+    public function destroy(Request $request, Post $postModel)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postLikeID) {
+                    $postLike = $this->postLikeModel->findOrNew($postLikeID);
+
+                    if(!$postLike->exists) {
+                        throw new Exception($postLikeID . '번 추천/비추 기록이 존재하지 않습니다.');
+                    }
+
+                    if(!$postLike->delete()) {
+                        throw new Exception($postLikeID . '번 추천/비추 기록을 삭제할 수 없습니다.');
+                    }
+
+                    // 게시글 추천/비추천 개수 갱신
+                    if($postLike->type == 1) {
+                        $postModel->decreaseLike($postLike->post_id);
+                    }else{
+                        $postModel->decreaseDisLike($postLike->post_id);
+                    }
+                }
+            }
+
+            $message = '게시글 추천/비추 기록이 삭제되었습니다.';
+            return redirect()->route('admin.board.like.post.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+
+    /**
+     * 게시판 > 추천/비추천 상세 보기
+     * @method GET
+     * @see /admin/board/like/{pk}/edit
+     */
+    public function edit(int $postLikeID)
+    {
+        $like = $this->postLikeModel->findOrFail($postLikeID);
+        $like->device = $this->device($like->user_agent);
+        $like->platform = $this->platform($like->user_agent);
+        $like->browser = $this->browser($like->user_agent);
+
+        return view('admin.board.like.post.edit', [
+            'like' => $like
+        ]);
+    }
+}

+ 84 - 0
app/Http/Controllers/Admin/Board/Link/ListController.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Link;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\PostLink;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class ListController extends Controller
+{
+    private Board $boardModel;
+    private PostLink $postLinkModel;
+
+    public function __construct(Board $board, PostLink $postLink)
+    {
+        $this->boardModel = $board;
+        $this->postLinkModel = $postLink;
+    }
+
+    /**
+     * 게시판 > URL 클릭 > 목록
+     * @method GET
+     * @see /admin/board/link/list
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postLinkData = $this->postLinkModel->data($params);
+
+        if ($postLinkData->rows > 0) {
+            $num = listNum($postLinkData->total, $params->page, $params->perPage);
+            foreach ($postLinkData->list as $i => $row) {
+                $row->num = $num--;
+                $row->hits = number_format($row->hits);
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+
+                $postLinkData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.link.list', [
+            'postLinkData' => $postLinkData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > URL 클릭 > 목록 삭제
+     * @method DELETE
+     * @see /admin/board/link/list/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postLinkID) {
+                    $postLink = $this->postLinkModel->findOrNew($postLinkID);
+
+                    if(!$postLink->exists) {
+                        throw new Exception($postLinkID . '번 URL은 존재하지 않습니다.');
+                    }
+
+                    if(!$postLink->remove($postLinkID)) {
+                        throw new Exception($postLinkID . "번 URL은 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = 'URL이 삭제되었습니다.';
+            return redirect()->route('admin.board.link.list.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 89 - 0
app/Http/Controllers/Admin/Board/Link/LogController.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Link;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\PostLinkClickLog;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class LogController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostLinkClickLog $postLinkClickLogModel;
+
+    public function __construct(Board $board, PostLinkClickLog $postLinkClickLog)
+    {
+        $this->boardModel = $board;
+        $this->postLinkClickLogModel = $postLinkClickLog;
+    }
+
+    /**
+     * 게시판 > URL 클릭 > 기록
+     * @method GET
+     * @see /admin/board/link/log
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postLinkClickLogData = $this->postLinkClickLogModel->data($params);
+
+        if ($postLinkClickLogData->rows > 0) {
+            $num = listNum($postLinkClickLogData->total, $params->page, $params->perPage);
+            foreach ($postLinkClickLogData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+                $row->device = $this->device($row->user_agent);
+                $row->browser = $this->browser($row->user_agent);
+                $row->platform = $this->platform($row->user_agent);
+
+                $postLinkClickLogData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.link.log', [
+            'postLinkClickLogData' => $postLinkClickLogData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > URL 클릭 기록 삭제
+     * @method DELETE
+     * @see /admin/board/link/log/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $postLinkClickLogID) {
+                    $postLinkClickLog = $this->postLinkClickLogModel->findOrNew($postLinkClickLogID);
+
+                    if(!$postLinkClickLog->exists) {
+                        throw new Exception($postLinkClickLogID . "번 URL 클릭 기록은 존재하지 않습니다.");
+                    }
+
+                    if(!$postLinkClickLog->delete()) {
+                        throw new Exception($postLinkClickLogID . '번 URL 클릭 기록을 삭제할 수 없습니다.');
+                    }
+                }
+            }
+
+            $message = 'URL 클릭 기록이 삭제되었습니다.';
+            return redirect()->route('admin.board.link.log.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 93 - 0
app/Http/Controllers/Admin/Board/PostController.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\BoardCategory;
+use App\Models\Post;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class PostController extends Controller
+{
+    private Board $boardModel;
+    private BoardCategory $boardCategoryModel;
+    private Post $postModel;
+
+    public function __construct(Board $board, BoardCategory $boardCategory, Post $post)
+    {
+        $this->boardModel = $board;
+        $this->boardCategoryModel = $boardCategory;
+        $this->postModel = $post;
+    }
+
+    /**
+     * 게시판 > 게시글 관리
+     * @method GET
+     * @see /admin/board/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postData = $this->postModel->data($params);
+
+        if ($postData->rows > 0) {
+            $num = listNum($postData->total, $params->page, $params->perPage);
+            foreach ($postData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->id]);
+                $row->hits = number_format($row->hit);
+                $row->hvFile = ($row->file_rows > 0);
+                $row->hvImage = ($row->image_rows > 0);
+                $row->device = MAP_DEVICE_ICON_TYPE[$row->device_type];
+                $row->secret = ($row->is_secret > 0);
+
+                $postData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.post.index', [
+            'postData' => $postData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시글 관리 > 게시물 삭제
+     * @method DELETE
+     * @see /admin/board/post/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+
+                $user = $request->user();
+                foreach ($chk as $postID) {
+                    $post = $this->postModel->findOrNew($postID);
+
+                    if(!$post->exists) {
+                        throw new Exception($postID . '번 게시물은 존재하지 않습니다.');
+                    }
+
+                    if (!$this->postModel->remove($post, $user)) {
+                        throw new Exception($postID . "번 게시물을 삭제할 수 없습니다.");
+                    }
+                }
+            }
+
+            $message = '게시물이 삭제되었습니다.';
+            return redirect()->route('admin.board.post.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 87 - 0
app/Http/Controllers/Admin/Board/TagController.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Board;
+use App\Models\Post;
+use App\Models\PostTag;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class TagController extends Controller
+{
+    private Board $boardModel;
+    private PostTag $postTagModel;
+
+    public function __construct(Board $board, PostTag $postTag)
+    {
+        $this->boardModel = $board;
+        $this->postTagModel = $postTag;
+    }
+
+    /**
+     * 게시판 >  태그 관리
+     * @method GET
+     * @see /admin/board/tag
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postTagData = $this->postTagModel->data($params);
+
+        if ($postTagData->rows > 0) {
+            $num = listNum($postTagData->total, $params->page, $params->perPage);
+            foreach ($postTagData->list as $i => $row) {
+                $row->num = $num--;
+                $row->postURL = route('board.post.view', [$row->code, $row->post_id]);
+
+                $postTagData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.tag.index', [
+            'postTagData' => $postTagData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 태그 삭제
+     * @method DELETE
+     * @see /admin/board/tag/destroy
+     */
+    public function destroy(Request $request, Post $postModel)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $tagID) {
+                    $postTag = $this->postTagModel->findOrNew($tagID);
+
+                    if (!$postTag->exists) {
+                        throw new Exception($tagID . '번 태그는 존재하지 않습니다.');
+                    }
+
+                    if (!$postTag->delete()) {
+                        throw new Exception($tagID . "번 태그를 삭제할 수 없습니다.");
+                    }
+
+                    // 게시글 태그 수 갱신
+                    $postModel->updateTagRows($postTag->post_id);
+                }
+            }
+
+            $message = '태그가 삭제되었습니다.';
+            return redirect()->route('admin.board.tag.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 127 - 0
app/Http/Controllers/Admin/Board/Trash/CommentController.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Trash;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\CommentDeleted;
+use App\Models\DTO\SearchData;
+
+class CommentController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private CommentDeleted $commentDeletedModel;
+
+    public function __construct(Board $board, CommentDeleted $commentDeleted)
+    {
+        $this->boardModel = $board;
+        $this->commentDeletedModel = $commentDeleted;
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(댓글)
+     * @method GET
+     * @see /admin/board/trash/comment
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $commentDeletedData = $this->commentDeletedModel->data($params);
+
+        if ($commentDeletedData->rows > 0) {
+            $num = listNum($commentDeletedData->total, $params->page, $params->perPage);
+            foreach ($commentDeletedData->list as $i => $row) {
+                $row->num = $num--;
+                $row->editURL = route('admin.board.trash.comment.edit', $row->id);
+
+                $commentDeletedData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.trash.comment.index', [
+            'commentDeletedData' => $commentDeletedData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(댓글) > 전체삭제
+     * @method GET
+     * @see /admin/board/trash/comment/truncate
+     */
+    public function truncate()
+    {
+        if(!$this->commentDeletedModel->truncate()) {
+            return back()->withErrors("휴지통 전체 삭제에 실패하였습니다.")->withInput();
+        }
+
+        $message = '휴지통에 댓글을 전체 삭제하였습니다.';
+        return redirect()->route('admin.board.trash.comment.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(댓글) > 선택삭제
+     * @method GET
+     * @see /admin/board/trash/comment/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $deletedID) {
+                if(!$this->commentDeletedModel->remove($deletedID)) {
+                    return back()->withErrors($deletedID . "번 댓글을 삭제할 수 없습니다.")->withInput();
+                }
+            }
+        }
+
+        $message = '댓글이 삭제되었습니다.';
+        return redirect()->route('admin.board.trash.comment.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(댓글) > 복원하기
+     * @method POST
+     * @see /admin/board/trash/comment/recovery
+     */
+    public function recovery(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $deletedID) {
+                if(!$this->commentDeletedModel->recovery($deletedID)) {
+                    return back()->withErrors($deletedID . "번 댓글을 복원할 수 없습니다.")->withInput();
+                }
+            }
+        }
+
+        $message = '댓글이 복원되었습니다.';
+        return redirect()->route('admin.board.trash.comment.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(댓글) > 상세보기
+     * @method GET
+     * @see /admin/board/trash/comment/{pk}/edit
+     */
+    public function edit(int $deletedID)
+    {
+        $deleted = $this->commentDeletedModel->findOrFail($deletedID);
+        $deleted->device = $this->device($deleted->user_agent);
+        $deleted->platform = $this->platform($deleted->user_agent);
+        $deleted->browser = $this->browser($deleted->user_agent);
+
+        return view('admin.board.trash.comment.edit', [
+            'deleted' => $deleted
+        ]);
+    }
+}

+ 127 - 0
app/Http/Controllers/Admin/Board/Trash/PostController.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Board\Trash;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\AgentTrait;
+use App\Models\Board;
+use App\Models\PostDeleted;
+use App\Models\DTO\SearchData;
+
+class PostController extends Controller
+{
+    use AgentTrait;
+
+    private Board $boardModel;
+    private PostDeleted $postDeletedModel;
+
+    public function __construct(Board $board, PostDeleted $postDeleted)
+    {
+        $this->boardModel = $board;
+        $this->postDeletedModel = $postDeleted;
+    }
+
+    /**
+     * 게시판 > 휴지통 관리 (게시글)
+     * @method GET
+     * @see /admin/board/trash/post
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->boardID = $request->get('board_id');
+
+        $postDeletedData = $this->postDeletedModel->data($params);
+
+        if ($postDeletedData->rows > 0) {
+            $num = listNum($postDeletedData->total, $params->page, $params->perPage);
+            foreach ($postDeletedData->list as $i => $row) {
+                $row->num = $num--;
+                $row->editURL = route('admin.board.trash.post.edit', $row->id);
+
+                $postDeletedData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.board.trash.post.index', [
+            'postDeletedData' => $postDeletedData,
+            'params' => $params,
+            'boardData' => $this->boardModel->all()
+        ]);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(게시글) > 전체삭제
+     * @method GET
+     * @see /admin/board/trash/post/truncate
+     */
+    public function truncate()
+    {
+        if(!$this->postDeletedModel->truncate()) {
+            return back()->withErrors("휴지통 전체 삭제에 실패하였습니다.")->withInput();
+        }
+
+        $message = '휴지통에 게시글을 전체 삭제하였습니다.';
+        return redirect()->route('admin.board.trash.post.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(게시글) > 선택삭제
+     * @method GET
+     * @see /admin/board/trash/post/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $deletedID) {
+                if(!$this->postDeletedModel->remove($deletedID)) {
+                    return back()->withErrors($deletedID . "번 게시글을 삭제할 수 없습니다.")->withInput();
+                }
+            }
+        }
+
+        $message = '게시글이 삭제되었습니다.';
+        return redirect()->route('admin.board.trash.post.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(게시글) > 복원하기
+     * @method POST
+     * @see /admin/board/trash/post/recovery
+     */
+    public function recovery(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $deletedID) {
+                if(!$this->postDeletedModel->recovery($deletedID)) {
+                    return back()->withErrors($deletedID . "번 게시글을 복원할 수 없습니다.")->withInput();
+                }
+            }
+        }
+
+        $message = '게시글이 복원되었습니다.';
+        return redirect()->route('admin.board.trash.post.index')->with('message', $message);
+    }
+
+    /**
+     * 게시판 > 휴지통 관리(게시글) > 상세보기
+     * @method GET
+     * @see /admin/board/trash/post/{pk}/edit
+     */
+    public function edit(int $deletedID)
+    {
+        $deleted = $this->postDeletedModel->findOrFail($deletedID);
+        $deleted->device = $this->device($deleted->user_agent);
+        $deleted->platform = $this->platform($deleted->user_agent);
+        $deleted->browser = $this->browser($deleted->user_agent);
+
+        return view('admin.board.trash.post.edit', [
+            'deleted' => $deleted
+        ]);
+    }
+}

+ 112 - 0
app/Http/Controllers/Admin/Config/Form/EmailController.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class EmailController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 이메일 양식
+     * @method GET
+     * @see /admin/config/form/email
+     */
+    public function index()
+    {
+        return view('admin.config.form.email', [
+            'config' => $this->configModel
+        ]);
+    }
+
+    /**
+     * 이메일 양식 저장
+     * @method POST
+     * @see /admin/config/form/email
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_email_register_form_title' => 'string|nullable',
+            'send_email_register_form_content' => 'string|nullable',
+            'send_email_changepw_form_title' => 'string|nullable',
+            'send_email_changepw_form_content' => 'string|nullable',
+            'send_email_withdraw_form_title' => 'string|nullable',
+            'send_email_withdraw_form_content' => 'string|nullable',
+            /*
+            'send_email_auth_form_title' => 'string|nullable',
+            'send_email_auth_form_content' => 'string|nullable',
+            'send_email_verify_code_form_title' => 'string|nullable',
+            'send_email_verify_code_form_content' => 'string|nullable',
+            */
+            'send_email_find_form_title' => 'string|nullable',
+            'send_email_find_form_content' => 'string|nullable',
+            'send_email_post_form_title' => 'string|nullable',
+            'send_email_post_form_content' => 'string|nullable',
+            'send_email_comment_form_title' => 'string|nullable',
+            'send_email_comment_form_content' => 'string|nullable',
+            'send_email_post_blame_form_title' => 'string|nullable',
+            'send_email_post_blame_form_content' => 'string|nullable',
+            'send_email_comment_blame_form_title' => 'string|nullable',
+            'send_email_comment_blame_form_content' => 'string|nullable',
+            'send_email_post_personal_form_title' => 'string|nullable',
+            'send_email_post_personal_form_content' => 'string|nullable',
+            'send_email_post_personal_reply_form_title' => 'string|nullable',
+            'send_email_post_personal_reply_form_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'send_email_register_form_title' => '회원가입 - 제목',
+            'send_email_register_form_content' => '회원가입 - 내용',
+            'send_email_changepw_form_title' => '비밀번호 변경 - 제목',
+            'send_email_changepw_form_content' => '비밀번호 변경 - 내용',
+            'send_email_withdraw_form_title' => '회원탈퇴 - 제목',
+            'send_email_withdraw_form_content' => '회원탈퇴 - 내용',
+            /*
+            'send_email_auth_form_title' => '이메일 인증',
+            'send_email_auth_form_content' => '이메일 인증',
+            'send_email_verify_code_form_title' => '인증번호 - 제목',
+            'send_email_verify_code_form_content' => '인증번호 - 내용',
+            */
+            'send_email_find_form_title' => '회원정보 찾기 - 제목',
+            'send_email_find_form_content' => '회원정보 찾기 - 내용',
+            'send_email_post_form_title' => '게시글 작성 - 제목',
+            'send_email_post_form_content' => '게시글 작성 - 내용',
+            'send_email_comment_form_title' => '댓글 작성 - 제목',
+            'send_email_comment_form_content' => '댓글 작성 - 내용',
+            'send_email_post_blame_form_title' => '게시글 신고 - 제목',
+            'send_email_post_blame_form_content' => '게시글 신고 - 내용',
+            'send_email_comment_blame_form_title' => '댓글 신고 - 제목',
+            'send_email_comment_blame_form_content' => '댓글 신고 - 내용',
+            'send_email_post_personal_form_title' => '1:1 문의 접수 - 제목',
+            'send_email_post_personal_form_content' => '1:1 문의 접수 - 내용',
+            'send_email_post_personal_reply_form_title' => '1:1 문의 답변 - 제목',
+            'send_email_post_personal_reply_form_content' => '1:1 문의 답변 - 내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '이메일 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.form.email.index')->with('message', $message);
+    }
+
+    /**
+     * 이메일 레이아웃 보기
+     * @method GET
+     * @see /admin/config/form/email/layout
+     */
+    public function layout()
+    {
+        return view('component.email');
+    }
+}

+ 70 - 0
app/Http/Controllers/Admin/Config/Form/NoteController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class NoteController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 쪽지 양식
+     * @method GET
+     * @see /admin/config/form/note
+     */
+    public function index()
+    {
+        return view('admin.config.form.note', []);
+    }
+
+    /**
+     * 쪽지 양식 저장
+     * @method POST
+     * @see /admin/config/form/note
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_note_register_form_content' => 'string|nullable',
+            'send_note_changepw_form_content' => 'string|nullable',
+            'send_note_withdraw_form_content' => 'string|nullable',
+            'send_note_auth_form_content' => 'string|nullable',
+            'send_note_find_form_content' => 'string|nullable',
+            'send_note_post_form_content' => 'string|nullable',
+            'send_note_comment_form_content' => 'string|nullable',
+            'send_note_post_blame_form_content' => 'string|nullable',
+            'send_note_comment_blame_form_content' => 'string|nullable',
+            'send_note_post_personal_form_content' => 'string|nullable',
+            'send_note_post_personal_reply_form_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'send_note_register_form_content' => '회원가입',
+            'send_note_changepw_form_content' => '비밀번호 변경',
+            'send_note_withdraw_form_content' => '회원탈퇴',
+            'send_note_auth_form_content' => '이메일 인증',
+            'send_note_find_form_content' => '회원정보 찾기',
+            'send_note_post_form_content' => '게시글 작성',
+            'send_note_comment_form_content' => '댓글 작성',
+            'send_note_post_blame_form_content' => '게시글 신고',
+            'send_note_comment_blame_form_content' => '댓글 신고',
+            'send_note_post_personal_form_content' => '1:1 문의 접수',
+            'send_note_post_personal_reply_form_content' => '1:1 문의 답변'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '쪽지 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.form.note.index')->with('message', $message);
+    }
+}

+ 70 - 0
app/Http/Controllers/Admin/Config/Form/SmsController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class SmsController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 문자 양식
+     * @method GET
+     * @see /admin/config/form/sms
+     */
+    public function index()
+    {
+        return view('admin.config.form.sms', []);
+    }
+
+    /**
+     * 문자 양식 저장
+     * @method POST
+     * @see /admin/config/form/sms
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_sms_register_form_content' => 'string|nullable',
+            'send_sms_changepw_form_content' => 'string|nullable',
+            'send_sms_withdraw_form_content' => 'string|nullable',
+            'send_sms_auth_form_content' => 'string|nullable',
+            'send_sms_find_form_content' => 'string|nullable',
+            'send_sms_post_form_content' => 'string|nullable',
+            'send_sms_comment_form_content' => 'string|nullable',
+            'send_sms_post_blame_form_content' => 'string|nullable',
+            'send_sms_comment_blame_form_content' => 'string|nullable',
+            'send_sms_post_personal_form_content' => 'string|nullable',
+            'send_sms_post_personal_reply_form_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'send_sms_register_form_content' => '회원가입',
+            'send_sms_changepw_form_content' => '비밀번호 변경',
+            'send_sms_withdraw_form_content' => '회원탈퇴',
+            'send_sms_auth_form_content' => '이메일 인증',
+            'send_sms_find_form_content' => '회원정보 찾기',
+            'send_sms_post_form_content' => '게시글 작성',
+            'send_sms_comment_form_content' => '댓글 작성',
+            'send_sms_post_blame_form_content' => '게시글 신고',
+            'send_sms_comment_blame_form_content' => '댓글 신고',
+            'send_sms_post_personal_form_content' => '1:1 문의 접수 - 게시글',
+            'send_sms_post_personal_reply_form_content' => '1:1 문의 답변 - 댓글'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '문자 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.form.sms.index')->with('message', $message);
+    }
+}

+ 58 - 0
app/Http/Controllers/Admin/Config/Form/TelegramController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class TelegramController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 문자 양식
+     * @method GET
+     * @see /admin/config/form/email
+     */
+    public function index()
+    {
+        return view('admin.config.form.telegram', []);
+    }
+
+    /**
+     * 문자 양식 저장
+     * @method POST
+     * @see /admin/config/form/email
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_sms_register_form_content' => 'string|nullable',
+            'send_sms_changepw_form_content' => 'string|nullable',
+            'send_sms_withdraw_form_content' => 'string|nullable',
+            'send_sms_auth_form_content' => 'string|nullable',
+            'send_sms_find_form_content' => 'string|nullable',
+            'send_sms_post_form_content' => 'string|nullable',
+            'send_sms_post_comment_form_content' => 'string|nullable',
+            'send_sms_post_blame_form_content' => 'string|nullable',
+            'send_sms_post_comment_blame_form_content' => 'string|nullable',
+        ];
+
+        $attributes = [
+
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts);
+
+        $message = '문자 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.form.sms.index')->with('message', $message);
+    }
+}

+ 84 - 0
app/Http/Controllers/Admin/Config/Layout/LogoController.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Layout;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Models\FileLib;
+
+class LogoController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 로고
+     * @method GET
+     * @see /admin/config/layout/logo
+     */
+    public function index()
+    {
+        return view('admin.config.layout.logo', []);
+    }
+
+    /**
+     * 로고 저장
+     * @method POST
+     * @see /admin/config/layout/logo
+     */
+    public function store(Request $request, FileLib $fileLib)
+    {
+        $rules = [
+            'site_favicon' => 'mimes:ico|max:1192',
+            'site_logo' => 'mimes:jpg,jpeg,gif,png|max:2192'
+        ];
+
+        $attributes = [
+            'site_favicon' => '사이트 파비콘',
+            'site_logo' => '사이트 로고',
+        ];
+
+        $this->validate($request, $rules, [], $attributes);
+
+        // 파일 저장
+        $storage = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR);
+        $updateData = [];
+        if($request->hasFile('site_favicon')) {
+            $siteFavicon = $request->file('site_favicon');
+            $siteFavicon->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_FAVICON);
+            $updateData['site_favicon'] = ($storage . UPLOAD_PATH_FAVICON . DIRECTORY_SEPARATOR . $siteFavicon->hashName());
+        }
+        if($request->hasFile('site_logo')) {
+            $siteLogo = $request->file('site_logo');
+            $siteLogo->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_LOGO);
+            $updateData['site_logo'] = ($storage . UPLOAD_PATH_LOGO . DIRECTORY_SEPARATOR . $siteLogo->hashName());
+        }
+
+        // 파일 삭제
+        if($request->get('site_favicon_del')) {
+            $faviconPath = ($request->get('site_favicon_url'));
+            if(file_exists($faviconPath)) {
+                unlink($faviconPath);
+            }
+            $updateData['site_favicon'] = '';
+        }
+
+        if($request->get('site_logo_del')) {
+            $logoPath = $request->get('site_logo_url');
+            if(file_exists($logoPath)) {
+                unlink($logoPath);
+            }
+            $updateData['site_logo'] = '';
+        }
+
+        $this->configModel->save($updateData, $attributes);
+
+        $message = '로고 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.layout.logo.index')->with('message', $message);
+    }
+}

+ 64 - 0
app/Http/Controllers/Admin/Config/Layout/MetaController.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Layout;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class MetaController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 메타 태그
+     * @method GET
+     * @see /admin/config/layout/meta
+     */
+    public function index()
+    {
+        return view('admin.config.layout.meta', []);
+    }
+
+    /**
+     * 메타 태그 저장
+     * @method POST
+     * @see /admin/config/layout/meta
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'meta_keywords' => 'string|nullable|max:500',
+            'meta_description' => 'string|nullable|max:500',
+            'meta_author' => 'string|nullable|max:500',
+            'meta_viewport' => 'string|nullable|max:500',
+            'meta_application_name' => 'string|nullable|max:500',
+            'meta_generator' => 'string|nullable|max:500',
+            'meta_robots' => 'string|nullable|max:500',
+            'meta_adds_info' => 'string|nullable|max:2000'
+        ];
+
+        $attributes = [
+            'meta_keywords' => 'Meta keywords',
+            'meta_description' => 'Meta description',
+            'meta_author' => 'Meta author',
+            'meta_viewport' => 'Meta viewport',
+            'meta_application_name' => 'Meta Application Name',
+            'meta_generator' => 'Meta Generator',
+            'meta_robots' => 'Meta Robots',
+            'meta_adds_info' => 'Meta Adds Info'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '메타 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.layout.meta.index')->with('message', $message);
+    }
+}

+ 59 - 0
app/Http/Controllers/Admin/Config/OptimizeController.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use Artisan;
+
+class OptimizeController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 복구 / 최적화
+     * @method GET
+     * @see /admin/config/optimize
+     */
+    public function index()
+    {
+        return view('admin.config.optimize.index', []);
+    }
+
+    /**
+     * 1: Clear Application Cache
+     * 2: Clear Route Cache
+     * 3: Clear Configuration Cache
+     * 4: Clear Compiled Views Cache
+     * @method GET
+     * @see /admin/config/optimize
+     */
+    public function clear(Request $request)
+    {
+        $cmd = $request->segment(5);
+        switch($cmd) {
+            case 'appCache' :
+                $cmd = 'cache:clear';
+                break;
+            case 'routeCache' :
+                $cmd = 'route:clear';
+                break;
+            case 'configCache' :
+                $cmd = 'config:clear';
+                break;
+            case 'viewCache' :
+                $cmd = 'view:clear';
+                break;
+        }
+
+        Artisan::call($cmd);
+        $message = Artisan::output();
+        return redirect()->route('admin.config.optimize')->with('message', $message);
+    }
+}

+ 86 - 0
app/Http/Controllers/Admin/Config/Register/BasicController.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Register;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class BasicController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 기본
+     * @method GET
+     * @see /admin/config/register/basic
+     */
+    public function index()
+    {
+        return view('admin.config.register.basic', []);
+    }
+
+    /**
+     * 기본 저장
+     * @method POST
+     * @see /admin/config/register/basic
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'use_register_block' => 'numeric|nullable',
+            'use_register_email_auth' => 'numeric|nullable',
+            'password_min_length' => 'numeric|nullable',
+            'password_uppercase_length' => 'numeric|nullable',
+            'password_numbers_length' => 'numeric|nullable',
+            'password_specialchars_length' => 'numeric|nullable',
+            'use_user_thumb' => 'numeric|nullable',
+            'user_thumb_width' => 'numeric|nullable',
+            'user_thumb_height' => 'numeric|nullable',
+            'use_user_icon' => 'numeric|nullable',
+            'user_icon_width' => 'numeric|nullable',
+            'user_icon_height' => 'numeric|nullable',
+            'denied_nickname_list' => 'string|nullable',
+            'denied_userid_list' => 'string|nullable',
+            'denied_email_list' => 'string|nullable',
+            'user_register_policy_1' => 'string|nullable',
+            'user_register_policy_2' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'use_register_block' => '회원가입 차단',
+            'use_register_email_auth' => '회원가입시 메일 인증 사용',
+            'password_min_length' => '비밀번호 보안수준 - 비밀번호 최소',
+            'password_uppercase_length' => '비밀번호 보안수준 - 대문자',
+            'password_numbers_length' => '비밀번호 보안수준 - 숫자',
+            'password_specialchars_length' => '비밀번호 보안수준 - 특수문자',
+            'use_user_thumb' => '회원 프로필 사진',
+            'user_thumb_width' => '회원 프로필 - 가로 길이',
+            'user_thumb_height' => '회원 프로필 - 세로 길이',
+            'use_user_icon' => '회원 아이콘',
+            'user_icon_width' => '회원 아이콘 - 가로 길이',
+            'user_icon_height' => '회원 아이콘 - 세로 길이',
+            'denied_nickname_list' => '금지 이름',
+            'denied_userid_list' => '금지 ID',
+            'denied_email_list' => '금지 E-mail',
+            'user_register_policy_1' => '이용약관',
+            'user_register_policy_2' => '개인정보처리방침'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['use_register_block'] ??= '0';
+        $posts['use_register_email_auth'] ??= '0';
+        $posts['use_user_thumb'] ??= '0';
+        $posts['use_user_icon'] ??= '0';
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '기본 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.register.basic.index')->with('message', $message);
+    }
+}

+ 58 - 0
app/Http/Controllers/Admin/Config/Register/LoginController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Register;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class LoginController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 로그인
+     * @method GET
+     * @see /admin/config/register/login
+     */
+    public function index()
+    {
+        return view('admin.config.register.login', []);
+    }
+
+    /**
+     * 로그인 저장
+     * @method POST
+     * @see /admin/config/register/login
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'change_password_day' => 'required|numeric|min:0|max:365',
+            'max_login_try_count' => 'required|numeric|min:0',
+            'max_login_try_limit_second' => 'required|numeric|min:0|max:86400',
+            'url_after_login' => 'string|nullable',
+            'url_after_logout' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'change_password_day' => '비밀번호 갱신주기',
+            'max_login_try_count' => '로그인 시도 제한 횟수',
+            'max_login_try_limit_second' => '로그인 시도 제한시간',
+            'url_after_login' => '로그인 후 이동할 주소',
+            'url_after_logout' => '로그아웃 후 이동할 주소'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '정보 수정시 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.register.login.index')->with('message', $message);
+    }
+}

+ 52 - 0
app/Http/Controllers/Admin/Config/Register/ModifyController.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Register;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class ModifyController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 정보 수정시
+     * @method GET
+     * @see /admin/config/register/modify
+     */
+    public function index()
+    {
+        return view('admin.config.register.modify', []);
+    }
+
+    /**
+     * 정보 수정시 저장
+     * @method POST
+     * @see /admin/config/register/modify
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'change_nickname_day' => 'required|numeric|min:0|max:365',
+            'change_email_day' => 'required|numeric|min:0|max:365'
+        ];
+
+        $attributes = [
+            'change_nickname_day' => '이름',
+            'change_email_day' => 'E-mail'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '정보 수정시 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.register.modify.index')->with('message', $message);
+    }
+}

+ 84 - 0
app/Http/Controllers/Admin/Config/Register/NotifyController.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Register;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class NotifyController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 알림
+     * @method GET
+     * @see /admin/config/register/notify
+     */
+    public function index()
+    {
+        return view('admin.config.register.notify', []);
+    }
+
+    /**
+     * 알림 저장
+     * @method POST
+     * @see /admin/config/register/notify
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_email_register_admin' => 'numeric|nullable',
+            'send_email_register_user' => 'numeric|nullable',
+            'send_telegram_register_admin' => 'numeric|nullable',
+            'send_telegram_register_user' => 'numeric|nullable',
+            'send_email_changepw_admin' => 'numeric|nullable',
+            'send_email_changepw_user' => 'numeric|nullable',
+            'send_telegram_changepw_admin' => 'numeric|nullable',
+            'send_telegram_changepw_user' => 'numeric|nullable',
+            'send_email_withdraw_admin' => 'numeric|nullable',
+            'send_email_withdraw_user' => 'numeric|nullable',
+            'send_telegram_withdraw_admin' => 'numeric|nullable',
+            'send_telegram_withdraw_user' => 'numeric|nullable'
+        ];
+
+        $attributes = [
+            'send_email_register_admin' => '회원가입 시 - 최고관리자에게 이메일을 발송합니다.',
+            'send_email_register_user' => '회원가입 시 - 회원에게 이메일을 발송합니다.',
+            'send_telegram_register_admin' => '회원가입 시 - 최고관리자에게 텔레그램를 발송합니다.',
+            'send_telegram_register_user' => '회원가입 시 - 회원에게 텔레그램를 발송합니다.',
+            'send_email_changepw_admin' => '비밀번호 변경 시 - 최고관리자에게 이메일을 발송합니다.',
+            'send_email_changepw_user' => '비밀번호 변경 시 - 회원에게 이메일을 발송합니다.',
+            'send_telegram_changepw_admin' => '비밀번호 변경 시 - 최고관리자에게 텔레그램를 발송합니다.',
+            'send_telegram_changepw_user' => '비밀번호 변경 시 - 회원에게 텔레그램를 발송합니다.',
+            'send_email_withdraw_admin' => '회원탈퇴 시 - 최고관리자에게 이메일을 발송합니다.',
+            'send_email_withdraw_user' => '회원탈퇴 시 - 회원에게 이메일을 발송합니다.',
+            'send_telegram_withdraw_admin' => '회원탈퇴 시 - 최고관리자에게 텔레그램를 발송합니다.',
+            'send_telegram_withdraw_user' => '회원탈퇴 시 - 회원에게 텔레그램를 발송합니다.'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['send_email_register_admin'] ??= '0';
+        $posts['send_email_register_user'] ??= '0';
+        $posts['send_telegram_register_admin'] ??= '0';
+        $posts['send_telegram_register_user'] ??= '0';
+        $posts['send_email_changepw_admin'] ??= '0';
+        $posts['send_email_changepw_user'] ??= '0';
+        $posts['send_telegram_changepw_admin'] ??= '0';
+        $posts['send_telegram_changepw_user'] ??= '0';
+        $posts['send_email_withdraw_admin'] ??= '0';
+        $posts['send_email_withdraw_user'] ??= '0';
+        $posts['send_telegram_withdraw_admin'] ??= '0';
+        $posts['send_telegram_withdraw_user'] ??= '0';
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '알림 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.register.notify.index')->with('message', $message);
+    }
+}

+ 60 - 0
app/Http/Controllers/Admin/Config/Register/TossController.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Register;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class TossController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 토스 인증
+     * @method GET
+     * @see /admin/config/register/toss
+     */
+    public function index()
+    {
+        return view('admin.config.register.toss', [
+            'config' => $this->configModel->getAllMeta()
+        ]);
+    }
+
+    /**
+     * 토스 인증 저장
+     * @method POST
+     * @see /admin/config/register/toss
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'toss_cert_is_test' => 'numeric|nullable|in:0,1',
+            'live_toss_cert_client_id' => 'required_if:toss_cert_is_test,0|string|max:255',
+            'live_toss_cert_client_secret' => 'required_if:toss_cert_is_test,0|string|max:255',
+            'test_toss_cert_client_id' => 'required_if:toss_cert_is_test,1|string|max:255',
+            'test_toss_cert_client_secret' => 'required_if:toss_cert_is_test,1|string|max:255',
+        ];
+
+        $attributes = [
+            'toss_cert_is_test' => '실 토스 인증 여부',
+            'live_toss_cert_client_id' => 'Live Client Key',
+            'live_toss_cert_client_secret' => 'Live client Secret',
+            'test_toss_cert_client_id' => 'Test Client Key',
+            'test_toss_cert_client_secret' => 'Test Client Secret'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '토스 인증서가 저장되었습니다.';
+        return redirect()->route('admin.config.register.toss.index')->with('message', $message);
+    }
+}

+ 359 - 0
app/Http/Controllers/Admin/Config/ServerController.php

@@ -0,0 +1,359 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+
+class ServerController extends Controller
+{
+    public function index()
+    {
+        $this->server();
+    }
+
+    /**
+     * 서버 상태
+     * @method GET
+     * @see /admin/config/server
+     */
+    public function server()
+    {
+        // 서버 자원 정보
+        return view('admin.config.server.index', [
+            'subject' => '서버상태',
+            'networkInfo' => $this->getNetworkInfo(),
+            'memInfo' => $this->getMemoryInfo(),
+            'cpuInfo' => $this->getCpuInfo(),
+            'sysInfo' => $this->getSysInfo(),
+            'os' => explode(" ", php_uname()) // 서버 운영체제 정보
+        ]);
+    }
+
+    /**
+     * 관리자 페이지 메인 ajax 호출
+     */
+    public function ajaxData(Request $request)
+    {
+        if (!$request->ajax()) {
+            exit('No direct script access allowed');
+        }
+
+        $sysInfo = $this->getSysInfo();
+
+        echo json_encode([
+            'networkInfo' => $this->getNetworkInfo(),
+            'sysInfo' => $sysInfo,
+            'upTime' => $sysInfo['uptime'],
+            'sTime' => date('Y-m-d H:i:s'),
+            'cpuInfo' => $this->getCpuInfo(),
+            'memInfo' => $this->getMemoryInfo()
+        ]);
+    }
+
+    /**
+     * 운영체제 및 서버 시스템 정보
+     */
+    private function getSysInfo()
+    {
+        $sysInfo = [null];
+        switch (PHP_OS) {
+            case "Linux":
+
+                // CPU
+                if (false === ($str = @file("/proc/cpuinfo"))) {
+                    return false;
+                }
+                $str = implode("", $str);
+
+                @preg_match_all("/model\s+name\s{0,}\:+\s{0,}([\w\s\)\(\@.-]+)([\r\n]+)/s", $str, $model);
+                @preg_match_all("/cpu\s+MHz\s{0,}\:+\s{0,}([\d\.]+)[\r\n]+/", $str, $mhz);
+                @preg_match_all("/cache\s+size\s{0,}\:+\s{0,}([\d\.]+\s{0,}[A-Z]+[\r\n]+)/", $str, $cache);
+                @preg_match_all("/bogomips\s{0,}\:+\s{0,}([\d\.]+)[\r\n]+/", $str, $bogomips);
+
+                if (false !== is_array($model[1])) {
+                    $sysInfo['cpu']['num'] = sizeof($model[1]);
+
+                    for ($i = 0; $i < $sysInfo['cpu']['num']; $i++) {
+                        $sysInfo['cpu']['model'][] = $model[1][$i] . '&nbsp;(' . $mhz[1][$i] . ')';
+                        $sysInfo['cpu']['mhz'][] = $mhz[1][$i];
+                        $sysInfo['cpu']['cache'][] = $cache[1][$i];
+                        $sysInfo['cpu']['bogomips'][] = $bogomips[1][$i];
+                    }
+
+                    if ($sysInfo['cpu']['num'] == 1) {
+                        $x1 = '';
+                    } else {
+                        $x1 = ' Г—' . $sysInfo['cpu']['num'];
+                    }
+
+                    $mhz[1][0] = ' | Frequency:' . $mhz[1][0];
+                    $cache[1][0] = ' | Secondary cache:' . $cache[1][0];
+                    $bogomips[1][0] = ' | Bogomips:' . $bogomips[1][0];
+                    $sysInfo['cpu']['model'][] = $model[1][0] . $mhz[1][0] . $cache[1][0] . $bogomips[1][0] . $x1;
+                    if (false !== is_array($sysInfo['cpu']['model'])) $sysInfo['cpu']['model'] = implode("<br />", $sysInfo['cpu']['model']);
+                    if (false !== is_array($sysInfo['cpu']['mhz'])) $sysInfo['cpu']['mhz'] = implode("<br />", $sysInfo['cpu']['mhz']);
+                    if (false !== is_array($sysInfo['cpu']['cache'])) $sysInfo['cpu']['cache'] = implode("<br />", $sysInfo['cpu']['cache']);
+                    if (false !== is_array($sysInfo['cpu']['bogomips'])) $sysInfo['cpu']['bogomips'] = implode("<br />", $sysInfo['cpu']['bogomips']);
+                }
+
+                // NETWORK, UPTIME
+                if (false === ($str = @file("/proc/uptime"))) {
+                    return false;
+                }
+                $str = explode(" ", implode("", $str));
+                $str = trim($str[0]);
+                $min = $str / 60;
+                $hours = $min / 60;
+                $days = floor($hours / 24);
+                $hours = floor($hours - ($days * 24));
+                $min = floor($min - ($days * 60 * 24) - ($hours * 60));
+                if ($days !== 0) $sysInfo['uptime'] = $days . " Day";
+                if ($hours !== 0) $sysInfo['uptime'] .= $hours . " Hour";
+                $sysInfo['uptime'] .= $min . " Minute";
+
+                // MEMORY
+                if (false === ($str = @file("/proc/meminfo"))) {
+                    return false;
+                }
+                $str = implode("", $str);
+                preg_match_all("/MemTotal\s{0,}\:+\s{0,}([\d\.]+).+?MemFree\s{0,}\:+\s{0,}([\d\.]+).+?Cached\s{0,}\:+\s{0,}([\d\.]+).+?SwapTotal\s{0,}\:+\s{0,}([\d\.]+).+?SwapFree\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buf);
+                preg_match_all("/Buffers\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buffers);
+                $sysInfo['memTotal'] = round($buf[1][0] / 1024, 2);
+                $sysInfo['memFree'] = round($buf[2][0] / 1024, 2);
+                $sysInfo['memBuffers'] = round($buffers[1][0] / 1024, 2);
+                $sysInfo['memCached'] = round($buf[3][0] / 1024, 2);
+                $sysInfo['memUsed'] = $sysInfo['memTotal'] - $sysInfo['memFree'];
+                $sysInfo['memPercent'] = (floatval($sysInfo['memTotal']) != 0) ? round($sysInfo['memUsed'] / $sysInfo['memTotal'] * 100, 2) : 0;
+                $sysInfo['memRealUsed'] = $sysInfo['memTotal'] - $sysInfo['memFree'] - $sysInfo['memCached'] - $sysInfo['memBuffers'];
+                $sysInfo['memRealFree'] = $sysInfo['memTotal'] - $sysInfo['memRealUsed'];
+                $sysInfo['memRealPercent'] = (floatval($sysInfo['memTotal']) != 0) ? round($sysInfo['memRealUsed'] / $sysInfo['memTotal'] * 100, 2) : 0;
+                $sysInfo['memCachedPercent'] = (floatval($sysInfo['memCached']) != 0) ? round($sysInfo['memCached'] / $sysInfo['memTotal'] * 100, 2) : 0;
+                $sysInfo['swapTotal'] = round($buf[4][0] / 1024, 2);
+                $sysInfo['swapFree'] = round($buf[5][0] / 1024, 2);
+                $sysInfo['swapUsed'] = round($sysInfo['swapTotal'] - $sysInfo['swapFree'], 2);
+                $sysInfo['swapPercent'] = (floatval($sysInfo['swapTotal']) != 0) ? round($sysInfo['swapUsed'] / $sysInfo['swapTotal'] * 100, 2) : 0;
+
+                // LOAD AVG
+                if (false === ($str = @file("/proc/loadavg"))) {
+                    return false;
+                }
+                $str = explode(" ", implode("", $str));
+                $str = array_chunk($str, 4);
+                $sysInfo['loadAvg'] = implode(" ", $str[0]);
+                break;
+
+            case "FreeBSD":
+
+                // CPU
+                if (false === ($sysInfo['cpu']['num'] = getKey("hw.ncpu"))) {
+                    return false;
+                }
+                $sysInfo['cpu']['model'] = getKey("hw.model");
+
+                // LOAD AVG
+                if (false === ($sysInfo['loadAvg'] = getKey("vm.loadavg"))) {
+                    return false;
+                }
+
+                // UPTIME
+                if (false === ($buf = getKey("kern.boottime"))) {
+                    return false;
+                }
+                $buf = explode(' ', $buf);
+                $sys_ticks = time() - intval($buf[3]);
+                $min = $sys_ticks / 60;
+                $hours = $min / 60;
+                $days = floor($hours / 24);
+                $hours = floor($hours - ($days * 24));
+                $min = floor($min - ($days * 60 * 24) - ($hours * 60));
+                if ($days !== 0) $sysInfo['uptime'] = $days . " Day";
+                if ($hours !== 0) $sysInfo['uptime'] .= $hours . " Hour";
+                $sysInfo['uptime'] .= $min . " Minute";
+
+                // MEMORY
+                if (false === ($buf = getKey("hw.physmem"))) {
+                    return false;
+                }
+
+                $sysInfo['memTotal'] = round($buf / 1024 / 1024, 2);
+                $str = getKey("vm.vmtotal");
+                preg_match_all("/\nVirtual Memory[\:\s]*\(Total[\:\s]*([\d]+)K[\,\s]*Active[\:\s]*([\d]+)K\)\n/i", $str, $buff, PREG_SET_ORDER);
+                preg_match_all("/\nReal Memory[\:\s]*\(Total[\:\s]*([\d]+)K[\,\s]*Active[\:\s]*([\d]+)K\)\n/i", $str, $buf, PREG_SET_ORDER);
+                $sysInfo['memRealUsed'] = round($buf[0][2] / 1024, 2);
+                $sysInfo['memCached'] = round($buff[0][2] / 1024, 2);
+                $sysInfo['memUsed'] = round($buf[0][1] / 1024, 2) + $sysInfo['memCached'];
+                $sysInfo['memFree'] = $sysInfo['memTotal'] - $sysInfo['memUsed'];
+                $sysInfo['memPercent'] = (floatval($sysInfo['memTotal']) != 0) ? round($sysInfo['memUsed'] / $sysInfo['memTotal'] * 100, 2) : 0;
+                $sysInfo['memRealPercent'] = (floatval($sysInfo['memTotal']) != 0) ? round($sysInfo['memRealUsed'] / $sysInfo['memTotal'] * 100, 2) : 0;
+                break;
+
+            default:
+                break;
+        }
+
+        if(in_array(PHP_OS, ['Linux', 'FreeBSD'])) {
+            if ($sysInfo['memTotal'] < 1024) {
+                $memTotal = $sysInfo['memTotal'] . " M";
+                $mt = $sysInfo['memTotal'] . " M";
+                $mu = $sysInfo['memUsed'] . " M";
+                $mf = $sysInfo['memFree'] . " M";
+                $mc = $sysInfo['memCached'] . " M";
+                $mb = $sysInfo['memBuffers'] . " M";
+                $st = $sysInfo['swapTotal'] . " M";
+                $su = $sysInfo['swapUsed'] . " M";
+                $sf = $sysInfo['swapFree'] . " M";
+                $swapPercent = ($sysInfo['swapPercent']);
+                $memRealUsed = $sysInfo['memRealUsed'] . " M";
+                $memRealFree = $sysInfo['memRealFree'] . " M";
+                $memRealPercent = ($sysInfo['memRealPercent']);
+                $memPercent = ($sysInfo['memPercent']);
+                $memCachedPercent = ($sysInfo['memCachedPercent']);
+            } else {
+                $memTotal = round($sysInfo['memTotal'] / 1024, 3) . " G";
+                $mt = round($sysInfo['memTotal'] / 1024, 3) . " G";
+                $mu = round($sysInfo['memUsed'] / 1024, 3) . " G";
+                $mf = round($sysInfo['memFree'] / 1024, 3) . " G";
+                $mc = round($sysInfo['memCached'] / 1024, 3) . " G";
+                $mb = round($sysInfo['memBuffers'] / 1024, 3) . " G";
+                $st = round($sysInfo['swapTotal'] / 1024, 3) . " G";
+                $su = round($sysInfo['swapUsed'] / 1024, 3) . " G";
+                $sf = round($sysInfo['swapFree'] / 1024, 3) . " G";
+                $swapPercent = ($sysInfo['swapPercent']);
+                $memRealUsed = round($sysInfo['memRealUsed'] / 1024, 3) . " G";
+                $memRealFree = round($sysInfo['memRealFree'] / 1024, 3) . " G";
+                $memRealPercent = ($sysInfo['memRealPercent']);
+                $memPercent = ($sysInfo['memPercent']);
+                $memCachedPercent = ($sysInfo['memCachedPercent']);
+            }
+
+            $sysInfo = array_merge($sysInfo, [
+                'memTotal' => $memTotal,
+                'mt' => $mt,
+                'mu' => $mu,
+                'mf' => $mf,
+                'mc' => $mc,
+                'mb' => $mb,
+                'st' => $st,
+                'su' => $su,
+                'sf' => $sf,
+                'swapPercent' => $swapPercent,
+                'memRealUsed' => $memRealUsed,
+                'memRealFree' => $memRealFree,
+                'memRealPercent' => $memRealPercent,
+                'memPercent' => $memPercent,
+                'memCachedPercent' => $memCachedPercent
+            ]);
+        }
+
+        return $sysInfo;
+    }
+
+    /**
+     * CPU 상세정보
+     */
+    private function getCpuInfo(): string
+    {
+        $stat1 = GetCoreInformation();
+        sleep(1);
+        $stat2 = GetCoreInformation();
+        $data = GetCpuPercentages($stat1, $stat2);
+
+        $ret = '';
+        if(isset($data['cpu0']['user'])) {
+            $ret .= $data['cpu0']['user'] . "%us,  ";
+        }
+        if(isset($data['cpu0']['sys'])) {
+            $ret .= $data['cpu0']['sys'] . "%sy,  ";
+        }
+        if(isset($data['cpu0']['nice'])) {
+            $ret .= $data['cpu0']['nice'] . "%ni, ";
+        }
+        if(isset($data['cpu0']['idle'])) {
+            $ret .= $data['cpu0']['idle'] . "%id,  ";
+        }
+        if(isset($data['cpu0']['iowait'])) {
+            $ret .= $data['cpu0']['iowait'] . "%wa,  ";
+        }
+        if(isset($data['cpu0']['irq'])) {
+            $ret .= $data['cpu0']['irq'] . "%irq,  ";
+        }
+        if(isset($data['cpu0']['softirq'])) {
+            $ret .= $data['cpu0']['softirq'] . "%softirq";
+        }
+
+        return $ret;
+    }
+
+    /**
+     * 사용 메모리 정보
+     */
+    private function getMemoryInfo(): array
+    {
+        $dt = round(@disk_total_space(".") / (1024 * 1024 * 1024), 3);
+        $df = round(@disk_free_space(".") / (1024 * 1024 * 1024), 3);
+        $du = ($dt - $df); //е·Із”Ё
+        $hdPercent = ((floatval($dt) != 0) ? round($du / $dt * 100, 2) : 0);
+
+        return ['dt' => $dt, 'df' => $df, 'du' => $du, 'hdPercent' => $hdPercent];
+    }
+
+    /**
+     * 네트워크 정보 조회
+     */
+    private function getNetworkInfo(): array
+    {
+        // 네트워크 정보 조회
+        $networkResult = [];
+        $networkData = @file("/proc/net/dev");
+
+        if($networkData) {
+            for ($i = 2; $i < count($networkData); $i++) {
+                preg_match_all("/([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/", $networkData[$i], $info);
+                $networkResult[$i - 2] = [
+                    'networkName' => $info[1][0],
+                    'NetOutSpeed' => $info[10][0],
+                    'NetInputSpeed' => $info[2][0],
+                    'NetInput' => byteFormat($info[2][0]),
+                    'NetOut' => byteFormat($info[10][0]),
+                ];
+            }
+        }
+
+        return $networkResult;
+    }
+
+    /**
+     * PHP 정보
+     * @method GET
+     * @see /admin/config/server/php
+     */
+    public function php()
+    {
+        ob_start();
+        phpinfo();
+        $phpinfo = ob_get_contents();
+        ob_end_clean();
+        $phpinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $phpinfo);
+        ob_clean();
+
+        return view('admin.config.php.index', [
+            'subject' => 'PHP 정보',
+            'phpinfo' => $phpinfo
+        ]);
+    }
+
+    /**
+     * DB 정보
+     * @method GET
+     * @see /admin/config/server/db
+     */
+    public function db()
+    {
+        return view('admin.config.db.index', [
+            'subject' => 'DB 정보',
+            'database' => config('database'),
+            'drivers' => join(', ', \Illuminate\Support\Facades\DB::availableDrivers())
+        ]);
+    }
+}

+ 60 - 0
app/Http/Controllers/Admin/Config/Setting/AccessController.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Models\FileLib;
+
+class AccessController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 접근
+     * @method GET
+     * @see /admin/config/setting/access
+     */
+    public function index()
+    {
+        return view('admin.config.setting.access', []);
+    }
+
+    /**
+     * 접근 저장
+     * @method POST
+     * @see /admin/config/setting/access
+     */
+    public function store(Request $request, FileLib $fileLib)
+    {
+        $rules = [
+            'admin_ip_whitelist' => 'string|nullable',
+            'site_ip_blacklist' => 'string|nullable',
+            'site_ip_whitelist' => 'string|nullable',
+            'site_blacklist_title' => 'string|nullable',
+            'site_blacklist_content' => 'string|nullable',
+        ];
+
+        $attributes = [
+            'admin_ip_whitelist' => '관리자 페이지 접근 가능 IP',
+            'site_ip_blacklist' => '사이트 접근 불가 IP',
+            'site_ip_whitelist' => '사이트 접근 가능 IP',
+            'site_blacklist_title' => '사이트 차단시 안내문 제목',
+            'site_blacklist_content' => '사이트 차단시 안내문 내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], );
+        $posts['site_blacklist_content'] = $fileLib->saveAsImage($posts['site_blacklist_content'], UPLOAD_PATH_EDITOR);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '접근 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.access.index')->with('message', $message);
+    }
+}

+ 88 - 0
app/Http/Controllers/Admin/Config/Setting/BasicController.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Models\User;
+
+class BasicController extends Controller
+{
+    private Config $configModel;
+    private User $userModel;
+
+    public function __construct(Config $config, User $user)
+    {
+        $this->configModel = $config;
+        $this->userModel = $user;
+    }
+
+    /**
+     * 기본
+     * @method GET
+     * @see /admin/config/setting/basic
+     */
+    public function index()
+    {
+        // 관리자 회원
+        return view('admin.config.setting.basic', [
+            'admins' => $this->userModel->getAdmins()
+        ]);
+    }
+
+    /**
+     * 기본 저장
+     * @method POST
+     * @see /admin/config/setting/basic
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'admin_title' => 'string|nullable',
+            'site_title' => 'string|nullable',
+            'master_key' => 'string|nullable',
+            'master_email' => 'email|nullable',
+            'master_name' => 'string|nullable',
+            'footer_script' => 'string|nullable|max:2000',
+            'spam_word' => 'string|nullable',
+            'white_iframe' => 'string|nullable|max:2000',
+            'ip_display_style' => 'string|nullable',
+            'new_post_second' => 'required|numeric|min:0',
+            'open_current_visitor' => 'nullable|numeric',
+            'current_visitor_minute' => 'required|numeric|min:0',
+            'cache_expire_time' => 'required|numeric|min:1|max:86400',
+            'verify_expires_at' => 'required|numeric|min:1|max:3600',
+            'verify_send_limit' => 'required|numeric|min:0',
+            'use_post_copy_log' => 'required|numeric|in:0,1',
+            'use_header_search_log' => 'required|numeric|in:0,1'
+        ];
+
+        $attributes = [
+            'admin_title' => '관리자 제목',
+            'site_title' => '홈페이지 제목',
+            'master_key' => '최고관리자',
+            'master_email' => '관리자 이메일',
+            'master_name' => '관리자 이메일 이름',
+            'footer_script' => '하단 Script',
+            'spam_word' => '금지 단어',
+            'white_iframe' => '허용 Iframe',
+            'ip_display_style' => 'IP 공개시 표시 형식',
+            'new_post_second' => '새로운 글쓰기 시간',
+            'open_current_visitor' => '현재 접속자 공개 여부',
+            'current_visitor_minute' => '현재 접속자 기준',
+            'cache_expire_time' => '캐시 생성 시간',
+            'verify_expires_at' => '인증번호 만료시간',
+            'verify_send_limit' => '인증번호 발송 분당 제한 수',
+            'use_post_copy_log' => '게시물 이동 및 복사 기록',
+            'use_header_search_log' => '검색기록 저장 여부'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '기본 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.basic.index')->with('message', $message);
+    }
+}

+ 93 - 0
app/Http/Controllers/Admin/Config/Setting/CompanyController.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Http\Traits\TossTrait;
+
+class CompanyController extends Controller
+{
+    use TossTrait;
+
+    private Config $configModel;
+    private array $tossBanks;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+        $this->tossBanks = $this->getBanks();
+    }
+
+    /**
+     * 회사
+     * @method GET
+     * @see /admin/config/setting/company
+     */
+    public function index()
+    {
+        return view('admin.config.setting.company', [
+            'config' => $this->configModel->getAllMeta(),
+            'banks' => $this->tossBanks
+        ]);
+    }
+
+    /**
+     * 회사 저장
+     * @method POST
+     * @see /admin/config/setting/company
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'company_name' => 'string|nullable',
+            'company_reg_no' => 'string|nullable',
+            'company_owner' => 'string|nullable',
+            'company_phone' => 'string|nullable',
+            'company_fax' => 'string|nullable',
+            'company_retail_sale_no' => 'string|nullable',
+            'company_added_sale_no' => 'string|nullable',
+            'company_zip_code' => 'string|nullable',
+            'company_address' => 'string|nullable',
+            'company_hosting' => 'string|nullable',
+            'company_admin_name' => 'string|nullable',
+            'company_admin_email' => 'string|nullable',
+            'company_site_url' => 'string|nullable',
+            'company_bank_code' => 'numeric|nullable',
+            'company_bank_owner' => 'string|nullable|max:20',
+            'company_bank_number' => 'string|nullable|max:20'
+        ];
+
+        $attributes = [
+            'company_name' => '회사명',
+            'company_reg_no' => '사업자등록번호',
+            'company_owner' => '대표자명',
+            'company_phone' => '대표전화번호',
+            'company_fax' => 'FAX 번호',
+            'company_retail_sale_no' => '통신판매업신고번호',
+            'company_added_sale_no' => '부가통신 사업자번호',
+            'company_zip_code' => '사업장 우편번호',
+            'company_address' => '사업장 주소',
+            'company_hosting' => '호스팅서비스',
+            'company_admin_name' => '정보관리책임자명',
+            'company_admin_email' => '정보관리책임자 이메일',
+            'company_site_url' => '홈페이지 주소',
+            'company_bank_code' => '회사 계좌 정보 - 은행 번호',
+            'company_bank_name' => '회사 계좌 정보 - 은행명',
+            'company_bank_owner' => '회사 계좌 정보 - 예금주',
+            'company_bank_number' => '회사 계좌 정보 - 계좌번호'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+    
+        if ($posts['company_bank_code']) {
+            $posts['company_bank_name'] = $this->tossBanks[$posts['company_bank_code']];
+        }
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '회사 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.company.index')->with('message', $message);
+    }
+}

+ 62 - 0
app/Http/Controllers/Admin/Config/Setting/ExpController.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class ExpController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 경험치
+     * @method GET
+     * @see /admin/config/setting/exp
+     */
+    public function index()
+    {
+        return view('admin.config.setting.exp', []);
+    }
+
+    /**
+     * 경험치 저장
+     * @method POST
+     * @see /admin/config/setting/exp
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'use_exp' => 'numeric|nullable',
+            'exp_register' => 'numeric|nullable',
+            'exp_login' => 'numeric|nullable',
+            'exp_recommended' => 'numeric|nullable',
+            'exp_recommender' => 'numeric|nullable',
+            'exp_attendance' => 'numeric|nullable',
+            'exp_poll' => 'numeric|nullable'
+        ];
+
+        $attributes = [
+            'use_exp' => '경험치 기능',
+            'exp_register' => '회원가입 시 경험치',
+            'exp_login' => '로그인 시 경험치',
+            'exp_recommended' => '회원가입 시 추천인에게 경험치',
+            'exp_recommender' => '추천인 존재 시 가입자에게 경험치',
+            'exp_attendance' => '출석 시 경험치',
+            'exp_poll' => '설문조사 시 경험치'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '경험치 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.exp.index')->with('message', $message);
+    }
+}

+ 58 - 0
app/Http/Controllers/Admin/Config/Setting/GeneralController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class GeneralController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 기타
+     * @method GET
+     * @see /admin/config/setting/general
+     */
+    public function index()
+    {
+        return view('admin.config.setting.general', [
+            'config' => $this->configModel->getAllMeta()
+        ]);
+    }
+
+    /**
+     * 기타 저장
+     * @method POST
+     * @see /admin/config/setting/general
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'g2a_order_bot_token' => 'nullable|string|max:255',
+            'g2a_product_bot_token' => 'nullable|string|max:255',
+            'g2a_error_bot_token' => 'nullable|string|max:255',
+            'main_bot_token' => 'nullable|string|max:255'
+        ];
+
+        $attributes = [
+            'g2a_order_bot_token' => 'G2A Order Bot Token',
+            'g2a_product_bot_token' => 'G2A Product Bot Token',
+            'g2a_error_bot_token' => 'G2A Error Bot Token',
+            'main_bot_token' => 'Main Bot Token'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '기타 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.general.index')->with('message', $message);
+    }
+}

+ 73 - 0
app/Http/Controllers/Admin/Config/Setting/NoteController.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class NoteController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 쪽지
+     * @method GET
+     * @see /admin/config/setting/note
+     */
+    public function index()
+    {
+        return view('admin.config.setting.note', []);
+    }
+
+    /**
+     * 쪽지 저장
+     * @method POST
+     * @see /admin/config/setting/note
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'use_note' => 'numeric|nullable',
+            'note_list_page' => 'numeric|nullable',
+            'note_mobile_list_page' => 'numeric|nullable',
+            'use_note_mobile_dhtml' => 'numeric|nullable',
+            'point_note' => 'numeric|nullable',
+            'use_note_file' => 'numeric|nullable',
+            'note_save_limit_day' => 'numeric|nullable',
+            'note_store_save_limit_day' => 'numeric|nullable',
+            'note_save_limit' => 'numeric|nullable',
+            'note_once_send_limit' => 'string|nullable',
+            'note_today_send_limit' => 'numeric|nullable',
+            'note_content_limit' => 'numeric|nullable'
+        ];
+
+        $attributes = [
+            'use_note' => '쪽지 기능',
+            'note_list_page' => '한페이지 쪽지수 - PC',
+            'note_mobile_list_page' => '한페이지 쪽지수 - 모바일',
+            'use_note_mobile_dhtml' => '쪽지 DHTML 기능',
+            'point_note' => '쪽지 발송시 차감포인트',
+            'point_note_file' => '파일 첨부시 차감포인트',
+            'use_note_file' => '쪽지 첨부파일 기능',
+            'note_save_limit_day' => '기본 보관일',
+            'note_store_save_limit_day' => '보관함 보관일',
+            'note_save_limit' => '보관함 최대개수',
+            'note_once_send_limit' => '동시 전송 제한 수',
+            'note_today_send_limit' => '하루 전송 제한 수',
+            'note_content_limit' => '작성 가능 글자 수'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '쪽지 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.note.index')->with('message', $message);
+    }
+}

+ 60 - 0
app/Http/Controllers/Admin/Config/Setting/NotifyController.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Setting;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class NotifyController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 알림
+     * @method GET
+     * @see /admin/config/setting/notify
+     */
+    public function index()
+    {
+        return view('admin.config.setting.notify', []);
+    }
+
+    /**
+     * 알림 저장
+     * @method POST
+     * @see /admin/config/setting/notify
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'use_notification' => 'numeric|nullable|in:0,1',
+            'notification_post_reply' => 'numeric|nullable|in:0,1',
+            'notification_post_comment' => 'numeric|nullable|in:0,1',
+            'notification_post_comment_reply' => 'numeric|nullable|in:0,1'
+        ];
+
+        $attributes = [
+            'use_notification' => '알림 기능',
+            'notification_post_reply' => '내 글에 답변글이 달렸을 때 알림',
+            'notification_post_comment' => '내 글에 댓글이 달렸을 때 알림',
+            'notification_post_comment_reply' => '내 댓글에 댓글이 달렸을 때 알림'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['use_notification'] ??= '0';
+        $posts['notification_post_reply'] ??= '0';
+        $posts['notification_post_comment'] ??= '0';
+        $posts['notification_post_comment_reply'] ??= '0';
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '알림 정보가 저장되었습니다.';
+        return redirect()->route('admin.config.setting.notify.index')->with('message', $message);
+    }
+}

+ 61 - 0
app/Http/Controllers/Admin/Config/Test/EmailController.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Test;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\EmailLib;
+
+class EmailController extends Controller
+{
+    private EmailLib $emailLib;
+
+    public function __construct(EmailLib $emailLib)
+    {
+        $this->emailLib = $emailLib;
+    }
+
+    /**
+     * 이메일 발송 확인
+     * @method GET
+     * @see /admin/config/test/email
+     */
+    public function index()
+    {
+        return view('admin.config.test.email', [
+            'subject' => '이메일 발송 확인',
+            'mailHost' => env('MAIL_HOST'),
+            'mailName' => env('MAIL_USERNAME')
+        ]);
+    }
+
+    /**
+     * 이메일 발송 확인 실행
+     * @method POST
+     * @see /admin/config/test/email
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'receive_email' => 'string|email',
+            'receive_name' => 'string',
+        ];
+
+        $attributes = [
+            'receive_email' => '받는 이메일 주소',
+            'receive_name' => '받는 이메일 이름'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        // 이메일 발송
+        $this->emailLib->test([
+            'today' => now(),
+            'email' => $posts['receive_email'],
+            'name' => $posts['receive_name']
+        ]);
+
+        $message = '이메일을 발송하였습니다.';
+        return redirect()->route('admin.config.test.email.index')->with('message', $message);
+    }
+}

+ 70 - 0
app/Http/Controllers/Admin/Config/Test/SmsController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Test;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Models\SmsLib;
+
+class SmsController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct()
+    {
+        $this->configModel = new Config();
+    }
+
+    /**
+     * SMS 발송 확인
+     * @method GET
+     * @see /admin/config/test/sms
+     */
+    public function index()
+    {
+        return view('admin.config.test.sms', [
+            'subject' => '문자 발송 확인',
+            'smsNumber' => env('SMS_NUMBER')
+        ]);
+    }
+
+    /**
+     * 문자 발송 확인 실행
+     * @method POST
+     * @see /admin/config/test/sms
+     */
+    public function store(Request $request, SmsLib $smsLib)
+    {
+        $rules = [
+            'receive_number' => 'required|string|regex:/^(\d+)-(\d+)-(\d+)$/',
+        ];
+
+        $attributes = [
+            'receive_number' => '받는 번호'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $sendNumber = [ // 수신번호
+            UID => $posts['receive_number']
+        ];
+
+        // 수신정보
+        $sendData = [
+            'userID' => UID, // 수신자 PK
+            'subject' => '[' . date('Y-m-d H:i:s') . '] 문자 발송 확인',
+            'content' => configs('site_title'),
+            'isReserve' => 0,
+            'reserveAt' => null
+        ];
+
+        $result = $smsLib->send($sendNumber, $sendData);
+        if(!$result) {
+            return back()->withErrors($smsLib->errors())->withInput();
+        }
+
+        $message = '문자를 발송하였습니다.';
+        return redirect()->route('admin.config.test.sms.index')->with('message', $message);
+    }
+}

+ 86 - 0
app/Http/Controllers/Admin/Config/Test/TelegramController.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config\Test;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use Telegram\Bot\Api;
+
+class TelegramController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 텔레그램 발송 확인
+     * @method GET
+     * @see /admin/config/test/telegram
+     */
+    public function index()
+    {
+        $config = $this->configModel->getAllMeta();
+        $response = $this->_getResponse($config);
+
+        return view('admin.config.test.telegram', [
+            'config' => $config,
+            'response' => $response->getMe()
+        ]);
+    }
+
+    /**
+     * 텔레그램 발송 확인 실행
+     * @method POST
+     * @see /admin/config/test/telegram
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'g2a_order_bot_token' => 'nullable|string|max:255',
+            'g2a_product_bot_token' => 'nullable|string|max:255',
+            'g2a_error_bot_token' => 'nullable|string|max:255',
+            'main_bot_token' => 'nullable|string|max:255',
+            'message' => 'required|string'
+        ];
+
+        $attributes = [
+            'g2a_order_bot_token' => 'G2A Order Bot Token',
+            'g2a_product_bot_token' => 'G2A Product Bot Token',
+            'g2a_error_bot_token' => 'G2A Error Bot Token',
+            'main_bot_token' => 'Main Bot Token',
+            'message' => '보낼 내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $response = $this->_getResponse(
+            $this->configModel->getAllMeta()
+        );
+
+        if (!$response->getMe()) {
+            return back()->with('message', '텔레그램 설정이 잘못되었습니다.');
+        }
+
+        $chatID = last($response->getUpdates())->getChat()->get('id');
+        $response->sendMessage([
+            'chat_id' => $chatID,
+            'text' => $posts['message']
+        ]);
+
+        $message = '텔레그램 문자를 발송하였습니다.';
+        return redirect()->route('admin.config.test.telegram.index')->with('message', $message);
+    }
+
+    private function _getResponse(Config $config): Api
+    {
+        return new Api(
+            $config->item('main_bot_token')
+        );
+    }
+}

+ 151 - 0
app/Http/Controllers/Admin/Page/Banner/GroupController.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Page\Banner;
+
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\BannerGroup;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class GroupController extends Controller
+{
+    private BannerGroup $bannerGroupModel;
+
+    public function __construct(BannerGroup $bannerGroup)
+    {
+        $this->bannerGroupModel = $bannerGroup;
+    }
+
+    /**
+     * 배너 그룹 관리
+     * @method GET
+     * @see /admin/page/banner/group
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $bannerGroupData = $this->bannerGroupModel->data($params);
+
+        if($bannerGroupData->rows > 0) {
+            $num = listNum($bannerGroupData->total, $params->page, $params->perPage);
+            foreach($bannerGroupData->list as $i => $row) {
+                $row->num = $num++;
+                $row->bannerCount = $row->banner->count();
+
+                $bannerGroupData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.page.banner.group.index', [
+            'actionURL' => route('admin.page.banner.group.store'),
+            'bannerGroupData' => $bannerGroupData
+        ]);
+    }
+
+    /**
+     * 배너 그룹 저장
+     * @method POST
+     * @see /admin/page/banner/group
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'name' => 'required|string|unique:tb_banner_group,name',
+        ];
+
+        $attributes = [
+            'name' => '배너 위치명',
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->bannerGroupModel->insert([
+            'name' => $posts['name'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => null,
+            'updated_at' => null,
+            'created_at' => now()
+        ]);
+
+        $message = '배너 그룹이 등록되었습니다.';
+        return redirect()->route('admin.page.banner.group.index')->with('message', $message);
+    }
+
+    /**
+     * 배너 그룹 수정
+     * @method PUT
+     * @see /admin/page/banner/group/{pk}
+     */
+    public function update(Request $request)
+    {
+        $chk = $request->post('chk');
+        $name = $request->post('name');
+
+        if($chk) {
+            foreach($chk as $groupID) {
+                $updateData = [
+                    'name' => ($name[$groupID] ?? ''),
+                    'ip_address' => IP_ADDRESS,
+                    'user_agent' => USER_AGENT,
+                    'updated_user_id' => UID
+                ];
+
+                $rules = [
+                    'name' => 'required|string',
+                ];
+
+                $attributes = [
+                    'name' => '위치명',
+                ];
+
+                $validator = Validator::make($updateData, $rules, [], $attributes);
+                if($validator->fails()) {
+                    return back()->withErrors($validator)->withInput();
+                }
+
+                $this->bannerGroupModel->find($groupID)->update($updateData);
+            }
+        }
+
+        $message = '배너 그룹 정보가 변경되었습니다.';
+        return redirect()->route('admin.page.banner.group.index')->with('message', $message);
+    }
+
+    /**
+     * 배너 그룹 삭제
+     * @method DELETE
+     * @see /admin/page/banner/group/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $bannerGroupID) {
+                    $bannerGroup = $this->bannerGroupModel->findOrNew($bannerGroupID);
+
+                    if(!$bannerGroup->exists)
+                    {
+                        throw new Exception($bannerGroupID . "번 배너 그룹은 존재하지 않습니다.");
+                    }
+
+                    if($bannerGroup->banner->count() > 0) {
+                        throw new Exception($bannerGroupID . "번 배너 그룹은 삭제할 수 없습니다.");
+                    }
+
+                    $bannerGroup->delete();
+                }
+            }
+
+            $message = '배너 그룹 정보가 삭제되었습니다.';
+            return redirect()->route('admin.page.banner.group.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 286 - 0
app/Http/Controllers/Admin/Page/Banner/ListController.php

@@ -0,0 +1,286 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Page\Banner;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Banner;
+use App\Models\BannerGroup;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class ListController extends Controller
+{
+    private Banner $bannerModel;
+    private BannerGroup $bannerGroupModel;
+
+    public function __construct(Banner $banner, BannerGroup $bannerGroup)
+    {
+        $this->bannerModel = $banner;
+        $this->bannerGroupModel = $bannerGroup;
+    }
+
+    /**
+     * 배너 관리
+     * @method GET
+     * @see /admin/page/banner
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->activated =  $request->get('activated');
+
+        $bannerData = $this->bannerModel->data($params);
+
+        if($bannerData->total > 0) {
+            $num = listNum($bannerData->total, $params->page, $params->perPage);
+            foreach($bannerData->list as $i => $row) {
+                $row->num = $num--;
+                $row->creater = $row->user->find($row->user_id);
+                $row->updater = $row->user->find($row->updated_user_id);
+                $row->editURL = route('admin.page.banner.edit', $row->id);
+
+                $bannerData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.page.banner.index', [
+            'bannerData' => $bannerData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 배너 등록
+     * @method GET
+     * @see /admin/page/banner
+     */
+    public function create()
+    {
+        return view('admin.page.banner.write', [
+            'actionURL' => route('admin.page.banner.store'),
+            'bannerGroupData' => $this->bannerGroupModel->all(),
+            'bannerData' => [],
+            'bannerID' => null
+        ]);
+    }
+
+    /**
+     * 배너 수정
+     * @method GET
+     * @see /admin/page/banner/{pk}/edit
+     */
+    public function edit(int $bannerID)
+    {
+        return view('admin.page.banner.write', [
+            'actionURL' => route('admin.page.banner.update', $bannerID),
+            'bannerGroupData' => $this->bannerGroupModel->all(),
+            'bannerData' => $this->bannerModel->find($bannerID),
+            'bannerID' => $bannerID
+        ]);
+    }
+
+    /**
+     * 배너 등록 저장
+     * @method POST
+     * @see /admin/page/banner
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'banner_group_id' => 'required|numeric|exists:tb_banner_group,id',
+            'image' => 'image|max:3192',
+            'subject' => 'nullable|string|max:50',
+            'summary' => 'nullable|string|max:255',
+            'start_date' => 'required|date',
+            'end_date' => 'required|date',
+            'url' => 'nullable|string',
+            'target' => 'required|numeric|in:0,1',
+            'device' => 'required|numeric|in:0,1,2',
+            'width' => 'required|numeric',
+            'height' => 'required|numeric',
+            'order' => 'required|numeric',
+            'activated' => 'required|numeric'
+        ];
+
+        $attributes = [
+            'banner_group_id' => '위치',
+            'image' => '이미지',
+            'subject' => '제목',
+            'summary' => '요약 설명',
+            'start_date' => '시작일',
+            'end_date' => '종료일',
+            'url' => 'URL',
+            'target' => '새창 여부',
+            'device' => '표시기기',
+            'width' => '가로 크기',
+            'height' => '세로 크기',
+            'order' => '정렬',
+            'activated' => '활성화 여부'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $saveData = [
+            'banner_group_id' => $posts['banner_group_id'],
+            'user_id' => UID,
+            'start_date' => $posts['start_date'],
+            'end_date' => $posts['end_date'],
+            'subject' => $posts['subject'],
+            'summary' => $posts['summary'],
+            'url' => $posts['url'],
+            'target' => $posts['target'],
+            'device' => $posts['device'],
+            'width' => $posts['width'],
+            'height' => $posts['height'],
+            'views' => 0,
+            'order' => $posts['order'],
+            'activated' => $posts['activated'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => null,
+            'updated_at' => null,
+            'created_at' => now()
+        ];
+
+        // 파일 저장
+        if($request->hasFile('image')) {
+            $siteFavicon = $request->file('image');
+            $siteFavicon->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_BANNER);
+            $saveData['image'] = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR . UPLOAD_PATH_BANNER . DIRECTORY_SEPARATOR .$siteFavicon->hashName());
+        }
+
+        // 파일 삭제
+        if($request->get('image_del')) {
+            $path = $request->get('image_url');
+            if(file_exists($path)) {
+                unlink($request->get('image_url'));
+            }
+            $saveData['image'] = null;
+        }
+
+        $this->bannerModel->insert($saveData);
+
+        $message = '배너가 새로 등록되었습니다.';
+        return redirect()->route('admin.page.banner.index')->with('message', $message);
+    }
+
+    /**
+     * 배너 수정 저장
+     * @method PUT
+     * @see /admin/page/banner/{pk}
+     */
+    public function update(Request $request)
+    {
+        $rules = [
+            'banner_id' => 'required|numeric|exists:tb_banner,id',
+            'banner_group_id' => 'required|numeric|exists:tb_banner_group,id',
+            'image' => 'mimes:jpg,jpeg,gif,png|max:3192',
+            'subject' => 'nullable|string|max:50',
+            'summary' => 'nullable|string|max:255',
+            'start_date' => 'required|date',
+            'end_date' => 'required|date',
+            'url' => 'nullable|string',
+            'target' => 'required|numeric|in:0,1',
+            'device' => 'required|numeric|in:0,1,2',
+            'width' => 'required|numeric',
+            'height' => 'required|numeric',
+            'order' => 'required|numeric',
+            'activated' => 'required|numeric'
+        ];
+
+        $attributes = [
+            'banner_id' => '번호',
+            'banner_group_id' => '위치',
+            'image' => '이미지',
+            'subject' => '제목',
+            'summary' => '요약 설명',
+            'start_date' => '시작일',
+            'end_date' => '종료일',
+            'url' => 'URL',
+            'target' => '새창 여부',
+            'device' => '표시기기',
+            'width' => '가로 크기',
+            'height' => '세로 크기',
+            'order' => '정렬',
+            'activated' => '활성화 여부'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $saveData = [
+            'banner_group_id' => $posts['banner_group_id'],
+            'start_date' => $posts['start_date'],
+            'end_date' => $posts['end_date'],
+            'subject' => $posts['subject'],
+            'summary' => $posts['summary'],
+            'url' => $posts['url'],
+            'target' => $posts['target'],
+            'device' => $posts['device'],
+            'width' => $posts['width'],
+            'height' => $posts['height'],
+            'order' => $posts['order'],
+            'activated' => $posts['activated'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => UID,
+            'updated_at' => now()
+        ];
+
+        // 파일 저장
+        if($request->hasFile('image')) {
+            $siteFavicon = $request->file('image');
+            $siteFavicon->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_BANNER);
+            $saveData['image'] = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR . UPLOAD_PATH_BANNER . DIRECTORY_SEPARATOR . $siteFavicon->hashName());
+        }
+
+        // 파일 삭제
+        if($request->get('image_del')) {
+            $path = $request->get('image_url');
+            if(file_exists($path)) {
+                unlink($request->get('image_url'));
+            }
+            $saveData['image'] = null;
+        }
+
+        $this->bannerModel->find($posts['banner_id'])->update($saveData);
+
+        $message = '배너 정보가 수정되었습니다.';
+        return redirect()->route('admin.page.banner.edit', $posts['banner_id'])->with('message', $message);
+    }
+
+    /**
+     * 배너 삭제
+     * @method DELETE
+     * @see /admin/page/banner
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $bannerID) {
+                    $banner = $this->bannerModel->findOrNew($bannerID);
+
+                    if (!$banner->exists) {
+                        throw new Exception($bannerID . "번 배너는 존재하지 않습니다.");
+                    }
+
+                    // 이미지 삭제
+                    if (file_exists($banner->image)) {
+                        unlink($banner->image);
+                    }
+
+                    $banner->delete();
+                }
+            }
+
+            $message = '배너 정보가 삭제되었습니다.';
+            return redirect()->route('admin.page.banner.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 205 - 0
app/Http/Controllers/Admin/Page/DocumentController.php

@@ -0,0 +1,205 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Page;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Models\Document;
+use App\Models\DTO\SearchData;
+use App\Models\FileLib;
+use Exception;
+
+class DocumentController extends Controller
+{
+    use CommonTrait;
+
+    private Document $documentModel;
+
+    public function __construct(Document $document)
+    {
+        $this->documentModel = $document;
+    }
+
+    /**
+     * 문서 관리
+     * @method GET
+     * @see /admin/page/document
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+
+        $documentData = $this->documentModel->data($params);
+
+        if ($documentData->total > 0) {
+            $num = listNum($documentData->total, $params->page, $params->perPage);
+            foreach ($documentData->list as $i => $row) {
+                $row->num = $num--;
+                $row->url = route('document', $row->code);
+                $row->views = number_format($row->views);
+                $row->creater = $row->user->find($row->user_id);
+                $row->updater = $row->user->find($row->updated_user_id);
+                $row->editURL = route('admin.page.document.edit', $row->id);
+
+                $documentData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.page.document.index', [
+            'documentData' => $documentData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 문서 등록
+     * @method GET
+     * @see /admin/page/document/create
+     */
+    public function create()
+    {
+        return view('admin.page.document.write', [
+            'actionURL' => route('admin.page.document.store'),
+            'documentData' => [],
+            'documentID' => null
+        ]);
+    }
+
+    /**
+     * 문서 수정
+     * @method GET
+     * @see /admin/page/document/{pk}/edit
+     */
+    public function edit(int $documentID)
+    {
+        return view('admin.page.document.write', [
+            'actionURL' => route('admin.page.document.update', $documentID),
+            'documentData' => $this->documentModel->find($documentID),
+            'documentID' => $documentID
+        ]);
+    }
+
+    /**
+     * 문서 등록 저장
+     * @method POST
+     * @see /admin/page/document
+     */
+    public function store(Request $request, FileLib $fileLib)
+    {
+        $rules = [
+            'code' => 'string|min:3|max:50|unique:tb_document,code',
+            'subject' => 'string|max:150',
+            'content' => 'string|nullable',
+            'mobile_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'code' => '주소',
+            'subject' => '제목',
+            'content' => 'PC 내용',
+            'mobile_content' => '모바일 내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['content'] = $fileLib->saveAsImage($posts['content'], UPLOAD_PATH_DOCUMENT);
+
+        $this->documentModel->insert([
+            'user_id' => UID,
+            'code' => $posts['code'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'views' => 0,
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => null,
+            'updated_at' => null,
+            'created_at' => now()
+        ]);
+
+        $message = '문서가 등록되었습니다.';
+        return redirect()->route('admin.page.document.index')->with('message', $message);
+    }
+
+    /**
+     * 문서 수정 저장
+     * @method PUT
+     * @see /admin/page/document/{pk}
+     */
+    public function update(Request $request, FileLib $fileLib)
+    {
+        $documentID = $request->post('document_id');
+
+        $rules = [
+            'document_id' => 'required|numeric|exists:tb_document,id',
+            'code' => [
+                'string',
+                'min:3',
+                'max:50',
+                Rule::unique('tb_document', 'code')->ignore($documentID, 'id'),
+            ],
+            'subject' => 'string|max:150',
+            'content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'document_id' => '번호',
+            'code' => '주소',
+            'subject' => '제목',
+            'content' => '내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['content'] = $fileLib->saveAsImage($posts['content'], UPLOAD_PATH_DOCUMENT);
+
+        $this->documentModel->find($documentID)->update([
+            'code' => $posts['code'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => UID,
+            'updated_at' => now()
+        ]);
+
+        $message = '문서 정보가 수정되었습니다.';
+        return redirect()->route(
+            'admin.page.document.edit', $documentID
+        )->with('message', $message);
+    }
+
+    /**
+     * 문서 삭제
+     * @method DELETE
+     * @see /admin/page/document/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $documentID) {
+                    $document = $this->documentModel->findOrNew($documentID);
+
+                    if(!$document->exists)
+                    {
+                        throw new Exception($documentID . "번 문서는 존재하지 않습니다.");
+                    }
+
+                    // 이미지 삭제
+                    $this->deleteImageFromContent($document->content);
+
+                    $document->delete();
+                }
+            }
+
+            $message = '문서 정보가 삭제되었습니다.';
+            return redirect()->route('admin.page.document.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 187 - 0
app/Http/Controllers/Admin/Page/MenuController.php

@@ -0,0 +1,187 @@
+<?php
+
+/*
+ * https://www.we-rc.com/blog/2015/07/19/nested-set-model-practical-examples-part-i
+ */
+namespace App\Http\Controllers\Admin\Page;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+use App\Http\Controllers\Controller;
+use App\Models\Menu;
+
+class MenuController extends Controller
+{
+    private Menu $menuModel;
+
+    public function __construct(Menu $menu)
+    {
+        $this->menuModel = $menu;
+    }
+
+    /**
+     * 메뉴 관리
+     * @method GET
+     * @see /admin/page/menu
+     */
+    public function index()
+    {
+        $menuData = $this->menuModel->data();
+
+        if ($menuData->rows > 0) {
+            foreach ($menuData->list as $i => $row) {
+                $row->upURL = route('admin.page.menu.up', $row->id);
+                $row->downURL = route('admin.page.menu.down', $row->id);
+
+                $menuData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.page.menu.index', [
+            'actionURL' => route('admin.page.menu.store'),
+            'menuData' => $menuData
+        ]);
+    }
+
+    /**
+     * 메뉴 관리 저장
+     * @method POST
+     * @see /admin/page/menu
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'menu_id' => 'nullable|numeric',
+            'name' => 'string',
+            'link' => 'string',
+            'target' => 'string|nullable',
+            'desktop' => 'numeric|in:1,0',
+            'mobile' => 'numeric|in:1,0',
+            'custom' => 'string|nullable',
+            'icon' => 'string|nullable',
+            'depth' => 'numeric'
+        ];
+
+        $attributes = [
+            'menu_id' => '메뉴 PK',
+            'name' => '메뉴명',
+            'link' => 'URL',
+            'target' => '새창 여부',
+            'desktop' => 'PC',
+            'mobile' => '모바일',
+            'custom' => '속성',
+            'icon' => '아이콘',
+            'depth' => '분류 깊이'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->menuModel->register($posts);
+
+        $message = '메뉴 정보가 저장되었습니다.';
+        return redirect()->route('admin.page.menu.index')->with('message', $message);
+    }
+
+    /**
+     * 메뉴 관리 수정
+     * @method PUT
+     * @see /admin/page/menu/{pk}
+     */
+    public function update(Request $request)
+    {
+        $posts = $request->all();
+
+        if ($posts['chk']) {
+            foreach ($posts['chk'] as $menuID) {
+                $updateData = [
+                    'menu_id' => $menuID,
+                    'parent_id' => ($posts['parent_id'][$menuID] ?? ''),
+                    'name' => ($posts['name'][$menuID] ?? ''),
+                    'link' => ($posts['link'][$menuID] ?? ''),
+                    'target' => ($posts['target'][$menuID] ?? ''),
+                    'desktop' => ($posts['desktop'][$menuID] ?? ''),
+                    'mobile' => ($posts['mobile'][$menuID] ?? ''),
+                    'custom' => ($posts['custom'][$menuID] ?? ''),
+                    'icon' => ($posts['icon'][$menuID] ?? ''),
+                    'depth' => ($posts['depth'][$menuID] ?? '')
+                ];
+
+                $rules = [
+                    'menu_id' => 'required|numeric|exists:tb_menu,id',
+                    'parent_id' => 'required|numeric|exists:tb_menu,parent_id',
+                    'name' => 'string',
+                    'link' => 'string',
+                    'target' => 'numeric|in:1,0',
+                    'desktop' => 'numeric|in:1,0',
+                    'mobile' => 'numeric|in:1,0',
+                    'custom' => 'string|nullable',
+                    'icon' => 'string|nullable',
+                    'depth' => 'numeric'
+                ];
+
+                $attributes = [
+                    'menu_id' => '메뉴 PK',
+                    'parent_id' => '부모 메뉴 PK',
+                    'name' => '메뉴명',
+                    'link' => 'URL',
+                    'target' => '새창 여부',
+                    'desktop' => 'PC',
+                    'mobile' => '모바일',
+                    'custom' => '속성',
+                    'icon' => '아이콘',
+                    'depth' => '분류 깊이'
+                ];
+
+                $validator = Validator::make($updateData, $rules, [], $attributes);
+                if ($validator->fails()) {
+                    return back()->withErrors($validator)->withInput();
+                }
+                $this->menuModel->updater($updateData);
+            }
+        }
+
+        $message = '메뉴 정보가 변경되었습니다.';
+        return redirect()->route('admin.page.menu.index')->with('message', $message);
+    }
+
+    /**
+     * 메뉴 관리 삭제
+     * @method DELETE
+     * @see /admin/page/menu/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $menuID) {
+                $this->menuModel->remove($menuID);
+            }
+        }
+
+        $message = '메뉴 정보가 삭제되었습니다.';
+        return redirect()->route('admin.page.menu.index')->with('message', $message);
+    }
+
+    /**
+     * 메뉴 한 단계 위로
+     * @method GET
+     */
+    public function up(int $menuID)
+    {
+        $this->menuModel->orderUp($menuID);
+
+        return redirect()->route('admin.page.menu.index');
+    }
+
+    /**
+     * 메뉴 한 단계 아래로
+     * @method GET
+     */
+    public function down(int $menuID)
+    {
+        $this->menuModel->orderDown($menuID);
+
+        return redirect()->route('admin.page.menu.index');
+    }
+}

+ 249 - 0
app/Http/Controllers/Admin/Page/PopupController.php

@@ -0,0 +1,249 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Page;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\CommonTrait;
+use App\Models\Popup;
+use App\Models\FileLib;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class PopupController extends Controller
+{
+    use CommonTrait;
+
+    private Popup $popupModel;
+
+    public function __construct(Popup $popup)
+    {
+        $this->popupModel = $popup;
+    }
+
+    /**
+     * 팝업 관리
+     * @method GET
+     * @see /admin/page/popup
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->activated = $request->get('activated');
+
+        $popupData = $this->popupModel->data($params);
+
+        if ($popupData->total > 0) {
+            $num = listNum($popupData->total, $params->page, $params->perPage);
+            foreach ($popupData->list as $i => $row) {
+                $row->num = $num--;
+                $row->deviceStr = MAP_DEVICE_TYPE[$row->device];
+                $row->activatedStr = ($row->activated == '1' ? '활성' : '비활성');
+                $row->creater = $row->user->find($row->user_id);
+                $row->updater = $row->user->find($row->updated_user_id);
+                $row->editURL = route('admin.page.popup.edit', $row->id);
+
+                $popupData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.page.popup.index', [
+            'popupData' => $popupData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 팝업 등록
+     * @method GET
+     * @see /admin/page/popup
+     */
+    public function create()
+    {
+        return view('admin.page.popup.write', [
+            'actionURL' => route('admin.page.popup.store'),
+            'popupData' => null,
+            'popupID' => null
+        ]);
+    }
+
+    /**
+     * 팝업 수정
+     * @method GET
+     * @see /admin/page/popup/{pk}/edit
+     */
+    public function edit(int $popupID)
+    {
+        return view('admin.page.popup.write', [
+            'actionURL' => route('admin.page.popup.update', $popupID),
+            'popupData' => $this->popupModel->find($popupID),
+            'popupID' => $popupID
+        ]);
+    }
+
+    /**
+     * 팝업 등록 저장
+     * @method POST
+     * @see /admin/page/popup
+     */
+    public function store(Request $request, FileLib $fileLib)
+    {
+        $rules = [
+            'subject' => 'required|string',
+            'start_date' => 'required|date',
+            'end_date' => 'required|date',
+            'is_center' => 'required|numeric|in:1,2',
+            'left' => 'required_if:is_center,2|present',
+            'top' => 'required_if:is_center,2|present',
+            'width' => 'required|numeric',
+            'height' => 'required|numeric',
+            'device' => 'required|numeric|in:0,1,2',
+            'disable_hours' => 'required|numeric',
+            'activated' => 'required|in:0,1',
+            'content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'subject' => '제목',
+            'start_date' => '시작일',
+            'end_date' => '종료일',
+            'is_center' => '화면 위치',
+            'left' => '좌측 위치',
+            'top' => '상단 위치',
+            'width' => '가로 크기',
+            'height' => '세로 크기',
+            'device' => '표시기기',
+            'disable_hours' => '닫기 유효시간',
+            'activated' => '활성화 여부',
+            'content' => '내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['content'] = $fileLib->saveAsImage($posts['content'], UPLOAD_PATH_POPUP);
+
+        $this->popupModel->insert([
+            'user_id' => UID,
+            'start_date' => $posts['start_date'],
+            'end_date' => $posts['end_date'],
+            'is_center' => $posts['is_center'],
+            'left' => $posts['left'],
+            'top' => $posts['top'],
+            'width' => $posts['width'],
+            'height' => $posts['height'],
+            'device' => $posts['device'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'disable_hours' => $posts['disable_hours'],
+            'activated' => $posts['activated'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => null,
+            'updated_at' => null,
+            'created_at' => now(),
+        ]);
+
+        $message = '팝업이 새로 등록되었습니다.';
+        return redirect()->route('admin.page.popup.index')->with('message', $message);
+    }
+
+    /**
+     * 팝업 수정 저장
+     * @method PUT
+     * @see /admin/page/popup/{pk}
+     */
+    public function update(Request $request, FileLib $fileLib)
+    {
+        $popupID = $request->post('popup_id');
+
+        $rules = [
+            'popup_id' => 'required|numeric|exists:tb_popup,id',
+            'subject' => 'required|string',
+            'start_date' => 'required|date',
+            'end_date' => 'required|date',
+            'is_center' => 'required|numeric|in:1,2',
+            'left' => 'required_if:is_center,2|present',
+            'top' => 'required_if:is_center,2|present',
+            'width' => 'required|numeric',
+            'height' => 'required|numeric',
+            'device' => 'required|numeric|in:0,1,2',
+            'disable_hours' => 'required|numeric',
+            'activated' => 'required|in:0,1',
+            'content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'popup_id' => '번호',
+            'subject' => '제목',
+            'start_date' => '시작일',
+            'end_date' => '종료일',
+            'is_center' => '화면 위치',
+            'left' => '좌측 위치',
+            'top' => '상단 위치',
+            'width' => '가로 크기',
+            'height' => '세로 크기',
+            'device' => '표시기기',
+            'disable_hours' => '닫기 유효시간',
+            'activated' => '활성화 여부',
+            'content' => '내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['content'] = $fileLib->saveAsImage($posts['content'], UPLOAD_PATH_POPUP);
+
+        $this->popupModel->find($popupID)->update([
+            'start_date' => $posts['start_date'],
+            'end_date' => $posts['end_date'],
+            'is_center' => $posts['is_center'],
+            'left' => $posts['left'],
+            'top' => $posts['top'],
+            'width' => $posts['width'],
+            'height' => $posts['height'],
+            'device' => $posts['device'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'disable_hours' => $posts['disable_hours'],
+            'activated' => $posts['activated'],
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'updated_user_id' => UID,
+            'updated_at' => now()
+        ]);
+
+        $message = '팝업 정보가 수정되었습니다.';
+        return redirect()->route('admin.page.popup.edit', $popupID)->with('message', $message);
+    }
+
+    /**
+     * 팝업 삭제
+     * @method DELETE
+     * @see /admin/page/popup/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try {
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $popupID) {
+                    $popup = $this->popupModel->findOrNew($popupID);
+
+                    if(!$popup->exists)
+                    {
+                        throw new Exception($popupID . "번 팝업은 존재하지 않습니다.");
+                    }
+
+                    // 이미지 삭제
+                    $this->deleteImageFromContent($popup->content);
+
+                    $popup->delete();
+                }
+            }
+
+            $message = '팝업 정보가 삭제되었습니다.';
+            return redirect()->route('admin.page.popup.index')->with('message', $message);
+        } catch (Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 42 - 0
app/Http/Controllers/Admin/Popup/SmsController.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Popup;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsFavorite;
+use App\Models\DTO\SearchData;
+
+class SmsController extends Controller
+{
+    private SmsFavorite $smsFavoriteModel;
+
+    public function __construct(SmsFavorite $smsFavorite)
+    {
+        $this->smsFavoriteModel = $smsFavorite;
+    }
+
+    /**
+     * SMS 설정 > 자주보내는 문자
+     * @method GET|POST
+     * @see /admin/popup/sms
+     */
+    public function favorite(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $smsFavoriteData = $this->smsFavoriteModel->data($params);
+
+        if ($smsFavoriteData->rows > 0) {
+            $num = listNum($smsFavoriteData->total, $params->page, $params->perPage);
+            foreach ($smsFavoriteData->list as $i => $row) {
+                $row->num = $num--;
+                $smsFavoriteData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.popup.sms.favorite', [
+            'smsFavoriteData' => $smsFavoriteData,
+            'params' => $params
+        ]);
+    }
+}

+ 89 - 0
app/Http/Controllers/Admin/Popup/UserController.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Popup;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\UserGroup;
+use App\Models\UserGrade;
+use App\Models\DTO\SearchData;
+
+class UserController extends Controller
+{
+    private User $userModel;
+    private UserGroup $userGroupModel;
+    private UserGrade $userGradeModel;
+
+    public function __construct(
+        User $user,
+        UserGroup $userGroup,
+        UserGrade $userGrade
+    ) {
+        $this->userModel = $user;
+        $this->userGroupModel = $userGroup;
+        $this->userGradeModel = $userGrade;
+    }
+
+    /**
+     * AJAX 회원 검색
+     * @method GET
+     * @see /admin/popup/user
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->activated = $request->get('activated');
+        $params->isAdmin = $request->get('is_admin');
+        $params->isDenied = $request->get('is_denied');
+        $params->isWithdraw = $request->get('is_withdraw');
+        $params->userGroupID = $request->get('user_group_id', []);
+        $params->userGradeID = $request->get('user_grade_id', []);
+
+        $userData = $this->userModel->data($params);
+
+        if ($userData->rows > 0) {
+            $num = listNum($userData->total, $params->page, $params->perPage);
+            foreach ($userData->list as $i => $row) {
+                $row->num = $num--;
+                $row->point = number_format($row->point);
+
+                $userData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.popup.user.index', [
+            'userGroupData' => $this->userGroupModel->all(),
+            'userGradeData' => $this->userGradeModel->all(),
+            'userData' => $userData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * AJAX 회원그룹 검색
+     * @method GET
+     * @see /admin/popup/user/group
+     */
+    public function group()
+    {
+        $userGroupData = $this->userGroupModel->data();
+        $userTotalCount = $this->userModel->count(); // 전체 회원 수
+
+        if($userGroupData->rows > 0) {
+            $num = 1;
+            foreach($userGroupData->list as $i =>$row) {
+                $row->num = $num++;
+                $row->userGroupCount = $row->user->count();
+                $row->strName = ($row->kor_name . ($row->eng_name ? ' (' . $row->eng_name . ')' : ''));
+                $row->strShare = (($row->userGroupCount > 0) ? round(($row->userGroupCount / $userTotalCount) * 100) : 0);
+
+                $userGroupData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.popup.user.group', [
+            'userGroupData' => $userGroupData
+        ]);
+    }
+}

+ 161 - 0
app/Http/Controllers/Admin/Sms/Book/ListController.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms\Book;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use App\Http\Controllers\Controller;
+use App\Models\SmsBook;
+use App\Models\DTO\SearchData;
+
+class ListController extends Controller
+{
+    private SmsBook $smsBookModel;
+
+    public function __construct(SmsBook $smsBook)
+    {
+        $this->smsBookModel = $smsBook;
+    }
+
+    /**
+     * 주소록 관리 목록
+     * @method GET
+     * @see /admin/sms/send
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $smsBookData = $this->smsBookModel->data($params);
+
+        if ($smsBookData->rows > 0) {
+            $num = listNum($smsBookData->total, $params->page, $params->perPage);
+            foreach ($smsBookData->list as $i => $row) {
+                $row->num = $num--;
+                $row->bookUserListURL = route('admin.sms.book.user.index') . '?book=' . $row->id;
+                $row->editURL = route('admin.sms.book.list.edit', $row->id);
+                $smsBookData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.book.list.index', [
+            'smsBookData' => $smsBookData
+        ]);
+    }
+
+    /**
+     * 주소록 관리 등록
+     * @method GET
+     * @see /admin/sms/send/create
+     */
+    public function create()
+    {
+        return view('admin.sms.book.list.write', [
+            'actionURL' => route('admin.sms.book.list.store'),
+            'smsBookData' => [],
+            'smsBookID' => null
+        ]);
+    }
+
+    /**
+     * 주소록 관리 수정
+     * @method GET
+     * @see /admin/sms/send/{pk}/edit
+     */
+    public function edit(int $smsBookID)
+    {
+        return view('admin.sms.book.list.write', [
+            'actionURL' => route('admin.sms.book.list.update', $smsBookID),
+            'smsBookData' => $this->smsBookModel->find($smsBookID),
+            'smsBookID' => $smsBookID
+        ]);
+    }
+
+    /**
+     * 주소록 관리 등록
+     * @method POST
+     * @see /admin/sms/send/create
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'name' => 'required|string|max:255|unique:tb_sms_book,name',
+            'order' => 'required|numeric'
+        ];
+
+        $attributes = [
+            'name' => '이름',
+            'order' => '순서'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->smsBookModel->insert([
+            'name' => $posts['name'],
+            'order' => $posts['order'],
+            'created_at' => now()
+        ]);
+
+        $message = '주소록이 등록되었습니다.';
+        return redirect()->route('admin.sms.book.list.index')->with('message', $message);
+    }
+
+    /**
+     * 주소록 관리 수정
+     * @method PUT
+     * @see /admin/sms/send
+     */
+    public function update(Request $request)
+    {
+        $smsBookID = $request->post('sms_book_id');
+
+        $rules = [
+            'sms_book_id' => 'required|numeric|exists:tb_sms_book,id',
+            'name' => 'required|string|max:255' . Rule::unique('tb_sms_book', 'name')->ignore($smsBookID, 'id'),
+            'order' => 'required|numeric'
+        ];
+
+        $attributes = [
+            'sms_book_id' => 'PK',
+            'name' => '이름',
+            'order' => '순서'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->smsBookModel->find($posts['sms_book_id'])->update([
+            'name' => $posts['name'],
+            'order' => $posts['order'],
+            'updated_at' => now()
+        ]);
+
+        $message = '주소록이 수정되었습니다.';
+        return redirect()->route('admin.sms.book.list.edit', $posts['sms_book_id'])->with('message', $message);
+    }
+
+    /**
+     * 주소록 관리 삭제
+     * @method DELETE
+     * @see /admin/sms/send/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $smsBookID) {
+                $smsBook = $this->smsBookModel->find($smsBookID);
+
+                // 연락처 그룹에 회원이 등록되면 삭제 불가
+                $userRows = $smsBook->smsUser->count();
+                if($userRows > 0) {
+                    return back()->withErrors("{$smsBookID}번 주소록은 삭제할 수 없습니다.")->withInput();
+                }
+
+                $smsBook->delete();
+            }
+        }
+
+        $message = '주소록이 삭제되었습니다.';
+        return redirect()->route('admin.sms.book.list.index')->with('message', $message);
+    }
+}

+ 155 - 0
app/Http/Controllers/Admin/Sms/Book/UserController.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms\Book;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsUser;
+use App\Models\User;
+use App\Models\UserGroup;
+use App\Models\UserGrade;
+use App\Models\DTO\SearchData;
+
+class UserController extends Controller
+{
+    private ?int $book;
+
+    private User $userModel;
+    private UserGroup $userGroupModel;
+    private UserGrade $userGradeModel;
+    private SmsUser $smsUserModel;
+
+    public function __construct(
+        User $user,
+        UserGroup $userGroup,
+        UserGrade $userGrade,
+        SmsUser $smsUser
+    ) {
+        $this->userModel = $user;
+        $this->userGroupModel = $userGroup;
+        $this->userGradeModel = $userGrade;
+        $this->smsUserModel = $smsUser;
+
+        $this->book = request()->input('book');
+        if(!$this->book) {
+            return redirect()->route('admin.sms.book.list.index')->with('message', 'Book ID는 필수 입니다.');
+        }
+    }
+
+    /**
+     * 연락처 관리 - 목록
+     * @method GET
+     * @see /admin/sms/book/user
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->userGroupID = $request->get('user_group_id', []);
+        $params->userGradeID = $request->get('user_grade_id', []);
+        $params->smsBookID = $this->book;
+
+        $smsUserData = $this->smsUserModel->data($params);
+
+        if ($smsUserData->rows > 0) {
+            $num = listNum($smsUserData->total, $params->page, $params->perPage);
+            foreach ($smsUserData->list as $i => $row) {
+                $row->num = $num--;
+
+                $row->searchURL = route('admin.user.list.index') . '?' . http_build_query([
+                    'field' => 'users.id',
+                    'keyword' => $row->user_id
+                ]);
+
+                $smsUserData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.book.user.index', [
+            'smsUserData' => $smsUserData,
+            'userGroupData' => $this->userGroupModel->all(),
+            'userGradeData' => $this->userGradeModel->all(),
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 주소록 회원 등록
+     * @method GET
+     * @see /admin/sms/book/user/create
+     */
+    public function create()
+    {
+        return view('admin.sms.book.user.write', [
+            'actionURL' => route('admin.sms.book.user.store'),
+            'book' => $this->book
+        ]);
+    }
+
+    /**
+     * 주소록 회원 저장
+     * @method POST
+     * @see /admin/sms/book/user
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'users' => 'required|string'
+        ];
+
+        $attributes = [
+            'users' => '회원 ID'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $today = now();
+
+        $users = explode(',', $posts['users']);
+        foreach($users as $sid) {
+            $user = $this->userModel->findByUserID($sid);
+            if(!$user->exists) {
+                return back()->withErrors(['sid' => "{$sid} 는 존재하지 않는 회원입니다."])->withInput();
+            }else{
+
+                // 이미 추가된 회원이면 무시
+                if($this->smsUserModel->where([
+                    ['sms_book_id', $this->book],
+                    ['user_id', $user->id]
+                ])->exists()) {
+                   continue;
+                }
+
+                $this->smsUserModel->insert([
+                    'sms_book_id' => $this->book,
+                    'user_id' => $user->id,
+                    'created_at' => $today
+                ]);
+            }
+        }
+
+        $message = '주소록에 회원이 등록되었습니다.';
+        return redirect()->route('admin.sms.book.user.index', [
+            'book' => $this->book
+        ])->with('message', $message);
+    }
+
+    /**
+     * 주소록 회원 삭제
+     * @method DELETE
+     * @see /admin/sms/book/users
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $smsUserID) {
+                $this->smsUserModel->find($smsUserID)->delete();
+            }
+        }
+
+        $message = '주소록에서 회원 정보가 삭제되었습니다.';
+        return redirect()->route('admin.sms.book.user.index', [
+            'book' => $this->book
+        ])->with('message', $message);
+    }
+}

+ 69 - 0
app/Http/Controllers/Admin/Sms/ConfigController.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+use App\Models\SmsLib;
+
+class ConfigController extends Controller
+{
+    private Config $configModel;
+    private SmsLib $smsLib;
+    private array $smsConfig;
+
+    public function __construct(Config $config, SmsLib $smsLib)
+    {
+        $this->configModel = $config;
+        $this->smsLib = $smsLib;
+        $this->smsConfig = $this->smsLib->info();
+    }
+
+    /**
+     * 환경 설정
+     * @method GET
+     * @see /admin/sms/config
+     */
+    public function index()
+    {
+        return view('admin.sms.config.index', [
+            'icodeRet' => $this->smsConfig
+        ]);
+    }
+
+    /**
+     * 환경 설정 저장
+     * @method POST
+     * @see /admin/sms/config
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'use_sms' => 'numeric|in:0,1',
+            'sms_icode_id' => 'string|nullable',
+            'sms_icode_pw' => 'string|nullable',
+            'sms_icode_call_num' => 'string|nullable',
+            'sms_icode_host' => 'string|nullable',
+            'sms_icode_port' => 'numeric|nullable',
+            'sms_icode_token' => 'string|nullable|max:255'
+        ];
+
+        $attributes = [
+            'use_sms' => 'SMS 기능 ',
+            'sms_icode_id' => '아이코드 ID',
+            'sms_icode_pw' => '아이코드 PW',
+            'sms_icode_call_num' => '문자 발송 번호',
+            'sms_icode_host' => '서버 IP',
+            'sms_icode_port' => '서버 Port',
+            'sms_icode_token' => '토큰키 (Token key)'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '환경 설정 정보가 저장되었습니다.';
+        return redirect()->route('admin.sms.config.index')->with('message', $message);
+    }
+}

+ 157 - 0
app/Http/Controllers/Admin/Sms/FavoriteController.php

@@ -0,0 +1,157 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsFavorite;
+use App\Models\DTO\SearchData;
+
+class FavoriteController extends Controller
+{
+    private SmsFavorite $smsFavoriteModel;
+
+    public function __construct(SmsFavorite $smsFavorite)
+    {
+        $this->smsFavoriteModel = $smsFavorite;
+    }
+
+    /**
+     * SMS > 자주보내는 문자
+     * @method GET
+     * @see /admin/sms/favorite
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $smsFavoriteData = $this->smsFavoriteModel->data($params);
+
+        if ($smsFavoriteData->rows > 0) {
+            $num = listNum($smsFavoriteData->total, $params->page, $params->perPage);
+            foreach ($smsFavoriteData->list as $i => $row) {
+                $row->num = $num--;
+                $row->editURL = route('admin.sms.favorite.edit', $row->id);
+
+                $smsFavoriteData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.favorite.index', [
+            'smsFavoriteData' => $smsFavoriteData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 자주 보내는 문자 등록
+     * @method GET
+     * @see /admin/sms/favorite/create
+     */
+    public function create()
+    {
+        return view('admin.sms.favorite.write', [
+            'actionURL' => route('admin.sms.favorite.store'),
+            'smsFavoriteData' => [],
+            'favoriteID' => null
+        ]);
+    }
+
+    /**
+     * 자주 보내는 문자 수정
+     * @method GET
+     * @see /admin/sms/favorite/{pk}/edit
+     */
+    public function edit(int $favoriteID)
+    {
+        return view('admin.sms.favorite.write', [
+            'actionURL' => route('admin.sms.favorite.update', $favoriteID),
+            'smsFavoriteData' => $this->smsFavoriteModel->find($favoriteID),
+            'favoriteID' => $favoriteID
+        ]);
+    }
+
+    /**
+     * 자주 보내는 문자 등록 저장
+     * @method POST
+     * @see /admin/sms/favorite
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'type' => 'required|numeric|in:1,2',
+            'subject' => 'required|string|max:255',
+            'content' => 'required|string'
+        ];
+
+        $attributes = [
+            'type' => '형식',
+            'subject' => '제목',
+            'content' => '내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->smsFavoriteModel->insert([
+            'type' => $posts['type'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'created_at' => now()
+        ]);
+
+        $message = '자주 보내는 문자가 등록되었습니다.';
+        return redirect()->route('admin.sms.favorite.index')->with('message', $message);
+    }
+
+    /**
+     * 자주 보내는 문자 수정 저장
+     * @method PUT
+     * @see /admin/sms/favorite
+     */
+    public function update(Request $request)
+    {
+        $rules = [
+            'favorite_id' => 'required|numeric|exists:tb_sms_favorite,id',
+            'type' => 'required|numeric|in:1,2',
+            'subject' => 'required|string|max:255',
+            'content' => 'required|string'
+        ];
+
+        $attributes = [
+            'favorite_id' => '번호',
+            'type' => '형식',
+            'subject' => '제목',
+            'content' => '내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->smsFavoriteModel->find($posts['favorite_id'])->update([
+            'type' => $posts['type'],
+            'subject' => $posts['subject'],
+            'content' => $posts['content'],
+            'updated_at' => now()
+        ]);
+
+        $message = '자주 보내는 문자가 수정되었습니다.';
+        return redirect()->route('admin.sms.favorite.edit', $posts['favorite_id'])->with('message', $message);
+    }
+
+    /**
+     * 자주 보내는 문자 삭제
+     * @method DELETE
+     * @see /admin/sms/favorite
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $favoriteID) {
+                $this->smsFavoriteModel->find($favoriteID)->delete();
+            }
+        }
+
+        $message = '자주 보내는 문자 정보가 삭제되었습니다.';
+        return redirect()->route('admin.sms.favorite.index')->with('message', $message);
+    }
+}

+ 65 - 0
app/Http/Controllers/Admin/Sms/HistoryController.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsHistory;
+use App\Models\DTO\SearchData;
+
+class HistoryController extends Controller
+{
+    private SmsHistory $smsHistoryModel;
+
+    public function __construct(SmsHistory $smsHistory)
+    {
+        $this->smsHistoryModel = $smsHistory;
+    }
+
+    /**
+     * SMS > 전송 내역
+     * @method GET
+     * @see /admin/sms/history
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $smsHistoryData = $this->smsHistoryModel->data($params);
+
+        if ($smsHistoryData->rows > 0) {
+            $num = listNum($smsHistoryData->total, $params->page, $params->perPage);
+            foreach ($smsHistoryData->list as $i => $row) {
+                $row->num = $num--;
+                $row->isReserveStr = ($row->is_reserve ? 'Y' : 'N');
+                $row->reserveAt = dateBr($row->reserve_at, '-');
+                $row->createdAt = dateBr($row->created_at);
+
+                $smsHistoryData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.history.index', [
+            'smsHistoryData' => $smsHistoryData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 전송내역 삭제
+     * @method DELETE
+     * @see /admin/sms/history
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $historyID) {
+                $this->smsHistoryModel->find($historyID)->delete();
+            }
+        }
+
+        $message = '전송 내역이 삭제되었습니다.';
+        return redirect()->route('admin.sms.history.index')->with('message', $message);
+    }
+}

+ 68 - 0
app/Http/Controllers/Admin/Sms/ResultController.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsResult;
+use App\Models\DTO\SearchData;
+
+class ResultController extends Controller
+{
+    private SmsResult $smsResultModel;
+
+    public function __construct(SmsResult $smsResult)
+    {
+        $this->smsResultModel = $smsResult;
+    }
+
+    /**
+     * SMS > 전송 결과
+     * @method GET
+     * @see /admin/sms/result
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $smsResultData = $this->smsResultModel->data($params);
+
+        if ($smsResultData->rows > 0) {
+            $num = listNum($smsResultData->total, $params->page, $params->perPage);
+            foreach ($smsResultData->list as $i => $row) {
+                $row->num = $num--;
+                $row->callNum = ($row->call_num ?: '-');
+                $row->totalCount = number_format($row->total_count);
+                $row->successCount = number_format($row->success_count);
+                $row->failedCount = number_format($row->failed_count);
+                $row->reserveAt = dateBr($row->reserve_at, '-');
+                $row->createdAt = dateBr($row->created_at);
+
+                $smsResultData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.result.index', [
+            'smsResultData' => $smsResultData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * SMS > 전송 결과 삭제
+     * @method DELETE
+     * @see /admin/sms/result/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $resultID) {
+                $this->smsResultModel->find($resultID)->delete();
+            }
+        }
+
+        $message = '전송 결과가 삭제되었습니다.';
+        return redirect()->route('admin.sms.result.index')->with('message', $message);
+    }
+}

+ 166 - 0
app/Http/Controllers/Admin/Sms/SendController.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Sms;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\SmsHistory;
+use App\Models\SmsBook;
+use App\Models\SmsUser;
+use App\Models\SmsLib;
+use App\Models\User;
+use App\Models\DTO\SearchData;
+
+class SendController extends Controller
+{
+    private SmsHistory $smsHistoryModel;
+    private SmsBook $smsBookModel;
+    private SmsUser $smsUserModel;
+    private User $userModel;
+    private SmsLib $smsLib;
+
+    public function __construct(
+        SmsHistory $smsHistory,
+        SmsBook $smsBook,
+        SmsUser $smsUser,
+        User $user,
+        SmsLib $smsLib
+    ) {
+        $this->smsHistoryModel = $smsHistory;
+        $this->smsBookModel = $smsBook;
+        $this->smsUserModel = $smsUser;
+        $this->userModel = $user;
+        $this->smsLib = $smsLib;
+    }
+
+    /**
+     * 문자 발송 관리
+     * @method GET
+     * @see /admin/sms/send
+     */
+    public function index()
+    {
+        return view('admin.sms.send.index', [
+            'actionURL' => route('admin.sms.send.store'),
+            'csrfToken' => csrf_token(),
+            'smsGroups' => $this->smsBookModel->all()
+        ]);
+    }
+
+    /**
+     * 회원 연락처 전체 조회
+     * @method GET
+     * @see /admin/sms/send/user
+     */
+    public function user(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->receiveSms = 1;
+        $params->actionURL = route('admin.sms.send.store');
+
+        $userData = $this->userModel->data($params);
+
+        if ($userData->rows > 0) {
+            $num = listNum($userData->total, $params->page, $params->perPage);
+            foreach ($userData->list as $i => $row) {
+                $row->num = $num--;
+                $userData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.send.contact', [
+            'userData' => $userData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 회원 연락처 그룹 조회
+     * @method GET
+     * @see /admin/sms/send/book
+     */
+    public function book(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->smsBookID = $request->get('book');
+        $params->actionURL = route('admin.sms.send.store');
+
+        $userData = $this->smsUserModel->data($params);
+
+        if ($userData->rows > 0) {
+            $num = listNum($userData->total, $params->page, $params->perPage);
+            foreach ($userData->list as $i => $row) {
+                $row->num = $num--;
+                $row->id = $row->user_id;
+                $userData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.sms.send.contact', [
+            'userData' => $userData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 문자 발송 저장
+     * @method POST
+     * @see /admin/sms/send
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'user_ids' => 'required|string',
+            'sms_subject' => 'required|string|max:30',
+            'sms_content' => 'required|string|max:2000',
+            'sms_tel' => 'nullable|required_without:sms_per_tel',
+            'sms_per_tel' => 'nullable|required_without:sms_tel',
+            'sms_is_reserve' => 'numeric|nullable|in:0,1',
+            'sms_reserve_at' => 'string|nullable|date'
+        ];
+
+        $attributes = [
+            'user_ids' => '수신 회원 PK',
+            'sms_subject' => '제목',
+            'sms_content' => '내용',
+            'sms_tel' => '발신 번호',
+            'sms_per_tel' => '발신 번호',
+            'sms_is_reserve' => '예약 여부',
+            'sms_reserve_at' => '예약 일시'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $userIDs = json_decode($posts['user_ids'], true); // input.hidden 으로 user_id 와 연락처를 받음
+
+        $sendNumber = []; // 수신번호
+        foreach ($userIDs as $userID => $tel) {
+            if($userID) {
+                $sendNumber[$userID] = $tel;
+            }else{
+                $sendNumber[] = $tel;
+            }
+        }
+
+        // 개인 발송
+        if($posts['sms_per_tel']) {
+            foreach(explode(',', $posts['sms_per_tel']) as $tel) {
+                $sendNumber[] = $tel;
+            }
+        }
+
+        $result = $this->smsLib->send($sendNumber, [
+            'userID' => UID, // 발신자 PK
+            'subject' => $posts['sms_subject'],
+            'content' => $posts['sms_content'],
+            'isReserve' => ($posts['sms_is_reserve'] ?? 0),
+            'reserveAt' => ($posts['sms_reserve_at'] ?? null)
+        ]);
+
+        if (!$result) {
+            return back()->withErrors($this->smsLib->errors())->withInput();
+        }
+
+        $message = $this->smsLib->report();
+        return redirect()->route('admin.sms.send.index')->with('message', $message);
+    }
+}

+ 70 - 0
app/Http/Controllers/Admin/User/Dormant/ConfigController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Dormant;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class ConfigController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 휴면계정 - 환경설정
+     * @method GET
+     * @see /admin/user/point
+     */
+    public function index()
+    {
+        return view('admin.user.dormant.config', []);
+    }
+
+    /**
+     * 휴면계정 - 환경설정 저장
+     * @method POST
+     * @see /admin/user/point
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'user_dormant_use' => 'required|numeric|in:0,1',
+            'user_auto_withdraw_day' => 'required|numeric|in:3,5,7,14,30',
+            'user_withdraw_shop_review_delete' => 'numeric|nullable',
+            'user_withdraw_shop_qna_delete' => 'numeric|nullable',
+            'user_withdraw_shop_order_delete' => 'numeric|nullable',
+            'user_withdraw_board_qna_delete' => 'numeric|nullable',
+            'user_withdraw_shop_wish_list_delete' => 'numeric|nullable',
+            'user_withdraw_shop_cart_delete' => 'numeric|nullable'
+        ];
+
+        $attributes = [
+            'user_dormant_use' => '휴면계정 전환 여부',
+            'user_auto_withdraw_day' => '탈퇴 요청회원 자동삭제 기한',
+            'user_withdraw_shop_review_delete' => '탈퇴 요청회원 자동삭제 자료 - 구매후기',
+            'user_withdraw_shop_qna_delete' => '탈퇴 요청회원 자동삭제 자료 - 상품문의',
+            'user_withdraw_shop_order_delete' => '탈퇴 요청회원 자동삭제 자료 - 주문내역',
+            'user_withdraw_board_qna_delete' => '탈퇴 요청회원 자동삭제 자료 - 1:1 상담',
+            'user_withdraw_shop_wish_list_delete' => '탈퇴 요청회원 자동삭제 자료 - 관심상품',
+            'user_withdraw_shop_cart_delete' => '탈퇴 요청회원 자동삭제 자료 - 장바구니'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $posts['user_withdraw_shop_review_delete'] ??= 0;
+        $posts['user_withdraw_shop_qna_delete'] ??= 0;
+        $posts['user_withdraw_shop_order_delete'] ??= 0;
+        $posts['user_withdraw_board_qna_delete'] ??= 0;
+        $posts['user_withdraw_shop_wish_list_delete'] ??= 0;
+        $posts['user_withdraw_shop_cart_delete'] ??= 0;
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '환경설정이 저장되었습니다.';
+        return redirect()->route('admin.user.dormant.config.index')->with('message', $message);
+    }
+}

+ 60 - 0
app/Http/Controllers/Admin/User/Dormant/Form/EmailController.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Dormant\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class EmailController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 휴면 계정 관리(이메일) - 알림 발송 양식
+     * @method GET
+     * @see /admin/user/dormant/form/email
+     */
+    public function index()
+    {
+        return view('admin.user.dormant.form.email', []);
+    }
+
+    /**
+     * 휴면 계정 관리(이메일) - 알림 발송 양식 저장
+     * @method POST
+     * @see /admin/user/dormant/form/email
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_email_dormant_form_title' => 'string|nullable',
+            'send_email_dormant_form_content' => 'string|nullable',
+            'send_email_dormancy_form_title' => 'string|nullable',
+            'send_email_dormancy_form_content' => 'string|nullable',
+            'send_email_recover_form_title' => 'string|nullable',
+            'send_email_recover_form_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'send_email_dormant_form_title' => '휴면 예정 - 제목',
+            'send_email_dormant_form_content' => '휴면 예정 - 내용',
+            'send_email_dormancy_form_title' => '휴면 전환 - 제목',
+            'send_email_dormancy_form_content' => '휴면 전환 - 내용',
+            'send_email_recover_form_title' => '휴면 해제 - 제목',
+            'send_email_recover_form_content' => '휴면 해제 - 내용'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '이메일 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.user.dormant.form.email.index')->with('message', $message);
+    }
+}

+ 54 - 0
app/Http/Controllers/Admin/User/Dormant/Form/SmsController.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Dormant\Form;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\Config;
+
+class SmsController extends Controller
+{
+    private Config $configModel;
+
+    public function __construct(Config $config)
+    {
+        $this->configModel = $config;
+    }
+
+    /**
+     * 휴면 계정 관리(문자) - 알림 발송 양식
+     * @method GET
+     * @see /admin/user/dormant/form/sms
+     */
+    public function index()
+    {
+        return view('admin.user.dormant.form.sms', []);
+    }
+
+    /**
+     * 휴면 계정 관리(문자) - 알림 발송 양식 저장
+     * @method POST
+     * @see /admin/user/dormant/form/sms
+     */
+    public function store(Request $request)
+    {
+        $rules = [
+            'send_sms_dormant_form_content' => 'string|nullable',
+            'send_sms_dormancy_form_content' => 'string|nullable',
+            'send_sms_recover_form_content' => 'string|nullable'
+        ];
+
+        $attributes = [
+            'send_sms_dormant_form_content' => '휴면 예정',
+            'send_sms_dormancy_form_content' => '휴면 전환',
+            'send_sms_recover_form_content' => '휴면 해제'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+
+        $this->configModel->save($posts, $attributes);
+
+        $message = '문자 양식 정보가 저장되었습니다.';
+        return redirect()->route('admin.user.dormant.form.sms.index')->with('message', $message);
+    }
+}

+ 169 - 0
app/Http/Controllers/Admin/User/Dormant/ListController.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Dormant;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\UserDormant;
+use App\Models\UserGroup;
+use App\Models\UserDormantNotify;
+use App\Models\EmailLib;
+use App\Models\DTO\SearchData;
+
+class ListController extends Controller
+{
+    private User $userModel;
+    private UserDormant $userDormantModel;
+    private UserDormantNotify $userDormantNotifyModel;
+    private UserGroup $userGroupModel;
+    private EmailLib $emailLib;
+
+    public function __construct(
+        User $user,
+        UserDormant $userDormant,
+        UserDormantNotify $userDormantNotify,
+        UserGroup $userGroup,
+        EmailLib $emailLib
+    ) {
+        $this->userModel = $user;
+        $this->userDormantModel = $userDormant;
+        $this->userDormantNotifyModel = $userDormantNotify;
+        $this->userGroupModel = $userGroup;
+        $this->emailLib = $emailLib;
+    }
+
+    /**
+     * 휴면계정 관리
+     * @method GET|POST
+     * @see /admin/user/dormant/list
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->activated = $request->get('activated');
+        $params->userGroupID = $request->get('user_group_id', []);
+        $params->isDormancy = $request->get('is_dormant', 0);
+
+        if (!$params->isDormancy) { // 회원
+            $sModel = $this->userModel;
+            $userDormantData = $sModel->dormantUsers($params);
+        }else{ // 휴면회원
+            $sModel = $this->userDormantModel;
+            $userDormantData = $sModel->data($params);
+        }
+
+        $sTable = $sModel->getTable();
+
+        if ($userDormantData->total > 0) {
+            $num = listNum($userDormantData->total, $params->page, $params->perPage);
+            foreach ($userDormantData->list as $i => $row) {
+                $row->num = $num--;
+                $row->point = number_format($row->point);
+                $row->lastLoginAt = dateBr($row->last_login_at, '-');
+                $row->createdAt = dateBr($row->created_at, '-');
+                $row->dormantAt = dateBr($row->dormant_at, '-');
+
+                $userDormantData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.dormant.index', [
+            'sTable' => $sTable,
+            'userDormantData' => $userDormantData,
+            'userGroupData' => $this->userGroupModel->all(), // 회원그룹
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 휴면예정 알림 발송
+     * @method POST
+     * @see /admin/user/dormant/list/notify
+     */
+    public function notify(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $userID) {
+
+                // 휴면예정 알림 발송
+                if(!$this->userDormantNotifyModel->register(
+                    $this->userModel->find($userID)
+                )) {
+                    return back()->withErrors([
+                        'message' => sprintf('%d번 회원은 알림 발송이 거부되었습니다.', $userID)
+                    ])->withInput();
+                }
+            }
+        }
+
+        $message = '선택한 회원에게 알림이 발송되었습니다.';
+        return redirect()->route('admin.user.dormant.list.index')->with('message', $message);
+    }
+
+    /**
+     * 휴면 처리
+     * @method POST
+     * @see /admin/user/dormant/list/dormancy
+     */
+    public function dormancy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $userID) {
+
+                // 휴면 처리
+                if($this->userDormantModel->dormancy($userID)) {
+
+                    // 휴면 알림 메일 발송
+                    $this->emailLib->send(
+                        SEND_MAIL_FORM_TYPE_12,
+                        $this->userDormantModel->find($userID)
+                    );
+
+                }else{
+                    return back()->withErrors([
+                        'message' => sprintf('%d번 회원 휴면 처리가 거부되었습니다.', $userID)
+                    ])->withInput();
+                }
+            }
+        }
+
+        $message = '선택한 회원이 휴면 처리되었습니다.';
+        return redirect()->route('admin.user.dormant.list.index')->with('message', $message);
+    }
+
+    /**
+     * 휴면 해제
+     * @method POST
+     * @see /admin/user/dormant/list/recover
+     */
+    public function recover(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $userID) {
+                if($this->userDormantModel->recover($userID)) {
+
+                    // 휴면 해제 메일 발송
+                    $this->emailLib->send(
+                        SEND_MAIL_FORM_TYPE_13,
+                        $this->userModel->find($userID)
+                    );
+
+                }else{
+                    return back()->withErrors([
+                        'message' => sprintf('%d번 회원 휴면 해제가 거부되었습니다.', $userID)
+                    ])->withInput();
+                }
+            }
+        }
+
+        $message = '선택한 회원이 휴면 해제되었습니다.';
+        return redirect()->route('admin.user.dormant.list.index')->with('message', $message);
+    }
+}

+ 80 - 0
app/Http/Controllers/Admin/User/Dormant/NotifyController.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Dormant;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\UserDormantNotify;
+use App\Models\DTO\SearchData;
+use Exception;
+
+class NotifyController extends Controller
+{
+    private UserDormantNotify $userDormantNotifyModel;
+
+    public function __construct(UserDormantNotify $userDormantNotify)
+    {
+        $this->userDormantNotifyModel = $userDormantNotify;
+    }
+
+    /**
+     * 휴면계정 관리 - 알림 발송 내역
+     * @method GET|POST
+     * @see /admin/user/dormant/notify
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->startDate = $request->get('start_date');
+        $params->endDate = $request->get('end_date');
+
+        $userDormantNotifyData = $this->userDormantNotifyModel->data($params);
+
+        if($userDormantNotifyData->rows > 0) {
+            $num = listNum($userDormantNotifyData->total, $params->page, $params->perPage);
+            foreach($userDormantNotifyData->list as $i => $row) {
+                $row->num = $num--;
+                $row->lastLoginAt = dateBr($row->last_login_at, '-');
+                $row->dormantAt = dateBr($row->dormant_at, '-');
+                $row->createdAt = dateBr($row->created_at, '-');
+
+                $userDormantNotifyData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.dormant.notify', [
+            'userDormantNotifyData' => $userDormantNotifyData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 휴면계정 관리 - 알림 발송 내역 삭제
+     * @method POST
+     * @see /admin/user/dormant/notify/destroy
+     */
+    public function destroy(Request $request)
+    {
+        try{
+
+            $chk = $request->post('chk');
+
+            if ($chk) {
+                foreach ($chk as $notifyID) {
+                    $notify = $this->userDormantNotifyModel->findOrNew($notifyID);
+
+                    if(!$notify->exists) {
+                        throw new Exception($notifyID . '번 알림은 존재하지 않습니다.');
+                    }
+
+                    $notify->delete();
+                }
+            }
+
+            $message = '알림 발송 내역이 삭제되었습니다.';
+            return redirect()->route('admin.user.dormant.notify.index')->with('message', $message);
+        }catch(Exception $e) {
+            return back()->withErrors($e->getMessage())->withInput();
+        }
+    }
+}

+ 154 - 0
app/Http/Controllers/Admin/User/GroupController.php

@@ -0,0 +1,154 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\UserGroup;
+
+class GroupController extends Controller
+{
+    private User $userModel;
+    private UserGroup $userGroupModel;
+
+    public function __construct(User $user, UserGroup $userGroup)
+    {
+        $this->userModel = $user;
+        $this->userGroupModel = $userGroup;
+    }
+
+    /**
+     * 회원그룹 관리
+     * @method GET
+     * @see /admin/user/group
+     */
+    public function index()
+    {
+        $userGroupData = $this->userGroupModel->data();
+
+        // 전체 회원 수
+        $totalUserCount = $this->userModel->count();
+
+        $userGroupIDs = [];
+        if ($userGroupData->total > 0) {
+            $num = 1;
+            foreach ($userGroupData->list as $i => $row) {
+                $row->num = $num++;
+                $userGroupIDs[] = $row->id;
+                $row->userCount = $row->user->count();
+                $row->updatedAt = dateBr($row->updated_at, '-');
+                $row->createdAt = dateBr($row->created_at, '-');
+                $row->share = (($row->userCount > 0) ? round(($row->userCount / $totalUserCount) * 100) : 0);
+
+                $userGroupData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.group.index', [
+            'userGroupData' => $userGroupData,
+            'totalUserCount' => $totalUserCount,
+            'userGroupID' => implode(',', $userGroupIDs)
+        ]);
+    }
+
+    /**
+     * 회원그룹 저장
+     * @method POST
+     * @see /admin/user/group/list
+     */
+    public function store(Request $request)
+    {
+        $posts = $request->all();
+        $oldUserGroupID = explode(',', $posts['old_user_group_id']); // 기존 회원그룹 PK
+        $curUserGroupID = $request->input('user_group_id', []);
+        $userID = UID;
+        $today = now();
+
+        if($curUserGroupID) {
+            $nLoop = 1;
+            foreach($curUserGroupID as $userGroupID => $pk) {
+                $saveData = [
+                    'user_group_id' => ($posts['user_group_id'][$userGroupID] ?? 0),
+                    'sort' => ($posts['sort'][$userGroupID] ?? 0),
+                    'kor_name' => ($posts['kor_name'][$userGroupID] ?? ''),
+                    'eng_name' => ($posts['eng_name'][$userGroupID] ?? ''),
+                    'description' => ($posts['description'][$userGroupID] ?? ''),
+                    'is_default' => ($posts['is_default'][$userGroupID] ?? 0),
+                    'is_use' => ($posts['is_use'][$userGroupID] ?? 0)
+                ];
+
+                $rules = [
+                    'image' => 'nullable|mimes:jpg,jpeg,gif,png|max:2192',
+                    'user_group_id' => 'required|numeric',
+                    'sort' => 'required|numeric',
+                    'kor_name' => 'required|string',
+                    'eng_name' => 'required|string',
+                    'description' => 'string|nullable',
+                    'is_default' => 'nullable|numeric',
+                    'is_use' => 'required|numeric|in:0,1'
+                ];
+
+                $attributes = [
+                    'image' => '이미지',
+                    'user_group_id' => '번호',
+                    'sort' => '순서',
+                    'kor_name' => '한글명',
+                    'eng_name' => '영문명',
+                    'description' => '설명',
+                    'is_default' => '기본 그룹',
+                    'is_use' => '사용 여부'
+                ];
+
+                $validator = Validator::make($saveData, $rules, [], $attributes);
+                if($validator->fails()) {
+                    return redirect()->route('admin.user.group.index')->withErrors($validator)->withInput();
+                }
+
+                // 파일 저장
+                $groupImages = $request->file('image');
+                if(isset($groupImages[$userGroupID])) {
+                    $groupImages[$userGroupID]->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . USER_GROUP_IMAGE_PATH);
+                    $saveData['image'] = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR . USER_GROUP_IMAGE_PATH . DIRECTORY_SEPARATOR . $groupImages[$userGroupID]->hashName());
+
+                }
+
+                // 파일 삭제
+                $groupImageDel = $request->get('image_del');
+                if(isset($groupImageDel[$userGroupID])) {
+                    if(file_exists($groupImageDel[$userGroupID])) {
+                        unlink($groupImageDel[$userGroupID]);
+                    }
+                    $saveData['image'] = null;
+                }
+
+                unset($saveData['user_group_id']);
+
+                if(!$pk) {
+                    $saveData['id'] = $userGroupID;
+                    $saveData['created_at'] = $today;
+                    $this->userGroupModel->insert($saveData);
+                }else{
+                    $saveData['updated_user_id'] = $userID;
+                    $saveData['updated_at'] = $today;
+                    $this->userGroupModel->find($pk)->update($saveData);
+                }
+
+                $nLoop++;
+            }
+        }
+
+        // 값이 없는 경우 삭제 처리
+        if($oldUserGroupID) {
+            foreach($oldUserGroupID as $userGroupID) {
+                if(!in_array($userGroupID, $curUserGroupID)) {
+                    $this->userGroupModel->find($userGroupID)->delete();
+                }
+            }
+        }
+
+        $message = '회원그룹 정보가 변경되었습니다.';
+        return redirect()->route('admin.user.group.index')->with('message', $message);
+    }
+}

+ 356 - 0
app/Http/Controllers/Admin/User/ListController.php

@@ -0,0 +1,356 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User;
+
+use Illuminate\Http\Request;
+use Illuminate\Validation\Rule;
+use App\Http\Controllers\Controller;
+use App\Models\User;
+use App\Models\UserGroup;
+use App\Models\Config;
+use App\Models\UserRegister;
+use App\Models\DTO\SearchData;
+use App\Rules\NumberLength;
+use App\Rules\SpecialCharLength;
+use App\Rules\UppercaseLength;
+use App\Rules\DeniedEmail;
+
+class ListController extends Controller
+{
+    private User $userModel;
+    private UserGroup $userGroupModel;
+    private UserRegister $userRegisterModel;
+
+    public function __construct(
+        User $user,
+        UserGroup $userGroup,
+        UserRegister $userRegister
+    ) {
+        $this->userModel = $user;
+        $this->userGroupModel = $userGroup;
+        $this->userRegisterModel = $userRegister;
+    }
+
+    /**
+     * 회원 관리
+     * @method GET
+     * @see /admin/user/list
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $params->activated = $request->get('activated');
+        $params->isAdmin = $request->get('is_admin');
+        $params->isDenied = $request->get('is_denied');
+        $params->isWithdraw = $request->get('is_withdraw');
+        $params->userGroupID = $request->get('user_group_id', []);
+
+        $userData = $this->userModel->data($params);
+
+        if ($userData->rows > 0) {
+            $num = listNum($userData->total, $params->page, $params->perPage);
+            foreach ($userData->list as $i => $row) {
+                $row->num = $num--;
+                $row->lastLoginAt = dateBr($row->last_login_at, '-');
+                $row->deletedAt = dateBr($row->deleted_at, '-');
+                $row->createdAt = dateBr($row->created_at);
+                $row->editURL = route('admin.user.list.edit', $row->id);
+
+                $userData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.list.index', [
+            'userData' => $userData,
+            'userGroupData' => $this->userGroupModel->getAllGroup(),
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 회원 등록
+     * @method GET
+     * @see /admin/user/create
+     */
+    public function create()
+    {
+        return view('admin.user.list.write', [
+            'actionURL' => route('admin.user.list.store'),
+            'userData' => [],
+            'uid' => null
+        ]);
+    }
+
+    /**
+     * 회원 수정
+     * @method GET
+     * @see /admin/user/{pk}/edit
+     */
+    public function edit(int $uid)
+    {
+        return view('admin.user.list.write', [
+            'actionURL' => route('admin.user.list.update', $uid),
+            'userData' => $this->userModel->find($uid),
+            'uid' => $uid
+        ]);
+    }
+
+    /**
+     * 회원 등록 저장
+     * @method POST
+     * @see /admin/user/list
+     */
+    public function store(Request $request, Config $config)
+    {
+        // 비밀번호 유효성 검사 규칙 지정
+        $passwordRule = ['required', 'confirmed'];
+        $passwordMinLength = $config->item('password_min_length');
+
+        // 비밀번호 최소 길이
+        if($passwordMinLength > 0) {
+            $passwordRule[] = 'min:' . $passwordMinLength;
+            $passwordRule[] = new UppercaseLength;
+            $passwordRule[] = new NumberLength;
+            $passwordRule[] = new SpecialCharLength;
+        }
+
+        $rules = [
+            'email' => ['required', 'email', 'unique:users,email', new DeniedEmail],
+            'name' => 'required|string|min:2|max:20',
+            'nickname' => 'required|string|min:2|max:20|unique:users,nickname',
+            'password' => $passwordRule,
+            'thumb_img' => 'nullable|mimes:jpg,jpeg,gif,png|max:3192',
+            'about_me' => 'string|nullable|max:500',
+            'receive_email' => 'nullable|numeric|in:0,1',
+            'is_denied' => 'nullable|numeric|in:0,1',
+            'is_withdraw' => 'nullable|numeric|in:0,1',
+            'is_admin' => 'nullable|numeric|in:0,1',
+            'is_open_profile' => 'nullable|numeric|in:0,1'
+        ];
+
+        $attributes = [
+            'email' => '이메일',
+            'name' => '이름',
+            'nickname' => '닉네임',
+            'password' => '비밀번호',
+            'thumb_img' => '프로필 이미지',
+            'about_me' => '자기소개',
+            'receive_email' => '이메일 수신 여부',
+            'is_denied' => '차단 여부',
+            'is_withdraw' => '탈퇴 여부',
+            'is_admin' => '관리자 여부',
+            'is_open_profile' => '정보 공개 여부'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        [$sid] = explode('@', $posts['email']);
+
+        $saveData = [
+            'sid' => $sid,
+            'name' => $posts['name'],
+            'nickname' => $posts['nickname'],
+            'email' => $posts['email'],
+            'email_verified_at' => now(),
+            'password' => bcrypt($posts['password']),
+            'thumb' => ($posts['thumb_img'] ?? 0),
+            'about_me' => $posts['about_me'],
+            'remember_token' => null,
+            'is_email_cert' => 1,
+            'is_denied' => ($posts['is_denied'] ?? 0),
+            'is_withdraw' => ($posts['is_withdraw'] ?? 0),
+            'is_admin' => ($posts['is_admin'] ?? 0),
+            'is_open_profile' => ($posts['is_open_profile'] ?? 0),
+            'receive_email' => ($posts['receive_email'] ?? 0),
+            'register_ip' => IP_ADDRESS,
+            'last_login_ip' => null,
+            'last_login_at' => null,
+            'password_updated_at' => now(),
+            'deleted_at' => null,
+            'created_at' => now(),
+            'updated_at' => null,
+        ];
+
+        // 파일 저장
+        if($request->hasFile('thumb')) {
+            $thumb = $request->file('thumb');
+            $thumb->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_USER_THUMB);
+            $saveData['thumb'] = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR . UPLOAD_PATH_USER_THUMB . DIRECTORY_SEPARATOR . $thumb->hashName());
+        }
+
+        // 파일 삭제
+        if($request->get('thumb_del')) {
+            $thumbPath = $request->get('thumb_url');
+            if(file_exists($thumbPath)) {
+                unlink($thumbPath);
+            }
+            $saveData['thumb'] = null;
+        }
+
+        $uid = $this->userModel->insertGetId($saveData);
+
+        $this->userRegisterModel->insert([
+            'user_id' => $uid,
+            'device' => DEVICE_TYPE,
+            'language' => null,
+            'browser' => BROWSER,
+            'platform' => PLATFORM,
+            'robot' => null,
+            'ip_address' => IP_ADDRESS,
+            'user_agent' => USER_AGENT,
+            'referer' => REFERER
+        ]);
+
+        $message = '회원이 등록되었습니다.';
+        return redirect()->route('admin.user.list.index')->with('message', $message);
+    }
+
+    /**
+     * 회원 수정 저장
+     * @method PUT
+     * @see /admin/user/list/{pk}
+     */
+    public function update(int $uid, Request $request, Config $config)
+    {
+        // 비밀번호 유효성 검사 규칙 지정
+        $passwordRule = ['nullable', 'confirmed'];
+        $passwordMinLength = $config->item('password_min_length');
+
+        // 비밀번호 최소 길이
+        if($passwordMinLength > 0) {
+            $passwordRule[] = 'min:' . $passwordMinLength;
+            $passwordRule[] = new UppercaseLength;
+            $passwordRule[] = new NumberLength;
+            $passwordRule[] = new SpecialCharLength;
+        }
+
+        $rules = [
+            'uid' => 'required|numeric|exists:users,id',
+            'email' => [
+                'required',
+                'email',
+                Rule::unique('users', 'email')->ignore($uid, 'id'),
+                new DeniedEmail
+            ],
+            'name' => 'required|string|min:2|max:20',
+            'nickname' => 'required|string|min:2|max:20|' . Rule::unique('users', 'nickname')->ignore($uid, 'id'),
+            'password' => $passwordRule,
+            'thumb_img' => 'nullable|mimes:jpg,jpeg,gif,png|max:3192',
+            'about_me' => 'string|nullable|max:500',
+            'receive_email' => 'nullable|numeric|in:0,1',
+            'is_denied' => 'nullable|numeric|in:0,1',
+            'is_withdraw' => 'nullable|numeric|in:0,1',
+            'is_admin' => 'nullable|numeric|in:0,1',
+            'is_open_profile' => 'nullable|numeric|in:0,1'
+        ];
+
+        $attributes = [
+            'uid' => '회원 ID',
+            'email' => '이메일',
+            'name' => '이름',
+            'nickname' => '닉네임',
+            'password' => '비밀번호',
+            'thumb_img' => '프로필 이미지',
+            'about_me' => '자기소개',
+            'receive_email' => '이메일 수신 여부',
+            'is_denied' => '차단 여부',
+            'is_withdraw' => '탈퇴 여부',
+            'is_admin' => '관리자 여부',
+            'is_open_profile' => '정보 공개 여부'
+        ];
+
+        $posts = $this->validate($request, $rules, [], $attributes);
+        $user = $this->userModel->find($uid);
+
+        $saveData = [
+            'name' => $posts['name'],
+            'nickname' => $posts['nickname'],
+            'email' => $posts['email'],
+            'password' => bcrypt($posts['password']),
+            'thumb' => ($posts['thumb_img'] ?? 0),
+            'about_me' => $posts['about_me'],
+            'receive_email' => ($posts['receive_email'] ?? 0),
+            'is_denied' => ($posts['is_denied'] ?? 0),
+            'is_withdraw' => ($posts['is_withdraw'] ?? 0),
+            'is_admin' => ($posts['is_admin'] ?? 0),
+            'is_open_profile' => ($posts['is_open_profile'] ?? 0),
+            'deleted_at' => null,
+            'updated_at' => now()
+        ];
+
+        if($user->password != $saveData['password']) {
+            $saveData['password_updated_at'] = now();
+        }
+
+        // 파일 저장
+        if($request->hasFile('thumb')) {
+            $thumb = $request->file('thumb');
+            $thumb->store(UPLOAD_PATH_PUBLIC . DIRECTORY_SEPARATOR . UPLOAD_PATH_USER_THUMB);
+            $saveData['thumb'] = (UPLOAD_PATH_STORAGE . DIRECTORY_SEPARATOR . UPLOAD_PATH_USER_THUMB . DIRECTORY_SEPARATOR . $thumb->hashName());
+        }
+
+        // 파일 삭제
+        if($request->get('thumb_del')) {
+            $thumbPath = $request->get('thumb_url');
+            if(file_exists($thumbPath)) {
+                unlink($thumbPath);
+            }
+            $saveData['thumb'] = null;
+        }
+
+        $this->userModel->updater($uid, $saveData);
+
+        $message = '회원 정보가 수정되었습니다.';
+        return redirect()->route('admin.user.list.edit', $uid)->with('message', $message);
+    }
+
+    /**
+     * 회원 삭제
+     * @method DELETE
+     * @see /admin/user/list/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $uid) {
+
+                $user = $this->userModel->findOrNew($uid);
+
+                if($user->exists) {
+                    // 프로필 이미지 삭제
+                    if(file_exists($user->thumb)) {
+                        unlink($user->thumb);
+                    }
+
+                    $user->delete();
+                }
+            }
+        }
+
+        $message = '회원 정보가 삭제되었습니다.';
+        return redirect()->route('admin.user.list.index')->with('message', $message);
+    }
+
+    /**
+     * 회원 탈퇴
+     * @method POST
+     * @see /admin/user/list/withdraw
+     */
+    public function withdraw(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $uid) {
+                $this->userModel->find($uid)->update([
+                    'is_withdraw' => 1,
+                    'deleted_at' => now()
+                ]);
+            }
+        }
+
+        $message = '선택 회원이 탈퇴 처리되었습니다.';
+        return redirect()->route('admin.user.list.index')->with('message', $message);
+    }
+}

+ 64 - 0
app/Http/Controllers/Admin/User/Log/EmailController.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Log;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\DTO\SearchData;
+use App\Models\User;
+use App\Models\UserEmailLog;
+
+class EmailController extends Controller
+{
+    private User $userModel;
+    private UserEmailLog $userEmailLogModel;
+
+    public function __construct(User $user, UserEmailLog $userEmailLogModel)
+    {
+        $this->userModel = $user;
+        $this->userEmailLogModel = $userEmailLogModel;
+    }
+
+    /**
+     * 이메일 변경 이력
+     * @method GET
+     * @see /admin/user/log/email
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $userEmailLog = $this->userEmailLogModel->data($params);
+
+        if ($userEmailLog->rows > 0) {
+            $num = listNum($userEmailLog->total, $params->page, $params->perPage);
+            foreach ($userEmailLog->list as $i => $row) {
+                $row->num = $num--;
+                $userEmailLog->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.log.email', [
+            'userEmailLog' => $userEmailLog,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 이메일 변경 이력 삭제
+     * @method DELETE
+     * @see /admin/user/log/email/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $logID) {
+                $this->userEmailLogModel->find($logID)->delete();
+            }
+        }
+
+        $message = '이름 변경 이력 정보가 삭제되었습니다.';
+        return redirect()->route('admin.user.log.email.index')->with('message', $message);
+    }
+}

+ 65 - 0
app/Http/Controllers/Admin/User/Log/Login/LogController.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Log\Login;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\DTO\SearchData;
+use App\Models\User;
+use App\Models\LoginLog;
+
+class LogController extends Controller
+{
+    private User $userModel;
+    private LoginLog $loginLogModel;
+
+    public function __construct(User $user, LoginLog $loginLogModel)
+    {
+        $this->userModel = $user;
+        $this->loginLogModel = $loginLogModel;
+    }
+
+    /**
+     * 로그인 이력 관리
+     * @method GET
+     * @see /admin/user/log/login
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $userLoginLogData = $this->loginLogModel->data($params);
+
+        if ($userLoginLogData->rows > 0) {
+            $num = listNum($userLoginLogData->total, $params->page, $params->perPage);
+            foreach ($userLoginLogData->list as $i => $row) {
+                $row->num = $num--;
+                $row->createdAt = dateBr($row->created_at);
+                $userLoginLogData->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.log.login.index', [
+            'userLoginLogData' => $userLoginLogData,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 로그인 이력 삭제
+     * @method DELETE
+     * @see /admin/user/log/login/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $logID) {
+                $this->loginLogModel->find($logID)->delete();
+            }
+        }
+
+        $message = '로그인 이력 정보가 삭제되었습니다.';
+        return redirect()->route('admin.user.log.login.index')->with('message', $message);
+    }
+}

+ 164 - 0
app/Http/Controllers/Admin/User/Log/Login/StatController.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Log\Login;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\LoginLog;
+
+class StatController extends Controller
+{
+    private LoginLog $userLoginLog;
+
+    public function __construct(LoginLog $userLoginLog)
+    {
+        $this->userLoginLog = $userLoginLog;
+    }
+
+    /**
+     * 로그인 그래프
+     * @method GET
+     * @see /admin/user/log/login/stat
+     * https://developers.google.com/chart/interactive/docs/basic_load_libs
+     */
+    public function index(Request $request)
+    {
+        /*
+         * d: 일별, m: 월별, y: 년도별
+         */
+        $dateType = $request->get('date_type', 'd');
+        if ($dateType !== 'm' && $dateType !== 'y') {
+            $dateType = 'd';
+        }
+
+        $startDate = $request->get('start_date', date('Y-m-01'));
+        $endDate = $request->get('end_date', date('Y-m-d'));
+        $success = $request->get('success', 1);
+
+        $startYear = '';
+        $endYear = '';
+        $startYearMonth = '';
+        $endYearMonth = '';
+        if ($dateType === 'y' || $dateType === 'm') {
+            $startYear = substr($startDate, 0, 4);
+            $endYear = substr($endDate, 0, 4);
+        }
+        if ($dateType === 'm') {
+            $startMonth = substr($startDate, 5, 2);
+            $endMonth = substr($endDate, 5, 2);
+            $startYearMonth = ($startYear * 12 + $startMonth);
+            $endYearMonth = ($endYear * 12 + $endMonth);
+        }
+
+        $userLoginLogData = $this->userLoginLog->getLoginSuccessCount($dateType, $startDate, $endDate, $success);
+        $sumCount = 0;
+        $arr = [];
+        $max = 0;
+
+        if ($userLoginLogData->total > 0) {
+            foreach ($userLoginLogData->list as $key => $value) {
+                $s = $value->day;
+                if (!isset($arr[$s])) {
+                    $arr[$s] = 0;
+                }
+                $arr[$s] += $value->cnt;
+
+                if ($arr[$s] > $max) {
+                    $max = $arr[$s];
+                }
+                $sumCount += $value->cnt;
+            }
+        }
+
+        $result = [];
+        $i = 0;
+        $no = 0;
+        $saveCount = -1;
+
+        /*
+         * 통계 데이터 생성
+         */
+        if (count($arr) > 0) {
+            foreach ($arr as $date => $value) {
+                $count = (int)$arr[$date];
+                $result[$date]['count'] = $count;
+                $i++;
+                if ($saveCount !== $count) {
+                    $no = $i;
+                    $saveCount = $count;
+                }
+                $result[$date]['no'] = $no;
+                $result[$date]['key'] = $date;
+                $rate = ($count / $sumCount * 100);
+                $result[$date]['rate'] = $rate;
+                $sRate = number_format($rate, 1);
+                $result[$date]['sRate'] = $sRate;
+
+                $bar = (int)($count / $max * 100);
+                $result[$date]['bar'] = $bar;
+            }
+            $data['maxValue'] = $max;
+            $data['sumCount'] = $sumCount;
+        }
+
+        /*
+         * 빈 배열 초기화
+         */
+        if ($dateType === 'y') {
+            for ($i = $startYear; $i <= $endYear; $i++) {
+                if (!isset($result[$i])) {
+                    $result[$i] = [
+                        'count' => 0,
+                        'no' => 0,
+                        'key' => 0,
+                        'rate' => 0,
+                        'sRate' => 0,
+                        'bar' => 0,
+                    ];
+                }
+            }
+        } elseif ($dateType === 'm') {
+            for ($i = $startYearMonth; $i <= $endYearMonth; $i++) {
+                $year = floor($i / 12);
+                if ($year * 12 == $i) $year--;
+                $month = sprintf("%02d", ($i - ($year * 12)));
+                $date = $year . '-' . $month;
+                if (!isset($result[$date])) {
+                    $result[$date] = [
+                        'count' => 0,
+                        'no' => 0,
+                        'key' => 0,
+                        'rate' => 0,
+                        'sRate' => 0,
+                        'bar' => 0,
+                    ];
+                }
+            }
+        } elseif ($dateType === 'd') {
+            $date = $startDate;
+            while ($date <= $endDate) {
+                if (!isset($result[$date])) {
+                    $result[$date] = [
+                        'count' => 0,
+                        'no' => 0,
+                        'key' => 0,
+                        'rate' => 0,
+                        'sRate' => 0,
+                        'bar' => 0,
+                    ];
+                }
+                $date = date('Y-m-d', strtotime($date) + 86400);
+            }
+        }
+
+        ksort($result);
+
+        $data['result'] = $result;
+        $data['dateType'] = $dateType;
+        $data['startDate'] = $startDate;
+        $data['endDate'] = $endDate;
+        $data['success'] = $success;
+
+        return view('admin.user.log.login.stat', $data);
+    }
+}

+ 64 - 0
app/Http/Controllers/Admin/User/Log/NameController.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Http\Controllers\Admin\User\Log;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Models\DTO\SearchData;
+use App\Models\User;
+use App\Models\UserNameLog;
+
+class NameController extends Controller
+{
+    private User $userModel;
+    private UserNameLog $userNameLogModel;
+
+    public function __construct(User $user, UserNameLog $userNameLogModel)
+    {
+        $this->userModel = $user;
+        $this->userNameLogModel = $userNameLogModel;
+    }
+
+    /**
+     * 이름 변경 이력
+     * @method GET
+     * @see /admin/user/log/name
+     */
+    public function index(Request $request)
+    {
+        $params = SearchData::fromRequest($request);
+        $userNameLog = $this->userNameLogModel->data($params);
+
+        if ($userNameLog->rows > 0) {
+            $num = listNum($userNameLog->total, $params->page, $params->perPage);
+            foreach ($userNameLog->list as $i => $row) {
+                $row->num = $num--;
+                $userNameLog->list[$i] = $row;
+            }
+        }
+
+        return view('admin.user.log.name', [
+            'userNameLog' => $userNameLog,
+            'params' => $params
+        ]);
+    }
+
+    /**
+     * 이름 변경 이력 삭제
+     * @method DELETE
+     * @see /admin/user/log/name/destroy
+     */
+    public function destroy(Request $request)
+    {
+        $chk = $request->post('chk');
+
+        if ($chk) {
+            foreach ($chk as $logID) {
+                $this->userNameLogModel->find($logID)->delete();
+            }
+        }
+
+        $message = '이름 변경 이력 정보가 삭제되었습니다.';
+        return redirect()->route('admin.user.log.name.index')->with('message', $message);
+    }
+}

+ 113 - 0
app/Http/Controllers/AdminController.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Http\Traits\CommonTrait;
+use App\Models\Visit;
+use App\Models\User;
+use DateTime;
+
+class AdminController extends Controller
+{
+    use CommonTrait;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+
+    /**
+     * Show the application dashboard.
+     *
+     * @return \Illuminate\Contracts\Support\Renderable
+     */
+    public function index(Request $request)
+    {
+        // 관리자만 접속 가능하다.
+        if(!$request->user()->is_admin) {
+            abort(401);
+        }
+
+        $visitStats = $this->_visitStats();
+        $userStats = $this->_userStats();
+
+        return view('admin.index', [
+            'visitStats' => $visitStats,
+            'userStats' => $userStats
+        ]);
+    }
+
+    /**
+     * 방문자 통계
+     */
+    private function _visitStats()
+    {
+        $visitModel = new Visit();
+
+        $monthlyData = $visitModel->getMonthlyData();
+        $arrDt = array_column($monthlyData, 'dt');
+        $arrCntNew = array_column($monthlyData, 'cntNew');
+        $arrCntRe = array_column($monthlyData, 'cntRe');
+        $arrCntTot = array_column($monthlyData, 'cntTot');
+
+        $dates = $this->setArraySingQuote($arrDt);
+        $cntNews = $this->setArraySingQuote($arrCntNew);
+        $cntRes = $this->setArraySingQuote($arrCntRe);
+        $cntTots = $this->setArraySingQuote($arrCntTot);
+
+        return [
+            'monthlyData' => $visitModel->getMonthlyData(),
+            'visitorTodayCount' => $visitModel->todayCount(),
+            'visitorYesterdayCount' => $visitModel->yesterdayCount(),
+            'visitorTotalCount' => $visitModel->totalCount(),
+            'dates' => $dates,
+            'cntNews' => $cntNews,
+            'cntRes' => $cntRes,
+            'cntTots' => $cntTots,
+            'totalCntNew' => array_sum($arrCntNew),
+            'totalCntRe' => array_sum($arrCntRe),
+            'totalCntTo' => array_sum($arrCntTot)
+        ];
+    }
+
+    /**
+     * 회원가입 통계
+     */
+    private function _userStats()
+    {
+        $userModel = new User();
+
+        $dailyData = $userModel->dailyStats();
+        $mapDailyData = array_column($dailyData, 'value', 'date');
+
+        $startDt = new DateTime(now()->subDays(14)->toDateString());
+        $endDt = new DateTime(now()->toDateString());
+
+        $dates = [];
+        for ($i = $startDt; $i <= $endDt; $i->modify('+1 day')) {
+            $dt = $i->format("Y-m-d");
+
+            $dates[$dt] = (array_key_exists($dt, $mapDailyData) ? $mapDailyData[$dt] : 0);
+        }
+
+        $todayCount = $dates[date('Y-m-d')];
+        $cntNews = $this->setArraySingQuote(array_values($dates));
+        $dates = $this->setArraySingQuote(array_keys($dates));
+
+        return [
+            'dailyData' => $dailyData, // 일별 통계
+            'dates' => $dates,
+            'cntNews' => $cntNews,
+            'todayCount' => $todayCount, // 오늘 가입자
+            'weeklyUserCount' => $userModel->totalWeeklyCount(), // 주간 회원가입자 수
+            'monthlyUserCount' => $userModel->totalMonthlyCount(), // 월별 회원가입자 수
+            'totalUserCount' => $userModel->totalUserCount(), // 전체 회원가입자 수
+        ];
+    }
+}

+ 296 - 0
app/Http/Controllers/ApiController.php

@@ -0,0 +1,296 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Http\Traits\TossTrait;
+use App\Http\Traits\CryptTrait;
+use App\Models\Config;
+use App\Models\User;
+use App\Models\DTO\ResponseData;
+use App\Models\DTO\Toss\AuthData;
+use App\Models\DTO\Toss\FailData;
+use App\Rules\DeniedEmail;
+use App\Rules\SpecialCharLength;
+use App\Rules\UppercaseLength;
+use App\Rules\NumberLength;
+use App\Rules\AllowNickname;
+use App\Rules\IsPhone;
+use Exception;
+
+class ApiController extends Controller
+{
+    use TossTrait, CryptTrait;
+
+    public function __construct()
+    {
+    }
+
+    /**
+     * 로그인 확인
+     */
+    public function loginCheck(): int
+    {
+        return intval(auth()->check());
+    }
+
+    /**
+     * 금지 단어 확인
+     */
+    public function filterSpamKeyword(Request $request, ResponseData $response): ResponseData
+    {
+        $subject = $request->input('subject');
+        $content = $request->input('content');
+        $response->subject = "";
+        $response->content = "";
+        $spamWord = explode(',', trim((new Config)->item("spam_word")));
+
+        if ($spamWord) {
+            for ($i = 0; $i < count($spamWord); $i++) {
+                $str = trim($spamWord[$i]);
+                if ($subject) {
+                    $pos = stripos($subject, $str);
+                    if ($pos !== false) {
+                        $response->subject = $str;
+                        break;
+                    }
+                }
+                if ($content) {
+                    $pos = stripos($content, $str);
+                    if ($pos !== false) {
+                        $response->content = $str;
+                        break;
+                    }
+                }
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * 중복 이메일 여부
+     */
+    public function isEmailAble(Request $request): bool
+    {
+        try {
+
+            $email = $request->input('email');
+            if (!$email) {
+                throw new Exception;
+            }
+
+            // 중복 여부
+            if ((new User)->where('email', $email)->exists()) {
+                throw new Exception;
+            }
+
+            // 유효성 확인
+            if (!(new DeniedEmail)->passes(null, $email)) {
+                throw new Exception;
+            }
+
+            return true;
+        } catch (Exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 비밀번호 유효성 검사
+     */
+    public function isPasswordAble(Request $request): bool
+    {
+        try {
+
+            $password = $request->input('password');
+            if (!$password) {
+                throw new Exception;
+            }
+
+            // 유효성 확인
+            if (!(new SpecialCharLength)->passes(null, $password)) {
+                throw new Exception;
+            }
+
+            if (!(new UppercaseLength)->passes(null, $password)) {
+                throw new Exception;
+            }
+
+            if (!(new NumberLength)->passes(null, $password)) {
+                throw new Exception;
+            }
+
+            return true;
+        } catch (Exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 중복 닉네임 여부
+     */
+    public function isNicknameAble(Request $request): bool
+    {
+        try {
+
+            $nickname = $request->input('nickname');
+            if (!$nickname) {
+                throw new Exception;
+            }
+
+            // 중복 여부
+            if ((new User)->where('nickname', $nickname)->exists()) {
+                throw new Exception;
+            }
+
+            // 유효성 확인
+            if (!(new AllowNickname)->passes(null, $nickname)) {
+                throw new Exception;
+            }
+
+            return true;
+        } catch (Exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 중복 휴대전화번호 유효성 검사
+     */
+    public function isPhoneAble(Request $request): bool
+    {
+        try {
+
+            $phone = $request->input('phone');
+            if (!$phone) {
+                throw new Exception;
+            }
+
+            // 중복 여부
+            if ((new User)->where('phone', $phone)->exists()) {
+                throw new Exception;
+            }
+
+            // 유효성 확인
+            if (!(new IsPhone)->passes(null, $phone)) {
+                throw new Exception;
+            }
+
+            return true;
+        } catch (Exception) {
+            return false;
+        }
+    }
+
+
+    /**
+     * 정기 비밀번호 변경 다음에 하기
+     */
+    public function passwordCampaignSkip()
+    {
+        return (new User)->where('id', UID)->update([
+            'password_updated_at' => now()
+        ]);
+    }
+
+    /**
+     * TinyMCE 에디터 이미지 첨부 화면
+     */
+    public function uploader()
+    {
+        return view('component.uploader');
+    }
+
+    /**
+     * 토스 본인확인 요청 (푸쉬앱)
+     */
+    public function requestTossCertToPush(Request $request)
+    {
+        try {
+
+            $posts = $request->validate([
+                'name' => 'required|string|min:2|max:10',
+                'birthday' => 'required|digits:8',
+                'phone' => 'required|numeric|unique:users'
+            ], [
+                'name.required' => '이름을 입력해주세요.',
+                'name.string' => '이름 형식이 옳지 않습니다.',
+                'name.min' => '이름은 최소 2자 이상 입력해주세요.',
+                'name.max' => '이름은 최대 10자 입력 가능합니다.',
+                'birthday.required' => '생년월일을 입력해주세요.',
+                'birthday.digits' => '생년월일 형식이 옳지 않습니다.',
+                'phone.required' => '휴대전화를 입력해주세요.',
+                'phone.numeric' => '휴대전화번호는 숫자(`-` 없이)만 입력해주세요.',
+                'phone.unique' => '이미 사용 중인 휴대전화입니다.'
+            ], [
+                'name' => '이름',
+                'birthday' => '생년월일',
+                'phone' => '휴대전화'
+            ]);
+
+            $sessionId = $this->generateSessionId();
+            $secretKey = $this->generateRandomBytes(32);
+            $iv = $this->generateRandomBytes(12);
+            $sessionKey = $this->generateSessionKey($sessionId, $secretKey, $iv);
+
+            $name = $this->encryptData($sessionId, $secretKey, $iv, $posts['name']);
+            $birthday = $this->encryptData($sessionId, $secretKey, $iv, $posts['birthday']);
+            $phone = $this->encryptData($sessionId, $secretKey, $iv, $posts['phone']);
+
+            $token = $this->requestAccessToken($request);
+            $result = $this->requestUserAuth($token, new AuthData(
+                'USER_PERSONAL', 'PUSH', $name, $phone, $birthday, $sessionKey
+            ));
+
+            return response()->json($result)->withCookie(
+                'tossAccessToken', serialize($token), ($token['expires_in'] / 60)
+            );
+        }catch(Exception $e) {
+            return new FailData($e->getCode(), $e->getMessage());
+        }
+    }
+
+    /**
+     * 토스 본인확인 요청 (표준창)
+     */
+    public function requestTossCertToPopup(Request $request)
+    {
+        try {
+
+            $token = $this->requestAccessToken($request);
+            $result = $this->requestUserAuth($token, new AuthData('USER_NONE'));
+
+            return response()->json($result)->withCookie(
+                'tossAccessToken', serialize($token), ($token['expires_in'] / 60)
+            );
+        }catch(Exception $e) {
+            return new FailData($e->getCode(), $e->getMessage());
+        }
+    }
+
+    /**
+     * 토스 본인확인 상태 검증
+     */
+    public function requestTossCertStatus(Request $request)
+    {
+        try {
+
+            $posts = $request->validate([
+                'tx_id' => 'required',
+            ], [
+                'tx_id.required' => '비 정상적인 접근입니다.'
+            ], [
+                'tx_id' => 'TXID'
+            ]);
+
+            $token = $this->requestAccessToken($request);
+
+            return response()->json(
+                $this->requestUserAuthStatus($token, $posts['tx_id'])
+            );
+        }catch(Exception $e) {
+            return new FailData($e->getCode(), $e->getMessage());
+        }
+    }
+}

+ 40 - 0
app/Http/Controllers/Auth/ConfirmPasswordController.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Http\Controllers\Auth;
+
+use App\Http\Controllers\Controller;
+use App\Providers\RouteServiceProvider;
+use Illuminate\Foundation\Auth\ConfirmsPasswords;
+
+class ConfirmPasswordController extends Controller
+{
+    /*
+    |--------------------------------------------------------------------------
+    | Confirm Password Controller
+    |--------------------------------------------------------------------------
+    |
+    | This controller is responsible for handling password confirmations and
+    | uses a simple trait to include the behavior. You're free to explore
+    | this trait and override any functions that require customization.
+    |
+    */
+
+    use ConfirmsPasswords;
+
+    /**
+     * Where to redirect users when the intended url fails.
+     *
+     * @var string
+     */
+    protected $redirectTo = RouteServiceProvider::HOME;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
+}

+ 65 - 0
app/Http/Controllers/Auth/ForgotAccountController.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Http\Controllers\Auth;
+
+use Illuminate\Http\Request;
+use App\Http\Controllers\Controller;
+use App\Http\Traits\TossTrait;
+use App\Http\Traits\CryptTrait;
+use App\Models\User;
+use App\Models\TossCertLog;
+use Exception;
+
+class ForgotAccountController extends Controller
+{
+    use TossTrait, CryptTrait;
+
+    public function __construct()
+    {
+        $this->middleware('front');
+    }
+
+    /**
+     * 아이디 찾기
+     * @method GET
+     * @see /auth/findID
+     */
+    public function index()
+    {
+        return view('auth.findID.index', []);
+    }
+
+    /**
+     * 아이디 찾기 결과
+     * @method POST
+     * @see /auth/findID
+     */
+    public function store(Request $request, TossCertLog $tossCertLogModel, User $userModel)
+    {
+        try {
+
+            $token = $request->cookie('tossAccessToken');
+            if (!$token) {
+                throw new Exception('토스 인증 절차가 잘못되었습니다. 다시 시도해주세요.');
+            }
+
+            $txId = $request->post('tx_id');
+            $token = unserialize($token);
+
+            // 토스 본인확인 결과 조회
+            $result = $this->getTossCertResult($token, $txId);
+
+            // 토스 인증 결과 기록
+            $tossCertLogModel->register($result);
+
+            unset($request, $txId, $token, $tossCertLogModel);
+
+            // 아이디 찾기 결과 보기
+            return view('auth.findID.result', [
+                'user' => $userModel->findUserByTossCI($result->success->personalData->ci)
+            ]);
+        } catch (Exception $e) {
+            return redirect()->route('auth.findID')->withErrors('message', $e->getMessage());
+        }
+    }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません