송용우 4 månader sedan
förälder
incheckning
b3766f028f
75 ändrade filer med 8352 tillägg och 249 borttagningar
  1. 202 0
      backend/app/Config/App.php
  2. 92 0
      backend/app/Config/Autoload.php
  3. 34 0
      backend/app/Config/Boot/development.php
  4. 25 0
      backend/app/Config/Boot/production.php
  5. 38 0
      backend/app/Config/Boot/testing.php
  6. 20 0
      backend/app/Config/CURLRequest.php
  7. 162 0
      backend/app/Config/Cache.php
  8. 79 0
      backend/app/Config/Constants.php
  9. 176 0
      backend/app/Config/ContentSecurityPolicy.php
  10. 107 0
      backend/app/Config/Cookie.php
  11. 105 0
      backend/app/Config/Cors.php
  12. 203 0
      backend/app/Config/Database.php
  13. 43 0
      backend/app/Config/DocTypes.php
  14. 121 0
      backend/app/Config/Email.php
  15. 92 0
      backend/app/Config/Encryption.php
  16. 55 0
      backend/app/Config/Events.php
  17. 106 0
      backend/app/Config/Exceptions.php
  18. 37 0
      backend/app/Config/Feature.php
  19. 110 0
      backend/app/Config/Filters.php
  20. 12 0
      backend/app/Config/ForeignCharacters.php
  21. 64 0
      backend/app/Config/Format.php
  22. 44 0
      backend/app/Config/Generators.php
  23. 42 0
      backend/app/Config/Honeypot.php
  24. 31 0
      backend/app/Config/Images.php
  25. 63 0
      backend/app/Config/Kint.php
  26. 151 0
      backend/app/Config/Logger.php
  27. 50 0
      backend/app/Config/Migrations.php
  28. 534 0
      backend/app/Config/Mimes.php
  29. 82 0
      backend/app/Config/Modules.php
  30. 30 0
      backend/app/Config/Optimize.php
  31. 37 0
      backend/app/Config/Pager.php
  32. 78 0
      backend/app/Config/Paths.php
  33. 28 0
      backend/app/Config/Publisher.php
  34. 173 37
      backend/app/Config/Routes.php
  35. 193 0
      backend/app/Config/Routes.php.bak
  36. 140 0
      backend/app/Config/Routing.php
  37. 86 0
      backend/app/Config/Security.php
  38. 32 0
      backend/app/Config/Services.php
  39. 127 0
      backend/app/Config/Session.php
  40. 122 0
      backend/app/Config/Toolbar.php
  41. 252 0
      backend/app/Config/UserAgents.php
  42. 44 0
      backend/app/Config/Validation.php
  43. 62 0
      backend/app/Config/View.php
  44. 80 0
      backend/app/Controllers/Alimtalk.php
  45. 58 0
      backend/app/Controllers/BaseController.php
  46. 113 0
      backend/app/Controllers/Deli.php
  47. 19 0
      backend/app/Controllers/Home.php
  48. 54 46
      backend/app/Controllers/InfluencerController.php
  49. 368 0
      backend/app/Controllers/InfluencerControllerV2.php
  50. 322 0
      backend/app/Controllers/Item.php
  51. 303 0
      backend/app/Controllers/Mng.php
  52. 282 19
      backend/app/Controllers/VendorController.php
  53. 370 0
      backend/app/Controllers/VendorControllerV2.php
  54. 203 0
      backend/app/Controllers/VendorInfluencerTerminate.php
  55. 484 0
      backend/app/Controllers/Winner.php
  56. 0 0
      backend/app/Models/.gitkeep
  57. 5 3
      backend/app/Models/InfluencerPartnershipModel.php
  58. 32 0
      backend/app/Models/LoginModel.php
  59. 12 0
      backend/app/Models/UserListModel.php
  60. 223 0
      backend/app/Models/UserModel.php
  61. 62 0
      backend/app/Models/VendorAddressModel.php
  62. 62 0
      backend/app/Models/VendorBusinessAreaModel.php
  63. 45 0
      backend/app/Models/VendorCategoryModel.php
  64. 64 0
      backend/app/Models/VendorContactModel.php
  65. 182 0
      backend/app/Models/VendorInfluencerReapply.php
  66. 183 15
      backend/app/Models/VendorInfluencerStatusHistoryModel.php
  67. 1 1
      backend/app/Models/VendorModel.php
  68. 34 0
      backend/app/Models/VendorPartnershipModel.php
  69. 62 0
      backend/app/Models/VendorProductModel.php
  70. 42 0
      ddl/012_add_rating_column.sql
  71. 54 0
      ddl/012_add_rating_column_fixed.sql
  72. 51 0
      ddl/012_add_rating_column_simple.sql
  73. 109 105
      ddl/README.md
  74. 177 0
      md/2024-12-22-백엔드-프론트엔드-완전통합-가이드.md
  75. 12 23
      pages/view/vendor/dashboard/influencer-requests.vue

+ 202 - 0
backend/app/Config/App.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class App extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Base Site URL
+     * --------------------------------------------------------------------------
+     *
+     * URL to your CodeIgniter root. Typically, this will be your base URL,
+     * WITH a trailing slash:
+     *
+     * E.g., http://example.com/
+     */
+    public string $baseURL = 'http://localhost:8080/';
+
+    /**
+     * Allowed Hostnames in the Site URL other than the hostname in the baseURL.
+     * If you want to accept multiple Hostnames, set this.
+     *
+     * E.g.,
+     * When your site URL ($baseURL) is 'http://example.com/', and your site
+     * also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
+     *     ['media.example.com', 'accounts.example.com']
+     *
+     * @var list<string>
+     */
+    public array $allowedHostnames = [];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Index File
+     * --------------------------------------------------------------------------
+     *
+     * Typically, this will be your `index.php` file, unless you've renamed it to
+     * something else. If you have configured your web server to remove this file
+     * from your site URIs, set this variable to an empty string.
+     */
+    public string $indexPage = 'index.php';
+
+    /**
+     * --------------------------------------------------------------------------
+     * URI PROTOCOL
+     * --------------------------------------------------------------------------
+     *
+     * This item determines which server global should be used to retrieve the
+     * URI string. The default setting of 'REQUEST_URI' works for most servers.
+     * If your links do not seem to work, try one of the other delicious flavors:
+     *
+     *  'REQUEST_URI': Uses $_SERVER['REQUEST_URI']
+     * 'QUERY_STRING': Uses $_SERVER['QUERY_STRING']
+     *    'PATH_INFO': Uses $_SERVER['PATH_INFO']
+     *
+     * WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
+     */
+    public string $uriProtocol = 'REQUEST_URI';
+
+    /*
+    |--------------------------------------------------------------------------
+    | Allowed URL Characters
+    |--------------------------------------------------------------------------
+    |
+    | This lets you specify which characters are permitted within your URLs.
+    | When someone tries to submit a URL with disallowed characters they will
+    | get a warning message.
+    |
+    | As a security measure you are STRONGLY encouraged to restrict URLs to
+    | as few characters as possible.
+    |
+    | By default, only these are allowed: `a-z 0-9~%.:_-`
+    |
+    | Set an empty string to allow all characters -- but only if you are insane.
+    |
+    | The configured value is actually a regular expression character group
+    | and it will be used as: '/\A[<permittedURIChars>]+\z/iu'
+    |
+    | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
+    |
+    */
+    public string $permittedURIChars = 'a-z 0-9~%.:_\-';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Default Locale
+     * --------------------------------------------------------------------------
+     *
+     * The Locale roughly represents the language and location that your visitor
+     * is viewing the site from. It affects the language strings and other
+     * strings (like currency markers, numbers, etc), that your program
+     * should run under for this request.
+     */
+    public string $defaultLocale = 'en';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Negotiate Locale
+     * --------------------------------------------------------------------------
+     *
+     * If true, the current Request object will automatically determine the
+     * language to use based on the value of the Accept-Language header.
+     *
+     * If false, no automatic detection will be performed.
+     */
+    public bool $negotiateLocale = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Supported Locales
+     * --------------------------------------------------------------------------
+     *
+     * If $negotiateLocale is true, this array lists the locales supported
+     * by the application in descending order of priority. If no match is
+     * found, the first locale will be used.
+     *
+     * IncomingRequest::setLocale() also uses this list.
+     *
+     * @var list<string>
+     */
+    public array $supportedLocales = ['en'];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Application Timezone
+     * --------------------------------------------------------------------------
+     *
+     * The default timezone that will be used in your application to display
+     * dates with the date helper, and can be retrieved through app_timezone()
+     *
+     * @see https://www.php.net/manual/en/timezones.php for list of timezones
+     *      supported by PHP.
+     */
+    public string $appTimezone = 'UTC';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Default Character Set
+     * --------------------------------------------------------------------------
+     *
+     * This determines which character set is used by default in various methods
+     * that require a character set to be provided.
+     *
+     * @see http://php.net/htmlspecialchars for a list of supported charsets.
+     */
+    public string $charset = 'UTF-8';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Force Global Secure Requests
+     * --------------------------------------------------------------------------
+     *
+     * If true, this will force every request made to this application to be
+     * made via a secure connection (HTTPS). If the incoming request is not
+     * secure, the user will be redirected to a secure version of the page
+     * and the HTTP Strict Transport Security (HSTS) header will be set.
+     */
+    public bool $forceGlobalSecureRequests = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Reverse Proxy IPs
+     * --------------------------------------------------------------------------
+     *
+     * If your server is behind a reverse proxy, you must whitelist the proxy
+     * IP addresses from which CodeIgniter should trust headers such as
+     * X-Forwarded-For or Client-IP in order to properly identify
+     * the visitor's IP address.
+     *
+     * You need to set a proxy IP address or IP address with subnets and
+     * the HTTP header for the client IP address.
+     *
+     * Here are some examples:
+     *     [
+     *         '10.0.1.200'     => 'X-Forwarded-For',
+     *         '192.168.5.0/24' => 'X-Real-IP',
+     *     ]
+     *
+     * @var array<string, string>
+     */
+    public array $proxyIPs = [];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Content Security Policy
+     * --------------------------------------------------------------------------
+     *
+     * Enables the Response's Content Secure Policy to restrict the sources that
+     * can be used for images, scripts, CSS files, audio, video, etc. If enabled,
+     * the Response object will populate default values for the policy from the
+     * `ContentSecurityPolicy.php` file. Controllers can always add to those
+     * restrictions at run time.
+     *
+     * For a better understanding of CSP, see these documents:
+     *
+     * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
+     * @see http://www.w3.org/TR/CSP/
+     */
+    public bool $CSPEnabled = false;
+}

+ 92 - 0
backend/app/Config/Autoload.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\AutoloadConfig;
+
+/**
+ * -------------------------------------------------------------------
+ * AUTOLOADER CONFIGURATION
+ * -------------------------------------------------------------------
+ *
+ * This file defines the namespaces and class maps so the Autoloader
+ * can find the files as needed.
+ *
+ * NOTE: If you use an identical key in $psr4 or $classmap, then
+ *       the values in this file will overwrite the framework's values.
+ *
+ * NOTE: This class is required prior to Autoloader instantiation,
+ *       and does not extend BaseConfig.
+ */
+class Autoload extends AutoloadConfig
+{
+    /**
+     * -------------------------------------------------------------------
+     * Namespaces
+     * -------------------------------------------------------------------
+     * This maps the locations of any namespaces in your application to
+     * their location on the file system. These are used by the autoloader
+     * to locate files the first time they have been instantiated.
+     *
+     * The 'Config' (APPPATH . 'Config') and 'CodeIgniter' (SYSTEMPATH) are
+     * already mapped for you.
+     *
+     * You may change the name of the 'App' namespace if you wish,
+     * but this should be done prior to creating any namespaced classes,
+     * else you will need to modify all of those classes for this to work.
+     *
+     * @var array<string, list<string>|string>
+     */
+    public $psr4 = [
+        APP_NAMESPACE => APPPATH,
+    ];
+
+    /**
+     * -------------------------------------------------------------------
+     * Class Map
+     * -------------------------------------------------------------------
+     * The class map provides a map of class names and their exact
+     * location on the drive. Classes loaded in this manner will have
+     * slightly faster performance because they will not have to be
+     * searched for within one or more directories as they would if they
+     * were being autoloaded through a namespace.
+     *
+     * Prototype:
+     *   $classmap = [
+     *       'MyClass'   => '/path/to/class/file.php'
+     *   ];
+     *
+     * @var array<string, string>
+     */
+    public $classmap = [];
+
+    /**
+     * -------------------------------------------------------------------
+     * Files
+     * -------------------------------------------------------------------
+     * The files array provides a list of paths to __non-class__ files
+     * that will be autoloaded. This can be useful for bootstrap operations
+     * or for loading functions.
+     *
+     * Prototype:
+     *   $files = [
+     *       '/path/to/my/file.php',
+     *   ];
+     *
+     * @var list<string>
+     */
+    public $files = [];
+
+    /**
+     * -------------------------------------------------------------------
+     * Helpers
+     * -------------------------------------------------------------------
+     * Prototype:
+     *   $helpers = [
+     *       'form',
+     *   ];
+     *
+     * @var list<string>
+     */
+    public $helpers = [];
+}

+ 34 - 0
backend/app/Config/Boot/development.php

@@ -0,0 +1,34 @@
+<?php
+
+/*
+ |--------------------------------------------------------------------------
+ | ERROR DISPLAY
+ |--------------------------------------------------------------------------
+ | In development, we want to show as many errors as possible to help
+ | make sure they don't make it to production. And save us hours of
+ | painful debugging.
+ |
+ | If you set 'display_errors' to '1', CI4's detailed error report will show.
+ */
+error_reporting(E_ALL);
+ini_set('display_errors', '1');
+
+/*
+ |--------------------------------------------------------------------------
+ | DEBUG BACKTRACES
+ |--------------------------------------------------------------------------
+ | If true, this constant will tell the error screens to display debug
+ | backtraces along with the other error information. If you would
+ | prefer to not see this, set this value to false.
+ */
+defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
+
+/*
+ |--------------------------------------------------------------------------
+ | DEBUG MODE
+ |--------------------------------------------------------------------------
+ | Debug mode is an experimental flag that can allow changes throughout
+ | the system. This will control whether Kint is loaded, and a few other
+ | items. It can always be used within your own application too.
+ */
+defined('CI_DEBUG') || define('CI_DEBUG', true);

+ 25 - 0
backend/app/Config/Boot/production.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ |--------------------------------------------------------------------------
+ | ERROR DISPLAY
+ |--------------------------------------------------------------------------
+ | Don't show ANY in production environments. Instead, let the system catch
+ | it and display a generic error message.
+ |
+ | If you set 'display_errors' to '1', CI4's detailed error report will show.
+ */
+error_reporting(E_ALL & ~E_DEPRECATED);
+// If you want to suppress more types of errors.
+// error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
+ini_set('display_errors', '0');
+
+/*
+ |--------------------------------------------------------------------------
+ | DEBUG MODE
+ |--------------------------------------------------------------------------
+ | Debug mode is an experimental flag that can allow changes throughout
+ | the system. It's not widely used currently, and may not survive
+ | release of the framework.
+ */
+defined('CI_DEBUG') || define('CI_DEBUG', false);

+ 38 - 0
backend/app/Config/Boot/testing.php

@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * The environment testing is reserved for PHPUnit testing. It has special
+ * conditions built into the framework at various places to assist with that.
+ * You can’t use it for your development.
+ */
+
+/*
+ |--------------------------------------------------------------------------
+ | ERROR DISPLAY
+ |--------------------------------------------------------------------------
+ | In development, we want to show as many errors as possible to help
+ | make sure they don't make it to production. And save us hours of
+ | painful debugging.
+ */
+error_reporting(E_ALL);
+ini_set('display_errors', '1');
+
+/*
+ |--------------------------------------------------------------------------
+ | DEBUG BACKTRACES
+ |--------------------------------------------------------------------------
+ | If true, this constant will tell the error screens to display debug
+ | backtraces along with the other error information. If you would
+ | prefer to not see this, set this value to false.
+ */
+defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
+
+/*
+ |--------------------------------------------------------------------------
+ | DEBUG MODE
+ |--------------------------------------------------------------------------
+ | Debug mode is an experimental flag that can allow changes throughout
+ | the system. It's not widely used currently, and may not survive
+ | release of the framework.
+ */
+defined('CI_DEBUG') || define('CI_DEBUG', true);

+ 20 - 0
backend/app/Config/CURLRequest.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class CURLRequest extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * CURLRequest Share Options
+     * --------------------------------------------------------------------------
+     *
+     * Whether share options between requests or not.
+     *
+     * If true, all the options won't be reset between requests.
+     * It may cause an error request with unnecessary headers.
+     */
+    public bool $shareOptions = false;
+}

+ 162 - 0
backend/app/Config/Cache.php

@@ -0,0 +1,162 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Cache\CacheInterface;
+use CodeIgniter\Cache\Handlers\DummyHandler;
+use CodeIgniter\Cache\Handlers\FileHandler;
+use CodeIgniter\Cache\Handlers\MemcachedHandler;
+use CodeIgniter\Cache\Handlers\PredisHandler;
+use CodeIgniter\Cache\Handlers\RedisHandler;
+use CodeIgniter\Cache\Handlers\WincacheHandler;
+use CodeIgniter\Config\BaseConfig;
+
+class Cache extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Primary Handler
+     * --------------------------------------------------------------------------
+     *
+     * The name of the preferred handler that should be used. If for some reason
+     * it is not available, the $backupHandler will be used in its place.
+     */
+    public string $handler = 'file';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Backup Handler
+     * --------------------------------------------------------------------------
+     *
+     * The name of the handler that will be used in case the first one is
+     * unreachable. Often, 'file' is used here since the filesystem is
+     * always available, though that's not always practical for the app.
+     */
+    public string $backupHandler = 'dummy';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Key Prefix
+     * --------------------------------------------------------------------------
+     *
+     * This string is added to all cache item names to help avoid collisions
+     * if you run multiple applications with the same cache engine.
+     */
+    public string $prefix = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Default TTL
+     * --------------------------------------------------------------------------
+     *
+     * The default number of seconds to save items when none is specified.
+     *
+     * WARNING: This is not used by framework handlers where 60 seconds is
+     * hard-coded, but may be useful to projects and modules. This will replace
+     * the hard-coded value in a future release.
+     */
+    public int $ttl = 60;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Reserved Characters
+     * --------------------------------------------------------------------------
+     *
+     * A string of reserved characters that will not be allowed in keys or tags.
+     * Strings that violate this restriction will cause handlers to throw.
+     * Default: {}()/\@:
+     *
+     * NOTE: The default set is required for PSR-6 compliance.
+     */
+    public string $reservedCharacters = '{}()/\@:';
+
+    /**
+     * --------------------------------------------------------------------------
+     * File settings
+     * --------------------------------------------------------------------------
+     *
+     * Your file storage preferences can be specified below, if you are using
+     * the File driver.
+     *
+     * @var array<string, int|string|null>
+     */
+    public array $file = [
+        'storePath' => WRITEPATH . 'cache/',
+        'mode'      => 0640,
+    ];
+
+    /**
+     * -------------------------------------------------------------------------
+     * Memcached settings
+     * -------------------------------------------------------------------------
+     *
+     * Your Memcached servers can be specified below, if you are using
+     * the Memcached drivers.
+     *
+     * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
+     *
+     * @var array<string, bool|int|string>
+     */
+    public array $memcached = [
+        'host'   => '127.0.0.1',
+        'port'   => 11211,
+        'weight' => 1,
+        'raw'    => false,
+    ];
+
+    /**
+     * -------------------------------------------------------------------------
+     * Redis settings
+     * -------------------------------------------------------------------------
+     *
+     * Your Redis server can be specified below, if you are using
+     * the Redis or Predis drivers.
+     *
+     * @var array<string, int|string|null>
+     */
+    public array $redis = [
+        'host'     => '127.0.0.1',
+        'password' => null,
+        'port'     => 6379,
+        'timeout'  => 0,
+        'database' => 0,
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Available Cache Handlers
+     * --------------------------------------------------------------------------
+     *
+     * This is an array of cache engine alias' and class names. Only engines
+     * that are listed here are allowed to be used.
+     *
+     * @var array<string, class-string<CacheInterface>>
+     */
+    public array $validHandlers = [
+        'dummy'     => DummyHandler::class,
+        'file'      => FileHandler::class,
+        'memcached' => MemcachedHandler::class,
+        'predis'    => PredisHandler::class,
+        'redis'     => RedisHandler::class,
+        'wincache'  => WincacheHandler::class,
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Web Page Caching: Cache Include Query String
+     * --------------------------------------------------------------------------
+     *
+     * Whether to take the URL query string into consideration when generating
+     * output cache files. Valid options are:
+     *
+     *    false = Disabled
+     *    true  = Enabled, take all query parameters into account.
+     *            Please be aware that this may result in numerous cache
+     *            files generated for the same page over and over again.
+     *    ['q'] = Enabled, but only take into account the specified list
+     *            of query parameters.
+     *
+     * @var bool|list<string>
+     */
+    public $cacheQueryString = false;
+}

+ 79 - 0
backend/app/Config/Constants.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ | --------------------------------------------------------------------
+ | App Namespace
+ | --------------------------------------------------------------------
+ |
+ | This defines the default Namespace that is used throughout
+ | CodeIgniter to refer to the Application directory. Change
+ | this constant to change the namespace that all application
+ | classes should use.
+ |
+ | NOTE: changing this will require manually modifying the
+ | existing namespaces of App\* namespaced-classes.
+ */
+defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
+
+/*
+ | --------------------------------------------------------------------------
+ | Composer Path
+ | --------------------------------------------------------------------------
+ |
+ | The path that Composer's autoload file is expected to live. By default,
+ | the vendor folder is in the Root directory, but you can customize that here.
+ */
+defined('COMPOSER_PATH') || define('COMPOSER_PATH', ROOTPATH . 'vendor/autoload.php');
+
+/*
+ |--------------------------------------------------------------------------
+ | Timing Constants
+ |--------------------------------------------------------------------------
+ |
+ | Provide simple ways to work with the myriad of PHP functions that
+ | require information to be in seconds.
+ */
+defined('SECOND') || define('SECOND', 1);
+defined('MINUTE') || define('MINUTE', 60);
+defined('HOUR')   || define('HOUR', 3600);
+defined('DAY')    || define('DAY', 86400);
+defined('WEEK')   || define('WEEK', 604800);
+defined('MONTH')  || define('MONTH', 2_592_000);
+defined('YEAR')   || define('YEAR', 31_536_000);
+defined('DECADE') || define('DECADE', 315_360_000);
+
+/*
+ | --------------------------------------------------------------------------
+ | Exit Status Codes
+ | --------------------------------------------------------------------------
+ |
+ | Used to indicate the conditions under which the script is exit()ing.
+ | While there is no universal standard for error codes, there are some
+ | broad conventions.  Three such conventions are mentioned below, for
+ | those who wish to make use of them.  The CodeIgniter defaults were
+ | chosen for the least overlap with these conventions, while still
+ | leaving room for others to be defined in future versions and user
+ | applications.
+ |
+ | The three main conventions used for determining exit status codes
+ | are as follows:
+ |
+ |    Standard C/C++ Library (stdlibc):
+ |       http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
+ |       (This link also contains other GNU-specific conventions)
+ |    BSD sysexits.h:
+ |       http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=sysexits
+ |    Bash scripting:
+ |       http://tldp.org/LDP/abs/html/exitcodes.html
+ |
+ */
+defined('EXIT_SUCCESS')        || define('EXIT_SUCCESS', 0);        // no errors
+defined('EXIT_ERROR')          || define('EXIT_ERROR', 1);          // generic error
+defined('EXIT_CONFIG')         || define('EXIT_CONFIG', 3);         // configuration error
+defined('EXIT_UNKNOWN_FILE')   || define('EXIT_UNKNOWN_FILE', 4);   // file not found
+defined('EXIT_UNKNOWN_CLASS')  || define('EXIT_UNKNOWN_CLASS', 5);  // unknown class
+defined('EXIT_UNKNOWN_METHOD') || define('EXIT_UNKNOWN_METHOD', 6); // unknown class member
+defined('EXIT_USER_INPUT')     || define('EXIT_USER_INPUT', 7);     // invalid user input
+defined('EXIT_DATABASE')       || define('EXIT_DATABASE', 8);       // database error
+defined('EXIT__AUTO_MIN')      || define('EXIT__AUTO_MIN', 9);      // lowest automatically-assigned error code
+defined('EXIT__AUTO_MAX')      || define('EXIT__AUTO_MAX', 125);    // highest automatically-assigned error code

+ 176 - 0
backend/app/Config/ContentSecurityPolicy.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+/**
+ * Stores the default settings for the ContentSecurityPolicy, if you
+ * choose to use it. The values here will be read in and set as defaults
+ * for the site. If needed, they can be overridden on a page-by-page basis.
+ *
+ * Suggested reference for explanations:
+ *
+ * @see https://www.html5rocks.com/en/tutorials/security/content-security-policy/
+ */
+class ContentSecurityPolicy extends BaseConfig
+{
+    // -------------------------------------------------------------------------
+    // Broadbrush CSP management
+    // -------------------------------------------------------------------------
+
+    /**
+     * Default CSP report context
+     */
+    public bool $reportOnly = false;
+
+    /**
+     * Specifies a URL where a browser will send reports
+     * when a content security policy is violated.
+     */
+    public ?string $reportURI = null;
+
+    /**
+     * Instructs user agents to rewrite URL schemes, changing
+     * HTTP to HTTPS. This directive is for websites with
+     * large numbers of old URLs that need to be rewritten.
+     */
+    public bool $upgradeInsecureRequests = false;
+
+    // -------------------------------------------------------------------------
+    // Sources allowed
+    // NOTE: once you set a policy to 'none', it cannot be further restricted
+    // -------------------------------------------------------------------------
+
+    /**
+     * Will default to self if not overridden
+     *
+     * @var list<string>|string|null
+     */
+    public $defaultSrc;
+
+    /**
+     * Lists allowed scripts' URLs.
+     *
+     * @var list<string>|string
+     */
+    public $scriptSrc = 'self';
+
+    /**
+     * Lists allowed stylesheets' URLs.
+     *
+     * @var list<string>|string
+     */
+    public $styleSrc = 'self';
+
+    /**
+     * Defines the origins from which images can be loaded.
+     *
+     * @var list<string>|string
+     */
+    public $imageSrc = 'self';
+
+    /**
+     * Restricts the URLs that can appear in a page's `<base>` element.
+     *
+     * Will default to self if not overridden
+     *
+     * @var list<string>|string|null
+     */
+    public $baseURI;
+
+    /**
+     * Lists the URLs for workers and embedded frame contents
+     *
+     * @var list<string>|string
+     */
+    public $childSrc = 'self';
+
+    /**
+     * Limits the origins that you can connect to (via XHR,
+     * WebSockets, and EventSource).
+     *
+     * @var list<string>|string
+     */
+    public $connectSrc = 'self';
+
+    /**
+     * Specifies the origins that can serve web fonts.
+     *
+     * @var list<string>|string
+     */
+    public $fontSrc;
+
+    /**
+     * Lists valid endpoints for submission from `<form>` tags.
+     *
+     * @var list<string>|string
+     */
+    public $formAction = 'self';
+
+    /**
+     * Specifies the sources that can embed the current page.
+     * This directive applies to `<frame>`, `<iframe>`, `<embed>`,
+     * and `<applet>` tags. This directive can't be used in
+     * `<meta>` tags and applies only to non-HTML resources.
+     *
+     * @var list<string>|string|null
+     */
+    public $frameAncestors;
+
+    /**
+     * The frame-src directive restricts the URLs which may
+     * be loaded into nested browsing contexts.
+     *
+     * @var list<string>|string|null
+     */
+    public $frameSrc;
+
+    /**
+     * Restricts the origins allowed to deliver video and audio.
+     *
+     * @var list<string>|string|null
+     */
+    public $mediaSrc;
+
+    /**
+     * Allows control over Flash and other plugins.
+     *
+     * @var list<string>|string
+     */
+    public $objectSrc = 'self';
+
+    /**
+     * @var list<string>|string|null
+     */
+    public $manifestSrc;
+
+    /**
+     * Limits the kinds of plugins a page may invoke.
+     *
+     * @var list<string>|string|null
+     */
+    public $pluginTypes;
+
+    /**
+     * List of actions allowed.
+     *
+     * @var list<string>|string|null
+     */
+    public $sandbox;
+
+    /**
+     * Nonce tag for style
+     */
+    public string $styleNonceTag = '{csp-style-nonce}';
+
+    /**
+     * Nonce tag for script
+     */
+    public string $scriptNonceTag = '{csp-script-nonce}';
+
+    /**
+     * Replace nonce tag automatically
+     */
+    public bool $autoNonce = true;
+}

+ 107 - 0
backend/app/Config/Cookie.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use DateTimeInterface;
+
+class Cookie extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Prefix
+     * --------------------------------------------------------------------------
+     *
+     * Set a cookie name prefix if you need to avoid collisions.
+     */
+    public string $prefix = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Expires Timestamp
+     * --------------------------------------------------------------------------
+     *
+     * Default expires timestamp for cookies. Setting this to `0` will mean the
+     * cookie will not have the `Expires` attribute and will behave as a session
+     * cookie.
+     *
+     * @var DateTimeInterface|int|string
+     */
+    public $expires = 0;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Path
+     * --------------------------------------------------------------------------
+     *
+     * Typically will be a forward slash.
+     */
+    public string $path = '/';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Domain
+     * --------------------------------------------------------------------------
+     *
+     * Set to `.your-domain.com` for site-wide cookies.
+     */
+    public string $domain = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Secure
+     * --------------------------------------------------------------------------
+     *
+     * Cookie will only be set if a secure HTTPS connection exists.
+     */
+    public bool $secure = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie HTTPOnly
+     * --------------------------------------------------------------------------
+     *
+     * Cookie will only be accessible via HTTP(S) (no JavaScript).
+     */
+    public bool $httponly = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie SameSite
+     * --------------------------------------------------------------------------
+     *
+     * Configure cookie SameSite setting. Allowed values are:
+     * - None
+     * - Lax
+     * - Strict
+     * - ''
+     *
+     * Alternatively, you can use the constant names:
+     * - `Cookie::SAMESITE_NONE`
+     * - `Cookie::SAMESITE_LAX`
+     * - `Cookie::SAMESITE_STRICT`
+     *
+     * Defaults to `Lax` for compatibility with modern browsers. Setting `''`
+     * (empty string) means default SameSite attribute set by browsers (`Lax`)
+     * will be set on cookies. If set to `None`, `$secure` must also be set.
+     *
+     * @phpstan-var 'None'|'Lax'|'Strict'|''
+     */
+    public string $samesite = 'Lax';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Cookie Raw
+     * --------------------------------------------------------------------------
+     *
+     * This flag allows setting a "raw" cookie, i.e., its name and value are
+     * not URL encoded using `rawurlencode()`.
+     *
+     * If this is set to `true`, cookie names should be compliant of RFC 2616's
+     * list of allowed characters.
+     *
+     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
+     * @see https://tools.ietf.org/html/rfc2616#section-2.2
+     */
+    public bool $raw = false;
+}

+ 105 - 0
backend/app/Config/Cors.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+/**
+ * Cross-Origin Resource Sharing (CORS) Configuration
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+ */
+class Cors extends BaseConfig
+{
+    /**
+     * The default CORS configuration.
+     *
+     * @var array{
+     *      allowedOrigins: list<string>,
+     *      allowedOriginsPatterns: list<string>,
+     *      supportsCredentials: bool,
+     *      allowedHeaders: list<string>,
+     *      exposedHeaders: list<string>,
+     *      allowedMethods: list<string>,
+     *      maxAge: int,
+     *  }
+     */
+    public array $default = [
+        /**
+         * Origins for the `Access-Control-Allow-Origin` header.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
+         *
+         * E.g.:
+         *   - ['http://localhost:8080']
+         *   - ['https://www.example.com']
+         */
+        'allowedOrigins' => [],
+
+        /**
+         * Origin regex patterns for the `Access-Control-Allow-Origin` header.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
+         *
+         * NOTE: A pattern specified here is part of a regular expression. It will
+         *       be actually `#\A<pattern>\z#`.
+         *
+         * E.g.:
+         *   - ['https://\w+\.example\.com']
+         */
+        'allowedOriginsPatterns' => [],
+
+        /**
+         * Weather to send the `Access-Control-Allow-Credentials` header.
+         *
+         * The Access-Control-Allow-Credentials response header tells browsers whether
+         * the server allows cross-origin HTTP requests to include credentials.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
+         */
+        'supportsCredentials' => false,
+
+        /**
+         * Set headers to allow.
+         *
+         * The Access-Control-Allow-Headers response header is used in response to
+         * a preflight request which includes the Access-Control-Request-Headers to
+         * indicate which HTTP headers can be used during the actual request.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
+         */
+        'allowedHeaders' => [],
+
+        /**
+         * Set headers to expose.
+         *
+         * The Access-Control-Expose-Headers response header allows a server to
+         * indicate which response headers should be made available to scripts running
+         * in the browser, in response to a cross-origin request.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
+         */
+        'exposedHeaders' => [],
+
+        /**
+         * Set methods to allow.
+         *
+         * The Access-Control-Allow-Methods response header specifies one or more
+         * methods allowed when accessing a resource in response to a preflight
+         * request.
+         *
+         * E.g.:
+         *   - ['GET', 'POST', 'PUT', 'DELETE']
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
+         */
+        'allowedMethods' => [],
+
+        /**
+         * Set how many seconds the results of a preflight request can be cached.
+         *
+         * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
+         */
+        'maxAge' => 7200,
+    ];
+}

+ 203 - 0
backend/app/Config/Database.php

@@ -0,0 +1,203 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Database\Config;
+
+/**
+ * Database Configuration
+ */
+class Database extends Config
+{
+    /**
+     * The directory that holds the Migrations and Seeds directories.
+     */
+    public string $filesPath = APPPATH . 'Database' . DIRECTORY_SEPARATOR;
+
+    /**
+     * Lets you choose which connection group to use if no other is specified.
+     */
+    public string $defaultGroup = 'default';
+
+    /**
+     * The default database connection.
+     *
+     * @var array<string, mixed>
+     */
+    public array $default = [
+        'DSN'          => '',
+        'hostname'     => 'localhost',
+        'username'     => 'shopdeli',
+        'password'     => 'min010206',
+        'database'     => 'shopdeli',
+        'DBDriver'     => 'MySQLi',
+        'DBPrefix'     => '',
+        'pConnect'     => false,
+        'DBDebug'      => true,
+        'charset'      => 'utf8mb4',
+        'DBCollat'     => 'utf8mb4_general_ci',
+        'swapPre'      => '',
+        'encrypt'      => false,
+        'compress'     => false,
+        'strictOn'     => false,
+        'failover'     => [],
+        'port'         => 3306,
+        'numberNative' => false,
+        'foundRows'    => false,
+        'dateFormat'   => [
+            'date'     => 'Y-m-d',
+            'datetime' => 'Y-m-d H:i:s',
+            'time'     => 'H:i:s',
+        ],
+    ];
+
+    //    /**
+    //     * Sample database connection for SQLite3.
+    //     *
+    //     * @var array<string, mixed>
+    //     */
+    //    public array $default = [
+    //        'database'    => 'database.db',
+    //        'DBDriver'    => 'SQLite3',
+    //        'DBPrefix'    => '',
+    //        'DBDebug'     => true,
+    //        'swapPre'     => '',
+    //        'failover'    => [],
+    //        'foreignKeys' => true,
+    //        'busyTimeout' => 1000,
+    //        'synchronous' => null,
+    //        'dateFormat'  => [
+    //            'date'     => 'Y-m-d',
+    //            'datetime' => 'Y-m-d H:i:s',
+    //            'time'     => 'H:i:s',
+    //        ],
+    //    ];
+
+    //    /**
+    //     * Sample database connection for Postgre.
+    //     *
+    //     * @var array<string, mixed>
+    //     */
+    //    public array $default = [
+    //        'DSN'        => '',
+    //        'hostname'   => 'localhost',
+    //        'username'   => 'root',
+    //        'password'   => 'root',
+    //        'database'   => 'ci4',
+    //        'schema'     => 'public',
+    //        'DBDriver'   => 'Postgre',
+    //        'DBPrefix'   => '',
+    //        'pConnect'   => false,
+    //        'DBDebug'    => true,
+    //        'charset'    => 'utf8',
+    //        'swapPre'    => '',
+    //        'failover'   => [],
+    //        'port'       => 5432,
+    //        'dateFormat' => [
+    //            'date'     => 'Y-m-d',
+    //            'datetime' => 'Y-m-d H:i:s',
+    //            'time'     => 'H:i:s',
+    //        ],
+    //    ];
+
+    //    /**
+    //     * Sample database connection for SQLSRV.
+    //     *
+    //     * @var array<string, mixed>
+    //     */
+    //    public array $default = [
+    //        'DSN'        => '',
+    //        'hostname'   => 'localhost',
+    //        'username'   => 'root',
+    //        'password'   => 'root',
+    //        'database'   => 'ci4',
+    //        'schema'     => 'dbo',
+    //        'DBDriver'   => 'SQLSRV',
+    //        'DBPrefix'   => '',
+    //        'pConnect'   => false,
+    //        'DBDebug'    => true,
+    //        'charset'    => 'utf8',
+    //        'swapPre'    => '',
+    //        'encrypt'    => false,
+    //        'failover'   => [],
+    //        'port'       => 1433,
+    //        'dateFormat' => [
+    //            'date'     => 'Y-m-d',
+    //            'datetime' => 'Y-m-d H:i:s',
+    //            'time'     => 'H:i:s',
+    //        ],
+    //    ];
+
+    //    /**
+    //     * Sample database connection for OCI8.
+    //     *
+    //     * You may need the following environment variables:
+    //     *   NLS_LANG                = 'AMERICAN_AMERICA.UTF8'
+    //     *   NLS_DATE_FORMAT         = 'YYYY-MM-DD HH24:MI:SS'
+    //     *   NLS_TIMESTAMP_FORMAT    = 'YYYY-MM-DD HH24:MI:SS'
+    //     *   NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS'
+    //     *
+    //     * @var array<string, mixed>
+    //     */
+    //    public array $default = [
+    //        'DSN'        => 'localhost:1521/XEPDB1',
+    //        'username'   => 'root',
+    //        'password'   => 'root',
+    //        'DBDriver'   => 'OCI8',
+    //        'DBPrefix'   => '',
+    //        'pConnect'   => false,
+    //        'DBDebug'    => true,
+    //        'charset'    => 'AL32UTF8',
+    //        'swapPre'    => '',
+    //        'failover'   => [],
+    //        'dateFormat' => [
+    //            'date'     => 'Y-m-d',
+    //            'datetime' => 'Y-m-d H:i:s',
+    //            'time'     => 'H:i:s',
+    //        ],
+    //    ];
+
+    /**
+     * This database connection is used when running PHPUnit database tests.
+     *
+     * @var array<string, mixed>
+     */
+    public array $tests = [
+        'DSN'         => '',
+        'hostname'    => '127.0.0.1',
+        'username'    => 'eventinter',
+        'password'    => 'min010206!',
+        'database'    => 'eventinter', //:memory:
+        'DBDriver'    => 'SQLite3',
+        'DBPrefix'    => 'db_',  // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
+        'pConnect'    => false,
+        'DBDebug'     => true,
+        'charset'     => 'utf8',
+        'DBCollat'    => '',
+        'swapPre'     => '',
+        'encrypt'     => false,
+        'compress'    => false,
+        'strictOn'    => false,
+        'failover'    => [],
+        'port'        => 3306,
+        'foreignKeys' => true,
+        'busyTimeout' => 1000,
+        'dateFormat'  => [
+            'date'     => 'Y-m-d',
+            'datetime' => 'Y-m-d H:i:s',
+            'time'     => 'H:i:s',
+        ],
+    ];
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        // Ensure that we always set the database group to 'tests' if
+        // we are currently running an automated test suite, so that
+        // we don't overwrite live data on accident.
+        if (ENVIRONMENT === 'testing') {
+            $this->defaultGroup = 'tests';
+        }
+    }
+}

+ 43 - 0
backend/app/Config/DocTypes.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Config;
+
+class DocTypes
+{
+    /**
+     * List of valid document types.
+     *
+     * @var array<string, string>
+     */
+    public array $list = [
+        'xhtml11'           => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
+        'xhtml1-strict'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
+        'xhtml1-trans'      => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
+        'xhtml1-frame'      => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
+        'xhtml-basic11'     => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
+        'html5'             => '<!DOCTYPE html>',
+        'html4-strict'      => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
+        'html4-trans'       => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
+        'html4-frame'       => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
+        'mathml1'           => '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
+        'mathml2'           => '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
+        'svg10'             => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
+        'svg11'             => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
+        'svg11-basic'       => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
+        'svg11-tiny'        => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
+        'xhtml-math-svg-xh' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
+        'xhtml-math-svg-sh' => '<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
+        'xhtml-rdfa-1'      => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
+        'xhtml-rdfa-2'      => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">',
+    ];
+
+    /**
+     * Whether to remove the solidus (`/`) character for void HTML elements (e.g. `<input>`)
+     * for HTML5 compatibility.
+     *
+     * Set to:
+     *    `true` - to be HTML5 compatible
+     *    `false` - to be XHTML compatible
+     */
+    public bool $html5 = true;
+}

+ 121 - 0
backend/app/Config/Email.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Email extends BaseConfig
+{
+    public string $fromEmail  = '';
+    public string $fromName   = '';
+    public string $recipients = '';
+
+    /**
+     * The "user agent"
+     */
+    public string $userAgent = 'CodeIgniter';
+
+    /**
+     * The mail sending protocol: mail, sendmail, smtp
+     */
+    public string $protocol = 'mail';
+
+    /**
+     * The server path to Sendmail.
+     */
+    public string $mailPath = '/usr/sbin/sendmail';
+
+    /**
+     * SMTP Server Hostname
+     */
+    public string $SMTPHost = '';
+
+    /**
+     * SMTP Username
+     */
+    public string $SMTPUser = '';
+
+    /**
+     * SMTP Password
+     */
+    public string $SMTPPass = '';
+
+    /**
+     * SMTP Port
+     */
+    public int $SMTPPort = 25;
+
+    /**
+     * SMTP Timeout (in seconds)
+     */
+    public int $SMTPTimeout = 5;
+
+    /**
+     * Enable persistent SMTP connections
+     */
+    public bool $SMTPKeepAlive = false;
+
+    /**
+     * SMTP Encryption.
+     *
+     * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command
+     *             to the server. 'ssl' means implicit SSL. Connection on port
+     *             465 should set this to ''.
+     */
+    public string $SMTPCrypto = 'tls';
+
+    /**
+     * Enable word-wrap
+     */
+    public bool $wordWrap = true;
+
+    /**
+     * Character count to wrap at
+     */
+    public int $wrapChars = 76;
+
+    /**
+     * Type of mail, either 'text' or 'html'
+     */
+    public string $mailType = 'text';
+
+    /**
+     * Character set (utf-8, iso-8859-1, etc.)
+     */
+    public string $charset = 'UTF-8';
+
+    /**
+     * Whether to validate the email address
+     */
+    public bool $validate = false;
+
+    /**
+     * Email Priority. 1 = highest. 5 = lowest. 3 = normal
+     */
+    public int $priority = 3;
+
+    /**
+     * Newline character. (Use “\r\n” to comply with RFC 822)
+     */
+    public string $CRLF = "\r\n";
+
+    /**
+     * Newline character. (Use “\r\n” to comply with RFC 822)
+     */
+    public string $newline = "\r\n";
+
+    /**
+     * Enable BCC Batch Mode.
+     */
+    public bool $BCCBatchMode = false;
+
+    /**
+     * Number of emails in each BCC batch
+     */
+    public int $BCCBatchSize = 200;
+
+    /**
+     * Enable notify message from server
+     */
+    public bool $DSN = false;
+}

+ 92 - 0
backend/app/Config/Encryption.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+/**
+ * Encryption configuration.
+ *
+ * These are the settings used for encryption, if you don't pass a parameter
+ * array to the encrypter for creation/initialization.
+ */
+class Encryption extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Encryption Key Starter
+     * --------------------------------------------------------------------------
+     *
+     * If you use the Encryption class you must set an encryption key (seed).
+     * You need to ensure it is long enough for the cipher and mode you plan to use.
+     * See the user guide for more info.
+     */
+    public string $key = '';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Encryption Driver to Use
+     * --------------------------------------------------------------------------
+     *
+     * One of the supported encryption drivers.
+     *
+     * Available drivers:
+     * - OpenSSL
+     * - Sodium
+     */
+    public string $driver = 'OpenSSL';
+
+    /**
+     * --------------------------------------------------------------------------
+     * SodiumHandler's Padding Length in Bytes
+     * --------------------------------------------------------------------------
+     *
+     * This is the number of bytes that will be padded to the plaintext message
+     * before it is encrypted. This value should be greater than zero.
+     *
+     * See the user guide for more information on padding.
+     */
+    public int $blockSize = 16;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Encryption digest
+     * --------------------------------------------------------------------------
+     *
+     * HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
+     */
+    public string $digest = 'SHA512';
+
+    /**
+     * Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
+     * This setting is only used by OpenSSLHandler.
+     *
+     * Set to false for CI3 Encryption compatibility.
+     */
+    public bool $rawData = true;
+
+    /**
+     * Encryption key info.
+     * This setting is only used by OpenSSLHandler.
+     *
+     * Set to 'encryption' for CI3 Encryption compatibility.
+     */
+    public string $encryptKeyInfo = '';
+
+    /**
+     * Authentication key info.
+     * This setting is only used by OpenSSLHandler.
+     *
+     * Set to 'authentication' for CI3 Encryption compatibility.
+     */
+    public string $authKeyInfo = '';
+
+    /**
+     * Cipher to use.
+     * This setting is only used by OpenSSLHandler.
+     *
+     * Set to 'AES-128-CBC' to decrypt encrypted data that encrypted
+     * by CI3 Encryption default configuration.
+     */
+    public string $cipher = 'AES-256-CTR';
+}

+ 55 - 0
backend/app/Config/Events.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Events\Events;
+use CodeIgniter\Exceptions\FrameworkException;
+use CodeIgniter\HotReloader\HotReloader;
+
+/*
+ * --------------------------------------------------------------------
+ * Application Events
+ * --------------------------------------------------------------------
+ * Events allow you to tap into the execution of the program without
+ * modifying or extending core files. This file provides a central
+ * location to define your events, though they can always be added
+ * at run-time, also, if needed.
+ *
+ * You create code that can execute by subscribing to events with
+ * the 'on()' method. This accepts any form of callable, including
+ * Closures, that will be executed when the event is triggered.
+ *
+ * Example:
+ *      Events::on('create', [$myInstance, 'myMethod']);
+ */
+
+Events::on('pre_system', static function (): void {
+    if (ENVIRONMENT !== 'testing') {
+        if (ini_get('zlib.output_compression')) {
+            throw FrameworkException::forEnabledZlibOutputCompression();
+        }
+
+        while (ob_get_level() > 0) {
+            ob_end_flush();
+        }
+
+        ob_start(static fn ($buffer) => $buffer);
+    }
+
+    /*
+     * --------------------------------------------------------------------
+     * Debug Toolbar Listeners.
+     * --------------------------------------------------------------------
+     * If you delete, they will no longer be collected.
+     */
+    if (CI_DEBUG && ! is_cli()) {
+        Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
+        service('toolbar')->respond();
+        // Hot Reload route - for framework use on the hot reloader.
+        if (ENVIRONMENT === 'development') {
+            service('routes')->get('__hot-reload', static function (): void {
+                (new HotReloader())->run();
+            });
+        }
+    }
+});

+ 106 - 0
backend/app/Config/Exceptions.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Debug\ExceptionHandler;
+use CodeIgniter\Debug\ExceptionHandlerInterface;
+use Psr\Log\LogLevel;
+use Throwable;
+
+/**
+ * Setup how the exception handler works.
+ */
+class Exceptions extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * LOG EXCEPTIONS?
+     * --------------------------------------------------------------------------
+     * If true, then exceptions will be logged
+     * through Services::Log.
+     *
+     * Default: true
+     */
+    public bool $log = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * DO NOT LOG STATUS CODES
+     * --------------------------------------------------------------------------
+     * Any status codes here will NOT be logged if logging is turned on.
+     * By default, only 404 (Page Not Found) exceptions are ignored.
+     *
+     * @var list<int>
+     */
+    public array $ignoreCodes = [404];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Error Views Path
+     * --------------------------------------------------------------------------
+     * This is the path to the directory that contains the 'cli' and 'html'
+     * directories that hold the views used to generate errors.
+     *
+     * Default: APPPATH.'Views/errors'
+     */
+    public string $errorViewPath = APPPATH . 'Views/errors';
+
+    /**
+     * --------------------------------------------------------------------------
+     * HIDE FROM DEBUG TRACE
+     * --------------------------------------------------------------------------
+     * Any data that you would like to hide from the debug trace.
+     * In order to specify 2 levels, use "/" to separate.
+     * ex. ['server', 'setup/password', 'secret_token']
+     *
+     * @var list<string>
+     */
+    public array $sensitiveDataInTrace = [];
+
+    /**
+     * --------------------------------------------------------------------------
+     * WHETHER TO THROW AN EXCEPTION ON DEPRECATED ERRORS
+     * --------------------------------------------------------------------------
+     * If set to `true`, DEPRECATED errors are only logged and no exceptions are
+     * thrown. This option also works for user deprecations.
+     */
+    public bool $logDeprecations = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * LOG LEVEL THRESHOLD FOR DEPRECATIONS
+     * --------------------------------------------------------------------------
+     * If `$logDeprecations` is set to `true`, this sets the log level
+     * to which the deprecation will be logged. This should be one of the log
+     * levels recognized by PSR-3.
+     *
+     * The related `Config\Logger::$threshold` should be adjusted, if needed,
+     * to capture logging the deprecations.
+     */
+    public string $deprecationLogLevel = LogLevel::WARNING;
+
+    /*
+     * DEFINE THE HANDLERS USED
+     * --------------------------------------------------------------------------
+     * Given the HTTP status code, returns exception handler that
+     * should be used to deal with this error. By default, it will run CodeIgniter's
+     * default handler and display the error information in the expected format
+     * for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
+     * response format.
+     *
+     * Custom handlers can be returned if you want to handle one or more specific
+     * error codes yourself like:
+     *
+     *      if (in_array($statusCode, [400, 404, 500])) {
+     *          return new \App\Libraries\MyExceptionHandler();
+     *      }
+     *      if ($exception instanceOf PageNotFoundException) {
+     *          return new \App\Libraries\MyExceptionHandler();
+     *      }
+     */
+    public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
+    {
+        return new ExceptionHandler($this);
+    }
+}

+ 37 - 0
backend/app/Config/Feature.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+/**
+ * Enable/disable backward compatibility breaking features.
+ */
+class Feature extends BaseConfig
+{
+    /**
+     * Use improved new auto routing instead of the legacy version.
+     */
+    public bool $autoRoutesImproved = true;
+
+    /**
+     * Use filter execution order in 4.4 or before.
+     */
+    public bool $oldFilterOrder = false;
+
+    /**
+     * The behavior of `limit(0)` in Query Builder.
+     *
+     * If true, `limit(0)` returns all records. (the behavior of 4.4.x or before in version 4.x.)
+     * If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.)
+     */
+    public bool $limitZeroAsAll = true;
+
+    /**
+     * Use strict location negotiation.
+     *
+     * By default, the locale is selected based on a loose comparison of the language code (ISO 639-1)
+     * Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2).
+     */
+    public bool $strictLocaleNegotiation = false;
+}

+ 110 - 0
backend/app/Config/Filters.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\Filters as BaseFilters;
+use CodeIgniter\Filters\Cors;
+use CodeIgniter\Filters\CSRF;
+use CodeIgniter\Filters\DebugToolbar;
+use CodeIgniter\Filters\ForceHTTPS;
+use CodeIgniter\Filters\Honeypot;
+use CodeIgniter\Filters\InvalidChars;
+use CodeIgniter\Filters\PageCache;
+use CodeIgniter\Filters\PerformanceMetrics;
+use CodeIgniter\Filters\SecureHeaders;
+
+class Filters extends BaseFilters
+{
+    /**
+     * Configures aliases for Filter classes to
+     * make reading things nicer and simpler.
+     *
+     * @var array<string, class-string|list<class-string>>
+     *
+     * [filter_name => classname]
+     * or [filter_name => [classname1, classname2, ...]]
+     */
+    public array $aliases = [
+        'csrf'          => CSRF::class,
+        'toolbar'       => DebugToolbar::class,
+        'honeypot'      => Honeypot::class,
+        'invalidchars'  => InvalidChars::class,
+        'secureheaders' => SecureHeaders::class,
+        'cors'          => Cors::class,
+        'forcehttps'    => ForceHTTPS::class,
+        'pagecache'     => PageCache::class,
+        'performance'   => PerformanceMetrics::class,
+        'auth'          => \App\Filters\AuthFilter::class,
+//        'WhiteList'     => \App\Filters\WhiteList::class,
+    ];
+
+    /**
+     * List of special required filters.
+     *
+     * The filters listed here are special. They are applied before and after
+     * other kinds of filters, and always applied even if a route does not exist.
+     *
+     * Filters set by default provide framework functionality. If removed,
+     * those functions will no longer work.
+     *
+     * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
+     *
+     * @var array{before: list<string>, after: list<string>}
+     */
+    public array $required = [
+        'before' => [
+            'forcehttps', // Force Global Secure Requests
+            'pagecache',  // Web Page Caching
+        ],
+        'after' => [
+            'pagecache',   // Web Page Caching
+            'performance', // Performance Metrics
+            'toolbar',     // Debug Toolbar
+        ],
+    ];
+
+    /**
+     * List of filter aliases that are always
+     * applied before and after every request.
+     *
+     * @var array<string, array<string, array<string, string>>>|array<string, list<string>>
+     */
+    public array $globals = [
+        'before' => [
+            // 'honeypot',
+            // 'csrf',
+            // 'invalidchars',
+            //'WhiteList'
+        ],
+        'after' => [
+            // 'honeypot',
+            // 'secureheaders',
+        ],
+    ];
+
+    /**
+     * List of filter aliases that works on a
+     * particular HTTP method (GET, POST, etc.).
+     *
+     * Example:
+     * 'POST' => ['foo', 'bar']
+     *
+     * If you use this, you should disable auto-routing because auto-routing
+     * permits any HTTP method to access a controller. Accessing the controller
+     * with a method you don't expect could bypass the filter.
+     *
+     * @var array<string, list<string>>
+     */
+    public array $methods = [];
+
+    /**
+     * List of filter aliases that should run on any
+     * before or after URI patterns.
+     *
+     * Example:
+     * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
+     *
+     * @var array<string, array<string, list<string>>>
+     */
+    public array $filters = [];
+}

+ 12 - 0
backend/app/Config/ForeignCharacters.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters;
+
+/**
+ * @immutable
+ */
+class ForeignCharacters extends BaseForeignCharacters
+{
+}

+ 64 - 0
backend/app/Config/Format.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Format\JSONFormatter;
+use CodeIgniter\Format\XMLFormatter;
+
+class Format extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Available Response Formats
+     * --------------------------------------------------------------------------
+     *
+     * When you perform content negotiation with the request, these are the
+     * available formats that your application supports. This is currently
+     * only used with the API\ResponseTrait. A valid Formatter must exist
+     * for the specified format.
+     *
+     * These formats are only checked when the data passed to the respond()
+     * method is an array.
+     *
+     * @var list<string>
+     */
+    public array $supportedResponseFormats = [
+        'application/json',
+        'application/xml', // machine-readable XML
+        'text/xml', // human-readable XML
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Formatters
+     * --------------------------------------------------------------------------
+     *
+     * Lists the class to use to format responses with of a particular type.
+     * For each mime type, list the class that should be used. Formatters
+     * can be retrieved through the getFormatter() method.
+     *
+     * @var array<string, string>
+     */
+    public array $formatters = [
+        'application/json' => JSONFormatter::class,
+        'application/xml'  => XMLFormatter::class,
+        'text/xml'         => XMLFormatter::class,
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Formatters Options
+     * --------------------------------------------------------------------------
+     *
+     * Additional Options to adjust default formatters behaviour.
+     * For each mime type, list the additional options that should be used.
+     *
+     * @var array<string, int>
+     */
+    public array $formatterOptions = [
+        'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
+        'application/xml'  => 0,
+        'text/xml'         => 0,
+    ];
+}

+ 44 - 0
backend/app/Config/Generators.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Generators extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Generator Commands' Views
+     * --------------------------------------------------------------------------
+     *
+     * This array defines the mapping of generator commands to the view files
+     * they are using. If you need to customize them for your own, copy these
+     * view files in your own folder and indicate the location here.
+     *
+     * You will notice that the views have special placeholders enclosed in
+     * curly braces `{...}`. These placeholders are used internally by the
+     * generator commands in processing replacements, thus you are warned
+     * not to delete them or modify the names. If you will do so, you may
+     * end up disrupting the scaffolding process and throw errors.
+     *
+     * YOU HAVE BEEN WARNED!
+     *
+     * @var array<string, array<string, string>|string>
+     */
+    public array $views = [
+        'make:cell' => [
+            'class' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php',
+            'view'  => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php',
+        ],
+        'make:command'      => 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
+        'make:config'       => 'CodeIgniter\Commands\Generators\Views\config.tpl.php',
+        'make:controller'   => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
+        'make:entity'       => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
+        'make:filter'       => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
+        'make:migration'    => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
+        'make:model'        => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
+        'make:seeder'       => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
+        'make:validation'   => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
+        'session:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
+    ];
+}

+ 42 - 0
backend/app/Config/Honeypot.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Honeypot extends BaseConfig
+{
+    /**
+     * Makes Honeypot visible or not to human
+     */
+    public bool $hidden = true;
+
+    /**
+     * Honeypot Label Content
+     */
+    public string $label = 'Fill This Field';
+
+    /**
+     * Honeypot Field Name
+     */
+    public string $name = 'honeypot';
+
+    /**
+     * Honeypot HTML Template
+     */
+    public string $template = '<label>{label}</label><input type="text" name="{name}" value="">';
+
+    /**
+     * Honeypot container
+     *
+     * If you enabled CSP, you can remove `style="display:none"`.
+     */
+    public string $container = '<div style="display:none">{template}</div>';
+
+    /**
+     * The id attribute for Honeypot container tag
+     *
+     * Used when CSP is enabled.
+     */
+    public string $containerId = 'hpc';
+}

+ 31 - 0
backend/app/Config/Images.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Images\Handlers\GDHandler;
+use CodeIgniter\Images\Handlers\ImageMagickHandler;
+
+class Images extends BaseConfig
+{
+    /**
+     * Default handler used if no other handler is specified.
+     */
+    public string $defaultHandler = 'gd';
+
+    /**
+     * The path to the image library.
+     * Required for ImageMagick, GraphicsMagick, or NetPBM.
+     */
+    public string $libraryPath = '/usr/local/bin/convert';
+
+    /**
+     * The available handler classes.
+     *
+     * @var array<string, string>
+     */
+    public array $handlers = [
+        'gd'      => GDHandler::class,
+        'imagick' => ImageMagickHandler::class,
+    ];
+}

+ 63 - 0
backend/app/Config/Kint.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Config;
+
+use Kint\Parser\ConstructablePluginInterface;
+use Kint\Renderer\Rich\TabPluginInterface;
+use Kint\Renderer\Rich\ValuePluginInterface;
+
+/**
+ * --------------------------------------------------------------------------
+ * Kint
+ * --------------------------------------------------------------------------
+ *
+ * We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
+ * that you can set to customize how Kint works for you.
+ *
+ * @see https://kint-php.github.io/kint/ for details on these settings.
+ */
+class Kint
+{
+    /*
+    |--------------------------------------------------------------------------
+    | Global Settings
+    |--------------------------------------------------------------------------
+    */
+
+    /**
+     * @var list<class-string<ConstructablePluginInterface>|ConstructablePluginInterface>|null
+     */
+    public $plugins;
+
+    public int $maxDepth           = 6;
+    public bool $displayCalledFrom = true;
+    public bool $expanded          = false;
+
+    /*
+    |--------------------------------------------------------------------------
+    | RichRenderer Settings
+    |--------------------------------------------------------------------------
+    */
+    public string $richTheme = 'aante-light.css';
+    public bool $richFolder  = false;
+
+    /**
+     * @var array<string, class-string<ValuePluginInterface>>|null
+     */
+    public $richObjectPlugins;
+
+    /**
+     * @var array<string, class-string<TabPluginInterface>>|null
+     */
+    public $richTabPlugins;
+
+    /*
+    |--------------------------------------------------------------------------
+    | CLI Settings
+    |--------------------------------------------------------------------------
+    */
+    public bool $cliColors      = true;
+    public bool $cliForceUTF8   = false;
+    public bool $cliDetectWidth = true;
+    public int $cliMinWidth     = 40;
+}

+ 151 - 0
backend/app/Config/Logger.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Log\Handlers\FileHandler;
+
+class Logger extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Error Logging Threshold
+     * --------------------------------------------------------------------------
+     *
+     * You can enable error logging by setting a threshold over zero. The
+     * threshold determines what gets logged. Any values below or equal to the
+     * threshold will be logged.
+     *
+     * Threshold options are:
+     *
+     * - 0 = Disables logging, Error logging TURNED OFF
+     * - 1 = Emergency Messages - System is unusable
+     * - 2 = Alert Messages - Action Must Be Taken Immediately
+     * - 3 = Critical Messages - Application component unavailable, unexpected exception.
+     * - 4 = Runtime Errors - Don't need immediate action, but should be monitored.
+     * - 5 = Warnings - Exceptional occurrences that are not errors.
+     * - 6 = Notices - Normal but significant events.
+     * - 7 = Info - Interesting events, like user logging in, etc.
+     * - 8 = Debug - Detailed debug information.
+     * - 9 = All Messages
+     *
+     * You can also pass an array with threshold levels to show individual error types
+     *
+     *     array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages
+     *
+     * For a live site you'll usually enable Critical or higher (3) to be logged otherwise
+     * your log files will fill up very fast.
+     *
+     * @var int|list<int>
+     */
+    //public $threshold = (ENVIRONMENT === 'production') ? 4 : 9;
+    public $threshold = 4;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Date Format for Logs
+     * --------------------------------------------------------------------------
+     *
+     * Each item that is logged has an associated date. You can use PHP date
+     * codes to set your own date formatting
+     */
+    public string $dateFormat = 'Y-m-d H:i:s';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Log Handlers
+     * --------------------------------------------------------------------------
+     *
+     * The logging system supports multiple actions to be taken when something
+     * is logged. This is done by allowing for multiple Handlers, special classes
+     * designed to write the log to their chosen destinations, whether that is
+     * a file on the getServer, a cloud-based service, or even taking actions such
+     * as emailing the dev team.
+     *
+     * Each handler is defined by the class name used for that handler, and it
+     * MUST implement the `CodeIgniter\Log\Handlers\HandlerInterface` interface.
+     *
+     * The value of each key is an array of configuration items that are sent
+     * to the constructor of each handler. The only required configuration item
+     * is the 'handles' element, which must be an array of integer log levels.
+     * This is most easily handled by using the constants defined in the
+     * `Psr\Log\LogLevel` class.
+     *
+     * Handlers are executed in the order defined in this array, starting with
+     * the handler on top and continuing down.
+     *
+     * @var array<class-string, array<string, int|list<string>|string>>
+     */
+    public array $handlers = [
+        /*
+         * --------------------------------------------------------------------
+         * File Handler
+         * --------------------------------------------------------------------
+         */
+        FileHandler::class => [
+            // The log levels that this handler will handle.
+            'handles' => [
+                'critical',
+                'alert',
+                'emergency',
+                'debug',
+                'error',
+                'info',
+                'notice',
+                'warning',
+            ],
+
+            /*
+             * The default filename extension for log files.
+             * An extension of 'php' allows for protecting the log files via basic
+             * scripting, when they are to be stored under a publicly accessible directory.
+             *
+             * NOTE: Leaving it blank will default to 'log'.
+             */
+            'fileExtension' => '',
+
+            /*
+             * The file system permissions to be applied on newly created log files.
+             *
+             * IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
+             * integer notation (i.e. 0700, 0644, etc.)
+             */
+            'filePermissions' => 0644,
+
+            /*
+             * Logging Directory Path
+             *
+             * By default, logs are written to WRITEPATH . 'logs/'
+             * Specify a different destination here, if desired.
+             */
+            'path' => '',
+        ],
+
+        /*
+         * The ChromeLoggerHandler requires the use of the Chrome web browser
+         * and the ChromeLogger extension. Uncomment this block to use it.
+         */
+        // 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
+        //     /*
+        //      * The log levels that this handler will handle.
+        //      */
+        //     'handles' => ['critical', 'alert', 'emergency', 'debug',
+        //                   'error', 'info', 'notice', 'warning'],
+        // ],
+
+        /*
+         * The ErrorlogHandler writes the logs to PHP's native `error_log()` function.
+         * Uncomment this block to use it.
+         */
+        // 'CodeIgniter\Log\Handlers\ErrorlogHandler' => [
+        //     /* The log levels this handler can handle. */
+        //     'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
+        //
+        //     /*
+        //     * The message type where the error should go. Can be 0 or 4, or use the
+        //     * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4)
+        //     */
+        //     'messageType' => 0,
+        // ],
+    ];
+}

+ 50 - 0
backend/app/Config/Migrations.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Migrations extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Enable/Disable Migrations
+     * --------------------------------------------------------------------------
+     *
+     * Migrations are enabled by default.
+     *
+     * You should enable migrations whenever you intend to do a schema migration
+     * and disable it back when you're done.
+     */
+    public bool $enabled = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Migrations Table
+     * --------------------------------------------------------------------------
+     *
+     * This is the name of the table that will store the current migrations state.
+     * When migrations runs it will store in a database table which migration
+     * files have already been run.
+     */
+    public string $table = 'migrations';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Timestamp Format
+     * --------------------------------------------------------------------------
+     *
+     * This is the format that will be used when creating new migrations
+     * using the CLI command:
+     *   > php spark make:migration
+     *
+     * NOTE: if you set an unsupported format, migration runner will not find
+     *       your migration files.
+     *
+     * Supported formats:
+     * - YmdHis_
+     * - Y-m-d-His_
+     * - Y_m_d_His_
+     */
+    public string $timestampFormat = 'Y-m-d-His_';
+}

+ 534 - 0
backend/app/Config/Mimes.php

@@ -0,0 +1,534 @@
+<?php
+
+namespace Config;
+
+/**
+ * This file contains an array of mime types.  It is used by the
+ * Upload class to help identify allowed file types.
+ *
+ * When more than one variation for an extension exist (like jpg, jpeg, etc)
+ * the most common one should be first in the array to aid the guess*
+ * methods. The same applies when more than one mime-type exists for a
+ * single extension.
+ *
+ * When working with mime types, please make sure you have the ´fileinfo´
+ * extension enabled to reliably detect the media types.
+ */
+class Mimes
+{
+    /**
+     * Map of extensions to mime types.
+     *
+     * @var array<string, list<string>|string>
+     */
+    public static array $mimes = [
+        'hqx' => [
+            'application/mac-binhex40',
+            'application/mac-binhex',
+            'application/x-binhex40',
+            'application/x-mac-binhex40',
+        ],
+        'cpt' => 'application/mac-compactpro',
+        'csv' => [
+            'text/csv',
+            'text/x-comma-separated-values',
+            'text/comma-separated-values',
+            'application/vnd.ms-excel',
+            'application/x-csv',
+            'text/x-csv',
+            'application/csv',
+            'application/excel',
+            'application/vnd.msexcel',
+            'text/plain',
+        ],
+        'bin' => [
+            'application/macbinary',
+            'application/mac-binary',
+            'application/octet-stream',
+            'application/x-binary',
+            'application/x-macbinary',
+        ],
+        'dms' => 'application/octet-stream',
+        'lha' => 'application/octet-stream',
+        'lzh' => 'application/octet-stream',
+        'exe' => [
+            'application/octet-stream',
+            'application/vnd.microsoft.portable-executable',
+            'application/x-dosexec',
+            'application/x-msdownload',
+        ],
+        'class' => 'application/octet-stream',
+        'psd'   => [
+            'application/x-photoshop',
+            'image/vnd.adobe.photoshop',
+        ],
+        'so'  => 'application/octet-stream',
+        'sea' => 'application/octet-stream',
+        'dll' => 'application/octet-stream',
+        'oda' => 'application/oda',
+        'pdf' => [
+            'application/pdf',
+            'application/force-download',
+            'application/x-download',
+        ],
+        'ai' => [
+            'application/pdf',
+            'application/postscript',
+        ],
+        'eps'  => 'application/postscript',
+        'ps'   => 'application/postscript',
+        'smi'  => 'application/smil',
+        'smil' => 'application/smil',
+        'mif'  => 'application/vnd.mif',
+        'xls'  => [
+            'application/vnd.ms-excel',
+            'application/msexcel',
+            'application/x-msexcel',
+            'application/x-ms-excel',
+            'application/x-excel',
+            'application/x-dos_ms_excel',
+            'application/xls',
+            'application/x-xls',
+            'application/excel',
+            'application/download',
+            'application/vnd.ms-office',
+            'application/msword',
+        ],
+        'ppt' => [
+            'application/vnd.ms-powerpoint',
+            'application/powerpoint',
+            'application/vnd.ms-office',
+            'application/msword',
+        ],
+        'pptx' => [
+            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+        ],
+        'wbxml' => 'application/wbxml',
+        'wmlc'  => 'application/wmlc',
+        'dcr'   => 'application/x-director',
+        'dir'   => 'application/x-director',
+        'dxr'   => 'application/x-director',
+        'dvi'   => 'application/x-dvi',
+        'gtar'  => 'application/x-gtar',
+        'gz'    => 'application/x-gzip',
+        'gzip'  => 'application/x-gzip',
+        'php'   => [
+            'application/x-php',
+            'application/x-httpd-php',
+            'application/php',
+            'text/php',
+            'text/x-php',
+            'application/x-httpd-php-source',
+        ],
+        'php4'  => 'application/x-httpd-php',
+        'php3'  => 'application/x-httpd-php',
+        'phtml' => 'application/x-httpd-php',
+        'phps'  => 'application/x-httpd-php-source',
+        'js'    => [
+            'application/x-javascript',
+            'text/plain',
+        ],
+        'swf' => 'application/x-shockwave-flash',
+        'sit' => 'application/x-stuffit',
+        'tar' => 'application/x-tar',
+        'tgz' => [
+            'application/x-tar',
+            'application/x-gzip-compressed',
+        ],
+        'z'     => 'application/x-compress',
+        'xhtml' => 'application/xhtml+xml',
+        'xht'   => 'application/xhtml+xml',
+        'zip'   => [
+            'application/x-zip',
+            'application/zip',
+            'application/x-zip-compressed',
+            'application/s-compressed',
+            'multipart/x-zip',
+        ],
+        'rar' => [
+            'application/vnd.rar',
+            'application/x-rar',
+            'application/rar',
+            'application/x-rar-compressed',
+        ],
+        'mid'  => 'audio/midi',
+        'midi' => 'audio/midi',
+        'mpga' => 'audio/mpeg',
+        'mp2'  => 'audio/mpeg',
+        'mp3'  => [
+            'audio/mpeg',
+            'audio/mpg',
+            'audio/mpeg3',
+            'audio/mp3',
+        ],
+        'aif' => [
+            'audio/x-aiff',
+            'audio/aiff',
+        ],
+        'aiff' => [
+            'audio/x-aiff',
+            'audio/aiff',
+        ],
+        'aifc' => 'audio/x-aiff',
+        'ram'  => 'audio/x-pn-realaudio',
+        'rm'   => 'audio/x-pn-realaudio',
+        'rpm'  => 'audio/x-pn-realaudio-plugin',
+        'ra'   => 'audio/x-realaudio',
+        'rv'   => 'video/vnd.rn-realvideo',
+        'wav'  => [
+            'audio/x-wav',
+            'audio/wave',
+            'audio/wav',
+        ],
+        'bmp' => [
+            'image/bmp',
+            'image/x-bmp',
+            'image/x-bitmap',
+            'image/x-xbitmap',
+            'image/x-win-bitmap',
+            'image/x-windows-bmp',
+            'image/ms-bmp',
+            'image/x-ms-bmp',
+            'application/bmp',
+            'application/x-bmp',
+            'application/x-win-bitmap',
+        ],
+        'gif' => 'image/gif',
+        'jpg' => [
+            'image/jpeg',
+            'image/pjpeg',
+        ],
+        'jpeg' => [
+            'image/jpeg',
+            'image/pjpeg',
+        ],
+        'jpe' => [
+            'image/jpeg',
+            'image/pjpeg',
+        ],
+        'jp2' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'j2k' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'jpf' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'jpg2' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'jpx' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'jpm' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'mj2' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'mjp2' => [
+            'image/jp2',
+            'video/mj2',
+            'image/jpx',
+            'image/jpm',
+        ],
+        'png' => [
+            'image/png',
+            'image/x-png',
+        ],
+        'webp' => 'image/webp',
+        'tif'  => 'image/tiff',
+        'tiff' => 'image/tiff',
+        'css'  => [
+            'text/css',
+            'text/plain',
+        ],
+        'html' => [
+            'text/html',
+            'text/plain',
+        ],
+        'htm' => [
+            'text/html',
+            'text/plain',
+        ],
+        'shtml' => [
+            'text/html',
+            'text/plain',
+        ],
+        'txt'  => 'text/plain',
+        'text' => 'text/plain',
+        'log'  => [
+            'text/plain',
+            'text/x-log',
+        ],
+        'rtx' => 'text/richtext',
+        'rtf' => 'text/rtf',
+        'xml' => [
+            'application/xml',
+            'text/xml',
+            'text/plain',
+        ],
+        'xsl' => [
+            'application/xml',
+            'text/xsl',
+            'text/xml',
+        ],
+        'mpeg' => 'video/mpeg',
+        'mpg'  => 'video/mpeg',
+        'mpe'  => 'video/mpeg',
+        'qt'   => 'video/quicktime',
+        'mov'  => 'video/quicktime',
+        'avi'  => [
+            'video/x-msvideo',
+            'video/msvideo',
+            'video/avi',
+            'application/x-troff-msvideo',
+        ],
+        'movie' => 'video/x-sgi-movie',
+        'doc'   => [
+            'application/msword',
+            'application/vnd.ms-office',
+        ],
+        'docx' => [
+            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+            'application/zip',
+            'application/msword',
+            'application/x-zip',
+        ],
+        'dot' => [
+            'application/msword',
+            'application/vnd.ms-office',
+        ],
+        'dotx' => [
+            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+            'application/zip',
+            'application/msword',
+        ],
+        'xlsx' => [
+            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+            'application/zip',
+            'application/vnd.ms-excel',
+            'application/msword',
+            'application/x-zip',
+        ],
+        'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+        'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+        'word' => [
+            'application/msword',
+            'application/octet-stream',
+        ],
+        'xl'   => 'application/excel',
+        'eml'  => 'message/rfc822',
+        'json' => [
+            'application/json',
+            'text/json',
+        ],
+        'pem' => [
+            'application/x-x509-user-cert',
+            'application/x-pem-file',
+            'application/octet-stream',
+        ],
+        'p10' => [
+            'application/x-pkcs10',
+            'application/pkcs10',
+        ],
+        'p12' => 'application/x-pkcs12',
+        'p7a' => 'application/x-pkcs7-signature',
+        'p7c' => [
+            'application/pkcs7-mime',
+            'application/x-pkcs7-mime',
+        ],
+        'p7m' => [
+            'application/pkcs7-mime',
+            'application/x-pkcs7-mime',
+        ],
+        'p7r' => 'application/x-pkcs7-certreqresp',
+        'p7s' => 'application/pkcs7-signature',
+        'crt' => [
+            'application/x-x509-ca-cert',
+            'application/x-x509-user-cert',
+            'application/pkix-cert',
+        ],
+        'crl' => [
+            'application/pkix-crl',
+            'application/pkcs-crl',
+        ],
+        'der' => 'application/x-x509-ca-cert',
+        'kdb' => 'application/octet-stream',
+        'pgp' => 'application/pgp',
+        'gpg' => 'application/gpg-keys',
+        'sst' => 'application/octet-stream',
+        'csr' => 'application/octet-stream',
+        'rsa' => 'application/x-pkcs7',
+        'cer' => [
+            'application/pkix-cert',
+            'application/x-x509-ca-cert',
+        ],
+        '3g2' => 'video/3gpp2',
+        '3gp' => [
+            'video/3gp',
+            'video/3gpp',
+        ],
+        'mp4' => 'video/mp4',
+        'm4a' => 'audio/x-m4a',
+        'f4v' => [
+            'video/mp4',
+            'video/x-f4v',
+        ],
+        'flv'  => 'video/x-flv',
+        'webm' => 'video/webm',
+        'aac'  => 'audio/x-acc',
+        'm4u'  => 'application/vnd.mpegurl',
+        'm3u'  => 'text/plain',
+        'xspf' => 'application/xspf+xml',
+        'vlc'  => 'application/videolan',
+        'wmv'  => [
+            'video/x-ms-wmv',
+            'video/x-ms-asf',
+        ],
+        'au'   => 'audio/x-au',
+        'ac3'  => 'audio/ac3',
+        'flac' => 'audio/x-flac',
+        'ogg'  => [
+            'audio/ogg',
+            'video/ogg',
+            'application/ogg',
+        ],
+        'kmz' => [
+            'application/vnd.google-earth.kmz',
+            'application/zip',
+            'application/x-zip',
+        ],
+        'kml' => [
+            'application/vnd.google-earth.kml+xml',
+            'application/xml',
+            'text/xml',
+        ],
+        'ics'  => 'text/calendar',
+        'ical' => 'text/calendar',
+        'zsh'  => 'text/x-scriptzsh',
+        '7zip' => [
+            'application/x-compressed',
+            'application/x-zip-compressed',
+            'application/zip',
+            'multipart/x-zip',
+        ],
+        'cdr' => [
+            'application/cdr',
+            'application/coreldraw',
+            'application/x-cdr',
+            'application/x-coreldraw',
+            'image/cdr',
+            'image/x-cdr',
+            'zz-application/zz-winassoc-cdr',
+        ],
+        'wma' => [
+            'audio/x-ms-wma',
+            'video/x-ms-asf',
+        ],
+        'jar' => [
+            'application/java-archive',
+            'application/x-java-application',
+            'application/x-jar',
+            'application/x-compressed',
+        ],
+        'svg' => [
+            'image/svg+xml',
+            'image/svg',
+            'application/xml',
+            'text/xml',
+        ],
+        'vcf' => 'text/x-vcard',
+        'srt' => [
+            'text/srt',
+            'text/plain',
+        ],
+        'vtt' => [
+            'text/vtt',
+            'text/plain',
+        ],
+        'ico' => [
+            'image/x-icon',
+            'image/x-ico',
+            'image/vnd.microsoft.icon',
+        ],
+        'stl' => [
+            'application/sla',
+            'application/vnd.ms-pki.stl',
+            'application/x-navistyle',
+            'model/stl',
+            'application/octet-stream',
+        ],
+    ];
+
+    /**
+     * Attempts to determine the best mime type for the given file extension.
+     *
+     * @return string|null The mime type found, or none if unable to determine.
+     */
+    public static function guessTypeFromExtension(string $extension)
+    {
+        $extension = trim(strtolower($extension), '. ');
+
+        if (! array_key_exists($extension, static::$mimes)) {
+            return null;
+        }
+
+        return is_array(static::$mimes[$extension]) ? static::$mimes[$extension][0] : static::$mimes[$extension];
+    }
+
+    /**
+     * Attempts to determine the best file extension for a given mime type.
+     *
+     * @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
+     *
+     * @return string|null The extension determined, or null if unable to match.
+     */
+    public static function guessExtensionFromType(string $type, ?string $proposedExtension = null)
+    {
+        $type = trim(strtolower($type), '. ');
+
+        $proposedExtension = trim(strtolower($proposedExtension ?? ''));
+
+        if (
+            $proposedExtension !== ''
+            && array_key_exists($proposedExtension, static::$mimes)
+            && in_array($type, (array) static::$mimes[$proposedExtension], true)
+        ) {
+            // The detected mime type matches with the proposed extension.
+            return $proposedExtension;
+        }
+
+        // Reverse check the mime type list if no extension was proposed.
+        // This search is order sensitive!
+        foreach (static::$mimes as $ext => $types) {
+            if (in_array($type, (array) $types, true)) {
+                return $ext;
+            }
+        }
+
+        return null;
+    }
+}

+ 82 - 0
backend/app/Config/Modules.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Modules\Modules as BaseModules;
+
+/**
+ * Modules Configuration.
+ *
+ * NOTE: This class is required prior to Autoloader instantiation,
+ *       and does not extend BaseConfig.
+ */
+class Modules extends BaseModules
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Enable Auto-Discovery?
+     * --------------------------------------------------------------------------
+     *
+     * If true, then auto-discovery will happen across all elements listed in
+     * $aliases below. If false, no auto-discovery will happen at all,
+     * giving a slight performance boost.
+     *
+     * @var bool
+     */
+    public $enabled = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Enable Auto-Discovery Within Composer Packages?
+     * --------------------------------------------------------------------------
+     *
+     * If true, then auto-discovery will happen across all namespaces loaded
+     * by Composer, as well as the namespaces configured locally.
+     *
+     * @var bool
+     */
+    public $discoverInComposer = true;
+
+    /**
+     * The Composer package list for Auto-Discovery
+     * This setting is optional.
+     *
+     * E.g.:
+     *   [
+     *       'only' => [
+     *           // List up all packages to auto-discover
+     *           'codeigniter4/shield',
+     *       ],
+     *   ]
+     *   or
+     *   [
+     *       'exclude' => [
+     *           // List up packages to exclude.
+     *           'pestphp/pest',
+     *       ],
+     *   ]
+     *
+     * @var array{only?: list<string>, exclude?: list<string>}
+     */
+    public $composerPackages = [];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Auto-Discovery Rules
+     * --------------------------------------------------------------------------
+     *
+     * Aliases list of all discovery classes that will be active and used during
+     * the current application request.
+     *
+     * If it is not listed, only the base application elements will be used.
+     *
+     * @var list<string>
+     */
+    public $aliases = [
+        'events',
+        'filters',
+        'registrars',
+        'routes',
+        'services',
+    ];
+}

+ 30 - 0
backend/app/Config/Optimize.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Config;
+
+/**
+ * Optimization Configuration.
+ *
+ * NOTE: This class does not extend BaseConfig for performance reasons.
+ *       So you cannot replace the property values with Environment Variables.
+ */
+class Optimize
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Config Caching
+     * --------------------------------------------------------------------------
+     *
+     * @see https://codeigniter.com/user_guide/concepts/factories.html#config-caching
+     */
+    public bool $configCacheEnabled = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Config Caching
+     * --------------------------------------------------------------------------
+     *
+     * @see https://codeigniter.com/user_guide/concepts/autoloader.html#file-locator-caching
+     */
+    public bool $locatorCacheEnabled = false;
+}

+ 37 - 0
backend/app/Config/Pager.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Pager extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Templates
+     * --------------------------------------------------------------------------
+     *
+     * Pagination links are rendered out using views to configure their
+     * appearance. This array contains aliases and the view names to
+     * use when rendering the links.
+     *
+     * Within each view, the Pager object will be available as $pager,
+     * and the desired group as $pagerGroup;
+     *
+     * @var array<string, string>
+     */
+    public array $templates = [
+        'default_full'   => 'CodeIgniter\Pager\Views\default_full',
+        'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
+        'default_head'   => 'CodeIgniter\Pager\Views\default_head',
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Items Per Page
+     * --------------------------------------------------------------------------
+     *
+     * The default number of results shown in a single page.
+     */
+    public int $perPage = 20;
+}

+ 78 - 0
backend/app/Config/Paths.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Config;
+
+/**
+ * Paths
+ *
+ * Holds the paths that are used by the system to
+ * locate the main directories, app, system, etc.
+ *
+ * Modifying these allows you to restructure your application,
+ * share a system folder between multiple applications, and more.
+ *
+ * All paths are relative to the project's root folder.
+ *
+ * NOTE: This class is required prior to Autoloader instantiation,
+ *       and does not extend BaseConfig.
+ */
+class Paths
+{
+    /**
+     * ---------------------------------------------------------------
+     * SYSTEM FOLDER NAME
+     * ---------------------------------------------------------------
+     *
+     * This must contain the name of your "system" folder. Include
+     * the path if the folder is not in the same directory as this file.
+     */
+    public string $systemDirectory = __DIR__ . '/../../system';
+
+    /**
+     * ---------------------------------------------------------------
+     * APPLICATION FOLDER NAME
+     * ---------------------------------------------------------------
+     *
+     * If you want this front controller to use a different "app"
+     * folder than the default one you can set its name here. The folder
+     * can also be renamed or relocated anywhere on your server. If
+     * you do, use a full server path.
+     *
+     * @see http://codeigniter.com/user_guide/general/managing_apps.html
+     */
+    public string $appDirectory = __DIR__ . '/..';
+
+    /**
+     * ---------------------------------------------------------------
+     * WRITABLE DIRECTORY NAME
+     * ---------------------------------------------------------------
+     *
+     * This variable must contain the name of your "writable" directory.
+     * The writable directory allows you to group all directories that
+     * need write permission to a single place that can be tucked away
+     * for maximum security, keeping it out of the app and/or
+     * system directories.
+     */
+    public string $writableDirectory = __DIR__ . '/../../writable';
+
+    /**
+     * ---------------------------------------------------------------
+     * TESTS DIRECTORY NAME
+     * ---------------------------------------------------------------
+     *
+     * This variable must contain the name of your "tests" directory.
+     */
+    public string $testsDirectory = __DIR__ . '/../../tests';
+
+    /**
+     * ---------------------------------------------------------------
+     * VIEW DIRECTORY NAME
+     * ---------------------------------------------------------------
+     *
+     * This variable must contain the name of the directory that
+     * contains the view files used by your application. By
+     * default this is in `app/Views`. This value
+     * is used when no value is provided to `Services::renderer()`.
+     */
+    public string $viewDirectory = __DIR__ . '/../Views';
+}

+ 28 - 0
backend/app/Config/Publisher.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\Publisher as BasePublisher;
+
+/**
+ * Publisher Configuration
+ *
+ * Defines basic security restrictions for the Publisher class
+ * to prevent abuse by injecting malicious files into a project.
+ */
+class Publisher extends BasePublisher
+{
+    /**
+     * A list of allowed destinations with a (pseudo-)regex
+     * of allowed files for each destination.
+     * Attempts to publish to directories not in this list will
+     * result in a PublisherException. Files that do no fit the
+     * pattern will cause copy/merge to fail.
+     *
+     * @var array<string, string>
+     */
+    public $restrictions = [
+        ROOTPATH => '*',
+        FCPATH   => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
+    ];
+}

+ 173 - 37
backend/app/Config/Routes.php

@@ -22,18 +22,97 @@
   $routes->post('roulette/login', 'Roulette::login'); //로그인 페이지 토큰 상관없이 호출가능
   $routes->post('roulette/refreshToken', 'Roulette::refreshToken'); //엑세스 토큰이 있어야만 발급 가능
   
-  // 디버그 API
-  $routes->get('debug/vendors', 'DebugController::checkVendors'); // 벤더사 데이터 확인
+  $routes->get('alimtalk/send', 'Alimtalk::send');
+  $routes->post('alimtalk/send', 'Alimtalk::send'); // POST 요청인 경우
   
-  // 디버깅용 라우트
-  $routes->group('debug', ['namespace' => 'App\\Controllers'], function($routes) {
-    $routes->get('foreign-key', 'DebugController::debugForeignKey');
-    $routes->get('simple-update', 'DebugController::testSimpleUpdate');
+  $routes->post('winner/reg', 'Winner::winnerReg');
+  $routes->post('winner/itemcount', 'Winner::itemCount');
+  $routes->post('winner/winnerchk', 'Winner::winnerChk');
+
+// 관리자 라우트
+  $routes->post('mng/list', 'Mng::mnglist');
+  $routes->post('mng/search', 'Mng::mngSearch');
+  $routes->post('mng/reg', 'Mng::mngRegister');
+  $routes->post('mng/chk', 'Mng::mngIDChk');
+  $routes->post('mng/update', 'Mng::mngUpdate');
+  $routes->get('mng/detail/(:segment)', 'Mng::mngDetail/$1');
+  $routes->post('mng/stupdate/(:segment)', 'Mng::mngStatusUpdate/$1');
+  $routes->post('mng/delete/(:segment)', 'Mng::mngDelete/$1');
+
+// 아이템 라우트
+  $routes->post('item/list', 'Item::itemlist');
+  $routes->post('item/reg', 'Item::itemRegister');
+  $routes->get('item/detail/(:num)', 'Item::itemDetail/$1');
+  $routes->post('item/update/(:num)', 'Item::itemUpdate/$1');
+  $routes->post('item/delete/(:num)', 'Item::itemDelete/$1');
+  $routes->post('item/search', 'Item::itemSearch');
+// 파일 다운로드
+  $routes->get('item/download/(:segment)', 'Item::file/$1');
+
+// 제품 주문 라우트
+  $routes->post('deli/list', 'Deli::delilist');
+  $routes->post('deli/reg', 'Deli::deliRegister');
+
+// 당첨자 라우트
+  $routes->post('winner/list', 'Winner::winnerlist');
+  $routes->get('winner/detail/(:num)', 'Winner::winnerDetail/$1');
+  $routes->post('winner/partclist', 'Winner::getParticipationByItem');
+  $routes->post('winner/matcheduser', 'Winner::matchedUser');
+  
+  $routes->group('', ['filter' => 'auth'], function ($routes) {
   });
 
-  // 인플루언서-벤더사 매핑 API 그룹
-  $routes->group('api', function($routes) {
-    // 인플루언서 관련 API
+// API 라우트 그룹
+  $routes->group('api', ['namespace' => 'App\Controllers'], function($routes) {
+    
+    // 벤더사 관련 API
+    $routes->group('vendor', function($routes) {
+      $routes->post('search', 'InfluencerController::searchVendors'); // 기존 VendorInfluencerController → InfluencerController
+      $routes->post('list', 'VendorController::getList');
+      $routes->post('detail', 'VendorController::getDetail');
+      $routes->post('create', 'VendorController::create');
+      $routes->post('update', 'VendorController::update');
+      $routes->post('delete', 'VendorController::delete');
+    });
+    
+    // 벤더사-인플루언서 매핑 관련 API (VendorInfluencerController → 새 컨트롤러들로 교체)
+    $routes->group('vendor-influencer', function($routes) {
+      $routes->post('search-vendors', 'InfluencerController::searchVendors'); // 벤더사 검색
+      $routes->post('request', 'InfluencerController::createApprovalRequest'); // 기존 createRequest → createApprovalRequest
+      $routes->post('requests', 'VendorController::getInfluencerRequests'); // 기존 getList → getInfluencerRequests
+      $routes->post('approve', 'VendorController::approveInfluencerRequest'); // 새로 추가 (프론트엔드용)
+      $routes->post('process', 'VendorController::processInfluencerRequest'); // 기존 approveRequest → processInfluencerRequest
+      $routes->post('terminate', 'VendorController::terminatePartnership'); // 파트너십 해지
+      $routes->post('reapply-request', 'InfluencerController::createReapplyRequest'); // 재승인요청
+      $routes->post('list', 'VendorController::getInfluencerRequests'); // 기존 getList → getInfluencerRequests
+      $routes->post('detail', 'VendorController::getRequestDetail'); // 기존 getDetail → getRequestDetail
+      $routes->post('cancel', 'InfluencerController::cancelRequest');
+      $routes->post('stats', 'VendorController::getStatusStats'); // 기존 getStats → getStatusStats
+      $routes->post('history/(:num)', 'VendorController::getHistory/$1');
+      $routes->post('create-request', 'InfluencerController::createApprovalRequest'); // 추가 호환성
+      $routes->post('my-partnerships', 'InfluencerController::getMyPartnerships'); // 추가 호환성
+      $routes->post('process-request', 'VendorController::processInfluencerRequest'); // 추가 호환성
+      $routes->post('status-stats', 'VendorController::getStatusStats'); // 추가 호환성
+    });
+    
+    // 인증 관련 API
+    $routes->group('auth', function($routes) {
+      $routes->post('login', 'AuthController::login');
+      $routes->post('logout', 'AuthController::logout');
+      $routes->post('register', 'AuthController::register');
+      $routes->post('refresh', 'AuthController::refreshToken');
+      $routes->post('verify', 'AuthController::verifyToken');
+    });
+    
+    // 사용자 관련 API
+    $routes->group('user', function($routes) {
+      $routes->post('profile', 'UserController::getProfile');
+      $routes->post('update-profile', 'UserController::updateProfile');
+      $routes->post('change-password', 'UserController::changePassword');
+      $routes->post('upload-avatar', 'UserController::uploadAvatar');
+    });
+    
+    // 인플루언서 관련 API (새로 추가)
     $routes->group('influencer', function($routes) {
         $routes->post('search-vendors', 'InfluencerController::searchVendors');
         $routes->post('create-request', 'InfluencerController::createApprovalRequest');
@@ -43,30 +122,89 @@
         $routes->post('profile', 'InfluencerController::getProfile');
     });
     
-    // 벤더사 관련 API
-    $routes->group('vendor', function($routes) {
-        $routes->post('influencer-requests', 'VendorController::getInfluencerRequests');
-        $routes->post('process-request', 'VendorController::processInfluencerRequest');
-        $routes->post('terminate', 'VendorController::terminatePartnership');
-        $routes->post('status-stats', 'VendorController::getStatusStats');
+    // 제품 관련 API
+    $routes->group('item', function($routes) {
+      $routes->post('list', 'ItemController::getList');
+      $routes->post('detail', 'ItemController::getDetail');
+      $routes->post('create', 'ItemController::create');
+      $routes->post('update', 'ItemController::update');
+      $routes->post('delete', 'ItemController::delete');
+      $routes->post('search', 'ItemController::search');
+    });
+    
+    // 파일 업로드 관련 API
+    $routes->group('upload', function($routes) {
+      $routes->post('image', 'UploadController::uploadImage');
+      $routes->post('file', 'UploadController::uploadFile');
+      $routes->post('multiple', 'UploadController::uploadMultiple');
+    });
+    
+    // 알림 관련 API
+    $routes->group('notification', function($routes) {
+      $routes->post('list', 'NotificationController::getList');
+      $routes->post('mark-read', 'NotificationController::markAsRead');
+      $routes->post('mark-all-read', 'NotificationController::markAllAsRead');
+      $routes->post('delete', 'NotificationController::delete');
     });
+    
+    // 대시보드 관련 API
+    $routes->group('dashboard', function($routes) {
+      $routes->post('stats', 'DashboardController::getStats');
+      $routes->post('recent-activities', 'DashboardController::getRecentActivities');
+      $routes->post('chart-data', 'DashboardController::getChartData');
+    });
+  });
 
-    // 기존 호환성을 위한 라우팅 (점진적 이전용)
-    $routes->post('vendor-influencer/requests', 'VendorController::getInfluencerRequests');
-    $routes->post('vendor-influencer/process-request', 'VendorController::processInfluencerRequest');
-    $routes->post('vendor-influencer/reapply-request', 'InfluencerController::createReapplyRequest');
-    $routes->post('vendor-influencer/search-vendors', 'InfluencerController::searchVendors');
-    $routes->post('vendor-influencer/create-request', 'InfluencerController::createApprovalRequest');
-    $routes->post('vendor-influencer/my-partnerships', 'InfluencerController::getMyPartnerships');
-    $routes->post('vendor-influencer/terminate', 'VendorController::terminatePartnership');
-    $routes->post('vendor-influencer/status-stats', 'VendorController::getStatusStats');
+// 인증이 필요한 API 라우트 (필터 적용)
+  $routes->group('api', ['namespace' => 'App\Controllers', 'filter' => 'auth'], function($routes) {
+    
+    // 보호된 벤더사-인플루언서 API (VendorInfluencerController → 새 컨트롤러들로 교체)
+    $routes->group('vendor-influencer/protected', function($routes) {
+      $routes->post('my-requests', 'InfluencerController::getMyRequests');
+      $routes->post('my-partnerships', 'InfluencerController::getMyPartnerships');
+      $routes->post('pending-approvals', 'VendorController::getPendingApprovals');
+    });
+    
+    // 관리자 전용 API
+    $routes->group('admin', ['filter' => 'admin'], function($routes) {
+      $routes->post('vendor-influencer/all', 'AdminController::getAllMappings');
+      $routes->post('vendor-influencer/expired', 'AdminController::getExpiredRequests');
+      $routes->post('vendor-influencer/process-expired', 'AdminController::processExpiredRequests');
+      $routes->post('system/stats', 'AdminController::getSystemStats');
+    });
+  });
+
+// 웹훅 및 외부 API
+  $routes->group('webhook', ['namespace' => 'App\Controllers'], function($routes) {
+    $routes->post('payment/success', 'WebhookController::paymentSuccess');
+    $routes->post('payment/failure', 'WebhookController::paymentFailure');
+    $routes->post('notification/send', 'WebhookController::sendNotification');
+  });
+
+// 크론잡 및 스케줄러 API
+  $routes->group('cron', ['namespace' => 'App\Controllers', 'filter' => 'cron'], function($routes) {
+    $routes->get('process-expired-requests', 'CronController::processExpiredRequests');
+    $routes->get('send-reminder-notifications', 'CronController::sendReminderNotifications');
+    $routes->get('cleanup-old-data', 'CronController::cleanupOldData');
   });
 
-  // 기존 호환성 라우팅 (최상위 레벨)
-  $routes->post('vendor-influencer/process-request', 'VendorController::processInfluencerRequest');
-  $routes->post('vendor-influencer/reapply-request', 'InfluencerController::createReapplyRequest');
+// 개발 및 테스트용 라우트 (개발 환경에서만 사용)
+  if (ENVIRONMENT === 'development') {
+    $routes->group('dev', ['namespace' => 'App\Controllers'], function($routes) {
+      $routes->get('test-db', 'DevController::testDatabase');
+      $routes->get('seed-data', 'DevController::seedTestData');
+      $routes->get('clear-cache', 'DevController::clearCache');
+      $routes->post('test-api', 'DevController::testApi');
+    });
+  }
+
+// 디버깅용 라우트 (임시)
+  $routes->group('debug', ['namespace' => 'App\\Controllers'], function($routes) {
+    $routes->get('foreign-key', 'DebugController::debugForeignKey');
+    $routes->get('simple-update', 'DebugController::testSimpleUpdate');
+  });
 
-  // 인플루언서 요청 라우트 (기존 구조와 호환성)
+// 인플루언서 요청 라우트 (기존 구조와 호환성)
   $routes->group('influencer-request', function($routes) {
     $routes->post('create', 'InfluencerController::createApprovalRequest');
     $routes->post('vendor-approval', 'VendorController::processInfluencerRequest'); // 벤더사의 인플루언서 승인/거절
@@ -78,13 +216,11 @@
     $routes->post('status-stats', 'VendorController::getStatusStats'); // 벤더사 요청 통계
     $routes->post('reapply-request', 'InfluencerController::createReapplyRequest'); // 인플루언서 재승인 요청
   });
+  
+  $routes->post('api/influencer/profile', 'InfluencerController::getProfile');
+
+    // 디버깅 라우트 (개발용)
+    $routes->get('debug/mapping/(:num)', 'VendorController::debugMappingStatus/$1');
+    $routes->get('debug/mapping', 'VendorController::debugMappingStatus');
+    $routes->post('debug/history-insert', 'VendorController::debugHistoryInsert');
 
-  // Test
-  if (ENVIRONMENT === 'development') {
-    $routes->group('dev', ['namespace' => 'App\Controllers'], function($routes) {
-      $routes->get('test-db', 'DevController::testDatabase');
-      $routes->get('seed-data', 'DevController::seedTestData');
-      $routes->get('clear-cache', 'DevController::clearCache');
-      $routes->post('test-api', 'DevController::testApi');
-    });
-  }

+ 193 - 0
backend/app/Config/Routes.php.bak

@@ -0,0 +1,193 @@
+<?php
+  
+  use CodeIgniter\Router\RouteCollection;
+  
+  /**
+   * @var RouteCollection $routes
+   */
+  $routes->get('auth/googleLogin', 'Auth::googleLogin');
+  $routes->get('auth/callback', 'Auth::callback');
+  $routes->post('auth/joinmember', 'Auth::join');
+  $routes->post('auth/joinvendor', 'Auth::joinVendor');
+  $routes->post('auth/withdrawal', 'Auth::withdrawal'); //구글 회원탈퇴 , 일반회원 탈퇴
+  $routes->post('auth/kakaowithdrawal', 'Auth::kakaoWithdrawal'); //카카오 회웥탈퇴
+  $routes->get('auth/kakaoLogin', 'Auth::kakaoLogin');
+  $routes->get('auth/kakao', 'Auth::kakao');
+  $routes->get('auth/naverLogin', 'Auth::naverLogin');
+  $routes->get('auth/naver', 'Auth::naver');
+  $routes->get('auth/naverWithdrawal', 'Auth::naverWithdrawal');
+  $routes->post('auth/checkId', 'Auth::checkId'); // 사용 중 체크 아이디
+  
+  $routes->get('/', 'Home::index'); //홈화면 리다이렉트용
+  $routes->post('roulette/login', 'Roulette::login'); //로그인 페이지 토큰 상관없이 호출가능
+  $routes->post('roulette/refreshToken', 'Roulette::refreshToken'); //엑세스 토큰이 있어야만 발급 가능
+  
+  $routes->get('alimtalk/send', 'Alimtalk::send');
+  $routes->post('alimtalk/send', 'Alimtalk::send'); // POST 요청인 경우
+  
+  $routes->post('winner/reg', 'Winner::winnerReg');
+  $routes->post('winner/itemcount', 'Winner::itemCount');
+  $routes->post('winner/winnerchk', 'Winner::winnerChk');
+
+// 관리자 라우트
+  $routes->post('mng/list', 'Mng::mnglist');
+  $routes->post('mng/search', 'Mng::mngSearch');
+  $routes->post('mng/reg', 'Mng::mngRegister');
+  $routes->post('mng/chk', 'Mng::mngIDChk');
+  $routes->post('mng/update', 'Mng::mngUpdate');
+  $routes->get('mng/detail/(:segment)', 'Mng::mngDetail/$1');
+  $routes->post('mng/stupdate/(:segment)', 'Mng::mngStatusUpdate/$1');
+  $routes->post('mng/delete/(:segment)', 'Mng::mngDelete/$1');
+
+// 아이템 라우트
+  $routes->post('item/list', 'Item::itemlist');
+  $routes->post('item/reg', 'Item::itemRegister');
+  $routes->get('item/detail/(:num)', 'Item::itemDetail/$1');
+  $routes->post('item/update/(:num)', 'Item::itemUpdate/$1');
+  $routes->post('item/delete/(:num)', 'Item::itemDelete/$1');
+  $routes->post('item/search', 'Item::itemSearch');
+// 파일 다운로드
+  $routes->get('item/download/(:segment)', 'Item::file/$1');
+
+// 제품 주문 라우트
+  $routes->post('deli/list', 'Deli::delilist');
+  $routes->post('deli/reg', 'Deli::deliRegister');
+
+// 당첨자 라우트
+  $routes->post('winner/list', 'Winner::winnerlist');
+  $routes->get('winner/detail/(:num)', 'Winner::winnerDetail/$1');
+  $routes->post('winner/partclist', 'Winner::getParticipationByItem');
+  $routes->post('winner/matcheduser', 'Winner::matchedUser');
+  
+  $routes->group('', ['filter' => 'auth'], function ($routes) {
+  });
+
+// API 라우트 그룹
+  $routes->group('api', ['namespace' => 'App\Controllers'], function($routes) {
+    
+    // 벤더사 관련 API
+    $routes->group('vendor', function($routes) {
+      $routes->post('search', 'VendorInfluencerController::searchVendors');
+      $routes->post('list', 'VendorController::getList');
+      $routes->post('detail', 'VendorController::getDetail');
+      $routes->post('create', 'VendorController::create');
+      $routes->post('update', 'VendorController::update');
+      $routes->post('delete', 'VendorController::delete');
+    });
+    
+    // 벤더사-인플루언서 매핑 관련 API
+    $routes->group('vendor-influencer', function($routes) {
+      $routes->post('search-vendors', 'VendorInfluencerController::searchVendors'); // 벤더사 검색
+      $routes->post('request', 'VendorInfluencerController::createRequest');
+      $routes->post('requests', 'VendorInfluencerController::getList');         // 요청목록 조회 (벤더사용)
+      $routes->post('approve', 'VendorInfluencerController::approveRequest');
+      $routes->post('process', 'VendorInfluencerController::approveRequest');   // 승인/거부 처리 (통합)
+      $routes->post('terminate', 'VendorInfluencerController::terminate');      // 파트너십 해지
+      $routes->post('reapply-request', 'VendorInfluencerController::reapplyRequest'); // 재승인요청
+      $routes->post('list', 'VendorInfluencerController::getList');
+      $routes->post('detail', 'VendorInfluencerController::getDetail');
+      $routes->post('cancel', 'VendorInfluencerController::cancelRequest');
+      $routes->post('stats', 'VendorInfluencerController::getStats');
+      $routes->post('history/(:num)', 'VendorInfluencerController::getHistory/$1');
+    });
+    
+    // 인증 관련 API
+    $routes->group('auth', function($routes) {
+      $routes->post('login', 'AuthController::login');
+      $routes->post('logout', 'AuthController::logout');
+      $routes->post('register', 'AuthController::register');
+      $routes->post('refresh', 'AuthController::refreshToken');
+      $routes->post('verify', 'AuthController::verifyToken');
+    });
+    
+    // 사용자 관련 API
+    $routes->group('user', function($routes) {
+      $routes->post('profile', 'UserController::getProfile');
+      $routes->post('update-profile', 'UserController::updateProfile');
+      $routes->post('change-password', 'UserController::changePassword');
+      $routes->post('upload-avatar', 'UserController::uploadAvatar');
+    });
+    
+    // 제품 관련 API
+    $routes->group('item', function($routes) {
+      $routes->post('list', 'ItemController::getList');
+      $routes->post('detail', 'ItemController::getDetail');
+      $routes->post('create', 'ItemController::create');
+      $routes->post('update', 'ItemController::update');
+      $routes->post('delete', 'ItemController::delete');
+      $routes->post('search', 'ItemController::search');
+    });
+    
+    // 파일 업로드 관련 API
+    $routes->group('upload', function($routes) {
+      $routes->post('image', 'UploadController::uploadImage');
+      $routes->post('file', 'UploadController::uploadFile');
+      $routes->post('multiple', 'UploadController::uploadMultiple');
+    });
+    
+    // 알림 관련 API
+    $routes->group('notification', function($routes) {
+      $routes->post('list', 'NotificationController::getList');
+      $routes->post('mark-read', 'NotificationController::markAsRead');
+      $routes->post('mark-all-read', 'NotificationController::markAllAsRead');
+      $routes->post('delete', 'NotificationController::delete');
+    });
+    
+    // 대시보드 관련 API
+    $routes->group('dashboard', function($routes) {
+      $routes->post('stats', 'DashboardController::getStats');
+      $routes->post('recent-activities', 'DashboardController::getRecentActivities');
+      $routes->post('chart-data', 'DashboardController::getChartData');
+    });
+  });
+
+// 인증이 필요한 API 라우트 (필터 적용)
+  $routes->group('api', ['namespace' => 'App\Controllers', 'filter' => 'auth'], function($routes) {
+    
+    // 보호된 벤더사-인플루언서 API
+    $routes->group('vendor-influencer/protected', function($routes) {
+      $routes->post('my-requests', 'VendorInfluencerController::getMyRequests');
+      $routes->post('my-partnerships', 'VendorInfluencerController::getMyPartnerships');
+      $routes->post('pending-approvals', 'VendorInfluencerController::getPendingApprovals');
+    });
+    
+    // 관리자 전용 API
+    $routes->group('admin', ['filter' => 'admin'], function($routes) {
+      $routes->post('vendor-influencer/all', 'AdminController::getAllMappings');
+      $routes->post('vendor-influencer/expired', 'AdminController::getExpiredRequests');
+      $routes->post('vendor-influencer/process-expired', 'AdminController::processExpiredRequests');
+      $routes->post('system/stats', 'AdminController::getSystemStats');
+    });
+  });
+
+// 웹훅 및 외부 API
+  $routes->group('webhook', ['namespace' => 'App\Controllers'], function($routes) {
+    $routes->post('payment/success', 'WebhookController::paymentSuccess');
+    $routes->post('payment/failure', 'WebhookController::paymentFailure');
+    $routes->post('notification/send', 'WebhookController::sendNotification');
+  });
+
+// 크론잡 및 스케줄러 API
+  $routes->group('cron', ['namespace' => 'App\Controllers', 'filter' => 'cron'], function($routes) {
+    $routes->get('process-expired-requests', 'CronController::processExpiredRequests');
+    $routes->get('send-reminder-notifications', 'CronController::sendReminderNotifications');
+    $routes->get('cleanup-old-data', 'CronController::cleanupOldData');
+  });
+
+// 개발 및 테스트용 라우트 (개발 환경에서만 사용)
+  if (ENVIRONMENT === 'development') {
+    $routes->group('dev', ['namespace' => 'App\Controllers'], function($routes) {
+      $routes->get('test-db', 'DevController::testDatabase');
+      $routes->get('seed-data', 'DevController::seedTestData');
+      $routes->get('clear-cache', 'DevController::clearCache');
+      $routes->post('test-api', 'DevController::testApi');
+    });
+  }
+
+// 디버깅용 라우트 (임시)
+  $routes->group('debug', ['namespace' => 'App\\Controllers'], function($routes) {
+    $routes->get('foreign-key', 'DebugController::debugForeignKey');
+    $routes->get('simple-update', 'DebugController::testSimpleUpdate');
+  });
+  
+  $routes->post('api/influencer/profile', 'InfluencerController::getProfile');

+ 140 - 0
backend/app/Config/Routing.php

@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * This file is part of CodeIgniter 4 framework.
+ *
+ * (c) CodeIgniter Foundation <admin@codeigniter.com>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Config;
+
+use CodeIgniter\Config\Routing as BaseRouting;
+
+/**
+ * Routing configuration
+ */
+class Routing extends BaseRouting
+{
+    /**
+     * For Defined Routes.
+     * An array of files that contain route definitions.
+     * Route files are read in order, with the first match
+     * found taking precedence.
+     *
+     * Default: APPPATH . 'Config/Routes.php'
+     *
+     * @var list<string>
+     */
+    public array $routeFiles = [
+        APPPATH . 'Config/Routes.php',
+    ];
+
+    /**
+     * For Defined Routes and Auto Routing.
+     * The default namespace to use for Controllers when no other
+     * namespace has been specified.
+     *
+     * Default: 'App\Controllers'
+     */
+    public string $defaultNamespace = 'App\Controllers';
+
+    /**
+     * For Auto Routing.
+     * The default controller to use when no other controller has been
+     * specified.
+     *
+     * Default: 'Home'
+     */
+    public string $defaultController = 'Home';
+
+    /**
+     * For Defined Routes and Auto Routing.
+     * The default method to call on the controller when no other
+     * method has been set in the route.
+     *
+     * Default: 'index'
+     */
+    public string $defaultMethod = 'index';
+
+    /**
+     * For Auto Routing.
+     * Whether to translate dashes in URIs for controller/method to underscores.
+     * Primarily useful when using the auto-routing.
+     *
+     * Default: false
+     */
+    public bool $translateURIDashes = false;
+
+    /**
+     * Sets the class/method that should be called if routing doesn't
+     * find a match. It can be the controller/method name like: Users::index
+     *
+     * This setting is passed to the Router class and handled there.
+     *
+     * If you want to use a closure, you will have to set it in the
+     * routes file by calling:
+     *
+     * $routes->set404Override(function() {
+     *    // Do something here
+     * });
+     *
+     * Example:
+     *  public $override404 = 'App\Errors::show404';
+     */
+    public ?string $override404 = null;
+
+    /**
+     * If TRUE, the system will attempt to match the URI against
+     * Controllers by matching each segment against folders/files
+     * in APPPATH/Controllers, when a match wasn't found against
+     * defined routes.
+     *
+     * If FALSE, will stop searching and do NO automatic routing.
+     */
+    public bool $autoRoute = false;
+
+    /**
+     * For Defined Routes.
+     * If TRUE, will enable the use of the 'prioritize' option
+     * when defining routes.
+     *
+     * Default: false
+     */
+    public bool $prioritize = false;
+
+    /**
+     * For Defined Routes.
+     * If TRUE, matched multiple URI segments will be passed as one parameter.
+     *
+     * Default: false
+     */
+    public bool $multipleSegmentsOneParam = false;
+
+    /**
+     * For Auto Routing (Improved).
+     * Map of URI segments and namespaces.
+     *
+     * The key is the first URI segment. The value is the controller namespace.
+     * E.g.,
+     *   [
+     *       'blog' => 'Acme\Blog\Controllers',
+     *   ]
+     *
+     * @var array<string, string>
+     */
+    public array $moduleRoutes = [];
+
+    /**
+     * For Auto Routing (Improved).
+     * Whether to translate dashes in URIs for controller/method to CamelCase.
+     * E.g., blog-controller -> BlogController
+     *
+     * If you enable this, $translateURIDashes is ignored.
+     *
+     * Default: false
+     */
+    public bool $translateUriToCamelCase = true;
+}

+ 86 - 0
backend/app/Config/Security.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class Security extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Protection Method
+     * --------------------------------------------------------------------------
+     *
+     * Protection Method for Cross Site Request Forgery protection.
+     *
+     * @var string 'cookie' or 'session'
+     */
+    public string $csrfProtection = 'cookie';
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Token Randomization
+     * --------------------------------------------------------------------------
+     *
+     * Randomize the CSRF Token for added security.
+     */
+    public bool $tokenRandomize = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Token Name
+     * --------------------------------------------------------------------------
+     *
+     * Token name for Cross Site Request Forgery protection.
+     */
+    public string $tokenName = 'csrf_test_name';
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Header Name
+     * --------------------------------------------------------------------------
+     *
+     * Header name for Cross Site Request Forgery protection.
+     */
+    public string $headerName = 'X-CSRF-TOKEN';
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Cookie Name
+     * --------------------------------------------------------------------------
+     *
+     * Cookie name for Cross Site Request Forgery protection.
+     */
+    public string $cookieName = 'csrf_cookie_name';
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Expires
+     * --------------------------------------------------------------------------
+     *
+     * Expiration time for Cross Site Request Forgery protection cookie.
+     *
+     * Defaults to two hours (in seconds).
+     */
+    public int $expires = 7200;
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Regenerate
+     * --------------------------------------------------------------------------
+     *
+     * Regenerate CSRF Token on every submission.
+     */
+    public bool $regenerate = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * CSRF Redirect
+     * --------------------------------------------------------------------------
+     *
+     * Redirect to previous page with error on failure.
+     *
+     * @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure
+     */
+    public bool $redirect = (ENVIRONMENT === 'production');
+}

+ 32 - 0
backend/app/Config/Services.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseService;
+
+/**
+ * Services Configuration file.
+ *
+ * Services are simply other classes/libraries that the system uses
+ * to do its job. This is used by CodeIgniter to allow the core of the
+ * framework to be swapped out easily without affecting the usage within
+ * the rest of your application.
+ *
+ * This file holds any application-specific services, or service overrides
+ * that you might need. An example has been included with the general
+ * method format you should use for your service methods. For more examples,
+ * see the core Services file at system/Config/Services.php.
+ */
+class Services extends BaseService
+{
+    /*
+     * public static function example($getShared = true)
+     * {
+     *     if ($getShared) {
+     *         return static::getSharedInstance('example');
+     *     }
+     *
+     *     return new \CodeIgniter\Example();
+     * }
+     */
+}

+ 127 - 0
backend/app/Config/Session.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Session\Handlers\BaseHandler;
+use CodeIgniter\Session\Handlers\FileHandler;
+
+class Session extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Session Driver
+     * --------------------------------------------------------------------------
+     *
+     * The session storage driver to use:
+     * - `CodeIgniter\Session\Handlers\FileHandler`
+     * - `CodeIgniter\Session\Handlers\DatabaseHandler`
+     * - `CodeIgniter\Session\Handlers\MemcachedHandler`
+     * - `CodeIgniter\Session\Handlers\RedisHandler`
+     *
+     * @var class-string<BaseHandler>
+     */
+    public string $driver = FileHandler::class;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Cookie Name
+     * --------------------------------------------------------------------------
+     *
+     * The session cookie name, must contain only [0-9a-z_-] characters
+     */
+    public string $cookieName = 'ci_session';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Expiration
+     * --------------------------------------------------------------------------
+     *
+     * The number of SECONDS you want the session to last.
+     * Setting to 0 (zero) means expire when the browser is closed.
+     */
+    public int $expiration = 7200;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Save Path
+     * --------------------------------------------------------------------------
+     *
+     * The location to save sessions to and is driver dependent.
+     *
+     * For the 'files' driver, it's a path to a writable directory.
+     * WARNING: Only absolute paths are supported!
+     *
+     * For the 'database' driver, it's a table name.
+     * Please read up the manual for the format with other session drivers.
+     *
+     * IMPORTANT: You are REQUIRED to set a valid save path!
+     */
+    public string $savePath = WRITEPATH . 'session';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Match IP
+     * --------------------------------------------------------------------------
+     *
+     * Whether to match the user's IP address when reading the session data.
+     *
+     * WARNING: If you're using the database driver, don't forget to update
+     *          your session table's PRIMARY KEY when changing this setting.
+     */
+    public bool $matchIP = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Time to Update
+     * --------------------------------------------------------------------------
+     *
+     * How many seconds between CI regenerating the session ID.
+     */
+    public int $timeToUpdate = 300;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Regenerate Destroy
+     * --------------------------------------------------------------------------
+     *
+     * Whether to destroy session data associated with the old session ID
+     * when auto-regenerating the session ID. When set to FALSE, the data
+     * will be later deleted by the garbage collector.
+     */
+    public bool $regenerateDestroy = false;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Session Database Group
+     * --------------------------------------------------------------------------
+     *
+     * DB Group for the database session.
+     */
+    public ?string $DBGroup = null;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Lock Retry Interval (microseconds)
+     * --------------------------------------------------------------------------
+     *
+     * This is used for RedisHandler.
+     *
+     * Time (microseconds) to wait if lock cannot be acquired.
+     * The default is 100,000 microseconds (= 0.1 seconds).
+     */
+    public int $lockRetryInterval = 100_000;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Lock Max Retries
+     * --------------------------------------------------------------------------
+     *
+     * This is used for RedisHandler.
+     *
+     * Maximum number of lock acquisition attempts.
+     * The default is 300 times. That is lock timeout is about 30 (0.1 * 300)
+     * seconds.
+     */
+    public int $lockMaxRetries = 300;
+}

+ 122 - 0
backend/app/Config/Toolbar.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Debug\Toolbar\Collectors\Database;
+use CodeIgniter\Debug\Toolbar\Collectors\Events;
+use CodeIgniter\Debug\Toolbar\Collectors\Files;
+use CodeIgniter\Debug\Toolbar\Collectors\Logs;
+use CodeIgniter\Debug\Toolbar\Collectors\Routes;
+use CodeIgniter\Debug\Toolbar\Collectors\Timers;
+use CodeIgniter\Debug\Toolbar\Collectors\Views;
+
+/**
+ * --------------------------------------------------------------------------
+ * Debug Toolbar
+ * --------------------------------------------------------------------------
+ *
+ * The Debug Toolbar provides a way to see information about the performance
+ * and state of your application during that page display. By default it will
+ * NOT be displayed under production environments, and will only display if
+ * `CI_DEBUG` is true, since if it's not, there's not much to display anyway.
+ */
+class Toolbar extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * Toolbar Collectors
+     * --------------------------------------------------------------------------
+     *
+     * List of toolbar collectors that will be called when Debug Toolbar
+     * fires up and collects data from.
+     *
+     * @var list<class-string>
+     */
+    public array $collectors = [
+        Timers::class,
+        Database::class,
+        Logs::class,
+        Views::class,
+        // \CodeIgniter\Debug\Toolbar\Collectors\Cache::class,
+        Files::class,
+        Routes::class,
+        Events::class,
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Collect Var Data
+     * --------------------------------------------------------------------------
+     *
+     * If set to false var data from the views will not be collected. Useful to
+     * avoid high memory usage when there are lots of data passed to the view.
+     */
+    public bool $collectVarData = true;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Max History
+     * --------------------------------------------------------------------------
+     *
+     * `$maxHistory` sets a limit on the number of past requests that are stored,
+     * helping to conserve file space used to store them. You can set it to
+     * 0 (zero) to not have any history stored, or -1 for unlimited history.
+     */
+    public int $maxHistory = 20;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Toolbar Views Path
+     * --------------------------------------------------------------------------
+     *
+     * The full path to the the views that are used by the toolbar.
+     * This MUST have a trailing slash.
+     */
+    public string $viewsPath = SYSTEMPATH . 'Debug/Toolbar/Views/';
+
+    /**
+     * --------------------------------------------------------------------------
+     * Max Queries
+     * --------------------------------------------------------------------------
+     *
+     * If the Database Collector is enabled, it will log every query that the
+     * the system generates so they can be displayed on the toolbar's timeline
+     * and in the query log. This can lead to memory issues in some instances
+     * with hundreds of queries.
+     *
+     * `$maxQueries` defines the maximum amount of queries that will be stored.
+     */
+    public int $maxQueries = 100;
+
+    /**
+     * --------------------------------------------------------------------------
+     * Watched Directories
+     * --------------------------------------------------------------------------
+     *
+     * Contains an array of directories that will be watched for changes and
+     * used to determine if the hot-reload feature should reload the page or not.
+     * We restrict the values to keep performance as high as possible.
+     *
+     * NOTE: The ROOTPATH will be prepended to all values.
+     *
+     * @var list<string>
+     */
+    public array $watchedDirectories = [
+        'app',
+    ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Watched File Extensions
+     * --------------------------------------------------------------------------
+     *
+     * Contains an array of file extensions that will be watched for changes and
+     * used to determine if the hot-reload feature should reload the page or not.
+     *
+     * @var list<string>
+     */
+    public array $watchedExtensions = [
+        'php', 'css', 'js', 'html', 'svg', 'json', 'env',
+    ];
+}

+ 252 - 0
backend/app/Config/UserAgents.php

@@ -0,0 +1,252 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+/**
+ * -------------------------------------------------------------------
+ * User Agents
+ * -------------------------------------------------------------------
+ *
+ * This file contains four arrays of user agent data. It is used by the
+ * User Agent Class to help identify browser, platform, robot, and
+ * mobile device data. The array keys are used to identify the device
+ * and the array values are used to set the actual name of the item.
+ */
+class UserAgents extends BaseConfig
+{
+    /**
+     * -------------------------------------------------------------------
+     * OS Platforms
+     * -------------------------------------------------------------------
+     *
+     * @var array<string, string>
+     */
+    public array $platforms = [
+        'windows nt 10.0' => 'Windows 10',
+        'windows nt 6.3'  => 'Windows 8.1',
+        'windows nt 6.2'  => 'Windows 8',
+        'windows nt 6.1'  => 'Windows 7',
+        'windows nt 6.0'  => 'Windows Vista',
+        'windows nt 5.2'  => 'Windows 2003',
+        'windows nt 5.1'  => 'Windows XP',
+        'windows nt 5.0'  => 'Windows 2000',
+        'windows nt 4.0'  => 'Windows NT 4.0',
+        'winnt4.0'        => 'Windows NT 4.0',
+        'winnt 4.0'       => 'Windows NT',
+        'winnt'           => 'Windows NT',
+        'windows 98'      => 'Windows 98',
+        'win98'           => 'Windows 98',
+        'windows 95'      => 'Windows 95',
+        'win95'           => 'Windows 95',
+        'windows phone'   => 'Windows Phone',
+        'windows'         => 'Unknown Windows OS',
+        'android'         => 'Android',
+        'blackberry'      => 'BlackBerry',
+        'iphone'          => 'iOS',
+        'ipad'            => 'iOS',
+        'ipod'            => 'iOS',
+        'os x'            => 'Mac OS X',
+        'ppc mac'         => 'Power PC Mac',
+        'freebsd'         => 'FreeBSD',
+        'ppc'             => 'Macintosh',
+        'linux'           => 'Linux',
+        'debian'          => 'Debian',
+        'sunos'           => 'Sun Solaris',
+        'beos'            => 'BeOS',
+        'apachebench'     => 'ApacheBench',
+        'aix'             => 'AIX',
+        'irix'            => 'Irix',
+        'osf'             => 'DEC OSF',
+        'hp-ux'           => 'HP-UX',
+        'netbsd'          => 'NetBSD',
+        'bsdi'            => 'BSDi',
+        'openbsd'         => 'OpenBSD',
+        'gnu'             => 'GNU/Linux',
+        'unix'            => 'Unknown Unix OS',
+        'symbian'         => 'Symbian OS',
+    ];
+
+    /**
+     * -------------------------------------------------------------------
+     * Browsers
+     * -------------------------------------------------------------------
+     *
+     * The order of this array should NOT be changed. Many browsers return
+     * multiple browser types so we want to identify the subtype first.
+     *
+     * @var array<string, string>
+     */
+    public array $browsers = [
+        'OPR'    => 'Opera',
+        'Flock'  => 'Flock',
+        'Edge'   => 'Spartan',
+        'Edg'    => 'Edge',
+        'Chrome' => 'Chrome',
+        // Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
+        'Opera.*?Version'   => 'Opera',
+        'Opera'             => 'Opera',
+        'MSIE'              => 'Internet Explorer',
+        'Internet Explorer' => 'Internet Explorer',
+        'Trident.* rv'      => 'Internet Explorer',
+        'Shiira'            => 'Shiira',
+        'Firefox'           => 'Firefox',
+        'Chimera'           => 'Chimera',
+        'Phoenix'           => 'Phoenix',
+        'Firebird'          => 'Firebird',
+        'Camino'            => 'Camino',
+        'Netscape'          => 'Netscape',
+        'OmniWeb'           => 'OmniWeb',
+        'Safari'            => 'Safari',
+        'Mozilla'           => 'Mozilla',
+        'Konqueror'         => 'Konqueror',
+        'icab'              => 'iCab',
+        'Lynx'              => 'Lynx',
+        'Links'             => 'Links',
+        'hotjava'           => 'HotJava',
+        'amaya'             => 'Amaya',
+        'IBrowse'           => 'IBrowse',
+        'Maxthon'           => 'Maxthon',
+        'Ubuntu'            => 'Ubuntu Web Browser',
+        'Vivaldi'           => 'Vivaldi',
+    ];
+
+    /**
+     * -------------------------------------------------------------------
+     * Mobiles
+     * -------------------------------------------------------------------
+     *
+     * @var array<string, string>
+     */
+    public array $mobiles = [
+        // legacy array, old values commented out
+        'mobileexplorer' => 'Mobile Explorer',
+        // 'openwave'             => 'Open Wave',
+        // 'opera mini'           => 'Opera Mini',
+        // 'operamini'            => 'Opera Mini',
+        // 'elaine'               => 'Palm',
+        'palmsource' => 'Palm',
+        // 'digital paths'        => 'Palm',
+        // 'avantgo'              => 'Avantgo',
+        // 'xiino'                => 'Xiino',
+        'palmscape' => 'Palmscape',
+        // 'nokia'                => 'Nokia',
+        // 'ericsson'             => 'Ericsson',
+        // 'blackberry'           => 'BlackBerry',
+        // 'motorola'             => 'Motorola'
+
+        // Phones and Manufacturers
+        'motorola'             => 'Motorola',
+        'nokia'                => 'Nokia',
+        'palm'                 => 'Palm',
+        'iphone'               => 'Apple iPhone',
+        'ipad'                 => 'iPad',
+        'ipod'                 => 'Apple iPod Touch',
+        'sony'                 => 'Sony Ericsson',
+        'ericsson'             => 'Sony Ericsson',
+        'blackberry'           => 'BlackBerry',
+        'cocoon'               => 'O2 Cocoon',
+        'blazer'               => 'Treo',
+        'lg'                   => 'LG',
+        'amoi'                 => 'Amoi',
+        'xda'                  => 'XDA',
+        'mda'                  => 'MDA',
+        'vario'                => 'Vario',
+        'htc'                  => 'HTC',
+        'samsung'              => 'Samsung',
+        'sharp'                => 'Sharp',
+        'sie-'                 => 'Siemens',
+        'alcatel'              => 'Alcatel',
+        'benq'                 => 'BenQ',
+        'ipaq'                 => 'HP iPaq',
+        'mot-'                 => 'Motorola',
+        'playstation portable' => 'PlayStation Portable',
+        'playstation 3'        => 'PlayStation 3',
+        'playstation vita'     => 'PlayStation Vita',
+        'hiptop'               => 'Danger Hiptop',
+        'nec-'                 => 'NEC',
+        'panasonic'            => 'Panasonic',
+        'philips'              => 'Philips',
+        'sagem'                => 'Sagem',
+        'sanyo'                => 'Sanyo',
+        'spv'                  => 'SPV',
+        'zte'                  => 'ZTE',
+        'sendo'                => 'Sendo',
+        'nintendo dsi'         => 'Nintendo DSi',
+        'nintendo ds'          => 'Nintendo DS',
+        'nintendo 3ds'         => 'Nintendo 3DS',
+        'wii'                  => 'Nintendo Wii',
+        'open web'             => 'Open Web',
+        'openweb'              => 'OpenWeb',
+
+        // Operating Systems
+        'android'    => 'Android',
+        'symbian'    => 'Symbian',
+        'SymbianOS'  => 'SymbianOS',
+        'elaine'     => 'Palm',
+        'series60'   => 'Symbian S60',
+        'windows ce' => 'Windows CE',
+
+        // Browsers
+        'obigo'         => 'Obigo',
+        'netfront'      => 'Netfront Browser',
+        'openwave'      => 'Openwave Browser',
+        'mobilexplorer' => 'Mobile Explorer',
+        'operamini'     => 'Opera Mini',
+        'opera mini'    => 'Opera Mini',
+        'opera mobi'    => 'Opera Mobile',
+        'fennec'        => 'Firefox Mobile',
+
+        // Other
+        'digital paths' => 'Digital Paths',
+        'avantgo'       => 'AvantGo',
+        'xiino'         => 'Xiino',
+        'novarra'       => 'Novarra Transcoder',
+        'vodafone'      => 'Vodafone',
+        'docomo'        => 'NTT DoCoMo',
+        'o2'            => 'O2',
+
+        // Fallback
+        'mobile'     => 'Generic Mobile',
+        'wireless'   => 'Generic Mobile',
+        'j2me'       => 'Generic Mobile',
+        'midp'       => 'Generic Mobile',
+        'cldc'       => 'Generic Mobile',
+        'up.link'    => 'Generic Mobile',
+        'up.browser' => 'Generic Mobile',
+        'smartphone' => 'Generic Mobile',
+        'cellphone'  => 'Generic Mobile',
+    ];
+
+    /**
+     * -------------------------------------------------------------------
+     * Robots
+     * -------------------------------------------------------------------
+     *
+     * There are hundred of bots but these are the most common.
+     *
+     * @var array<string, string>
+     */
+    public array $robots = [
+        'googlebot'            => 'Googlebot',
+        'msnbot'               => 'MSNBot',
+        'baiduspider'          => 'Baiduspider',
+        'bingbot'              => 'Bing',
+        'slurp'                => 'Inktomi Slurp',
+        'yahoo'                => 'Yahoo',
+        'ask jeeves'           => 'Ask Jeeves',
+        'fastcrawler'          => 'FastCrawler',
+        'infoseek'             => 'InfoSeek Robot 1.0',
+        'lycos'                => 'Lycos',
+        'yandex'               => 'YandexBot',
+        'mediapartners-google' => 'MediaPartners Google',
+        'CRAZYWEBCRAWLER'      => 'Crazy Webcrawler',
+        'adsbot-google'        => 'AdsBot Google',
+        'feedfetcher-google'   => 'Feedfetcher Google',
+        'curious george'       => 'Curious George',
+        'ia_archiver'          => 'Alexa Crawler',
+        'MJ12bot'              => 'Majestic-12',
+        'Uptimebot'            => 'Uptimebot',
+    ];
+}

+ 44 - 0
backend/app/Config/Validation.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+use CodeIgniter\Validation\StrictRules\CreditCardRules;
+use CodeIgniter\Validation\StrictRules\FileRules;
+use CodeIgniter\Validation\StrictRules\FormatRules;
+use CodeIgniter\Validation\StrictRules\Rules;
+
+class Validation extends BaseConfig
+{
+    // --------------------------------------------------------------------
+    // Setup
+    // --------------------------------------------------------------------
+
+    /**
+     * Stores the classes that contain the
+     * rules that are available.
+     *
+     * @var list<string>
+     */
+    public array $ruleSets = [
+        Rules::class,
+        FormatRules::class,
+        FileRules::class,
+        CreditCardRules::class,
+    ];
+
+    /**
+     * Specifies the views that are used to display the
+     * errors.
+     *
+     * @var array<string, string>
+     */
+    public array $templates = [
+        'list'   => 'CodeIgniter\Validation\Views\list',
+        'single' => 'CodeIgniter\Validation\Views\single',
+    ];
+
+    // --------------------------------------------------------------------
+    // Rules
+    // --------------------------------------------------------------------
+}

+ 62 - 0
backend/app/Config/View.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\View as BaseView;
+use CodeIgniter\View\ViewDecoratorInterface;
+
+/**
+ * @phpstan-type parser_callable (callable(mixed): mixed)
+ * @phpstan-type parser_callable_string (callable(mixed): mixed)&string
+ */
+class View extends BaseView
+{
+    /**
+     * When false, the view method will clear the data between each
+     * call. This keeps your data safe and ensures there is no accidental
+     * leaking between calls, so you would need to explicitly pass the data
+     * to each view. You might prefer to have the data stick around between
+     * calls so that it is available to all views. If that is the case,
+     * set $saveData to true.
+     *
+     * @var bool
+     */
+    public $saveData = true;
+
+    /**
+     * Parser Filters map a filter name with any PHP callable. When the
+     * Parser prepares a variable for display, it will chain it
+     * through the filters in the order defined, inserting any parameters.
+     * To prevent potential abuse, all filters MUST be defined here
+     * in order for them to be available for use within the Parser.
+     *
+     * Examples:
+     *  { title|esc(js) }
+     *  { created_on|date(Y-m-d)|esc(attr) }
+     *
+     * @var         array<string, string>
+     * @phpstan-var array<string, parser_callable_string>
+     */
+    public $filters = [];
+
+    /**
+     * Parser Plugins provide a way to extend the functionality provided
+     * by the core Parser by creating aliases that will be replaced with
+     * any callable. Can be single or tag pair.
+     *
+     * @var         array<string, callable|list<string>|string>
+     * @phpstan-var array<string, list<parser_callable_string>|parser_callable_string|parser_callable>
+     */
+    public $plugins = [];
+
+    /**
+     * View Decorators are class methods that will be run in sequence to
+     * have a chance to alter the generated output just prior to caching
+     * the results.
+     *
+     * All classes must implement CodeIgniter\View\ViewDecoratorInterface
+     *
+     * @var list<class-string<ViewDecoratorInterface>>
+     */
+    public array $decorators = [];
+}

+ 80 - 0
backend/app/Controllers/Alimtalk.php

@@ -0,0 +1,80 @@
+<?php
+
+  namespace App\Controllers;
+
+  use CodeIgniter\Controller;
+
+  class Alimtalk extends Controller
+  {
+    public function send()
+    {
+      $receivers = [
+        [
+          'phone'   => '01011112222',
+          'name'    => '홍길동',
+
+          // 아래 부분만 알림톡 템플릿 변수와 1:1 매칭!
+          '이벤트 명'    => '여름휴가 응모 이벤트',
+          '상품명'      => '스타벅스 기프티콘',
+          '당첨일'      => '2024-06-12',
+          '고객센터번호' => '1544-1234'
+        ]
+      ];
+
+
+      // 기본 파라미터
+      $variables = [
+        'apikey'    => 'npb9ryvqh9439js2sfbhuji4lwfmdgqu',
+        'userid'    => 'interscope',
+        'senderkey' => '846700656ab2c0b136e4433751d75018ea48b8ec',
+        'tpl_code'  => 'UA_1201',
+        'sender'    => '010-8384-5309',
+        //'senddate'  => date("YmdHis", strtotime("+10 minutes")),
+      ];
+
+      // 수신자별 파라미터 추가
+      foreach ($receivers as $idx => $info) {
+        $i = $idx + 1;
+        $variables["receiver_{$i}"] = $info['phone'];
+        $variables["recvname_{$i}"] = $info['name'];
+        // 템플릿 변수값 매칭
+        $variables["subject_{$i}"]  = ''; // 필요 없으면 공란
+        $variables["message_{$i}"]  =
+          "안녕하세요 고객님!\n"
+          . "{$info['이벤트 명']}\n"
+          . "{$info['상품명']} 당첨을 축하드립니다.\n"
+          . "당첨일자 : {$info['당첨일']}\n\n"
+          . "*이 메시지는 고객님이 참여한 이벤트 당첨으로 발송된 안내 메시지입니다.\n\n"
+          . "문의 : 고객센터 {$info['고객센터번호']}\n"
+          . "감사합니다.";
+        // 버튼 필요시 추가
+        // $variables["button_{$i}"] = '...';
+      }
+
+
+      $apiURL = 'https://kakaoapi.aligo.in/akv10/alimtalk/send/';
+      $hostInfo = parse_url($apiURL);
+      $port = (strtolower($hostInfo['scheme']) == 'https') ? 443 : 80;
+
+      // cURL 요청
+      $ch = curl_init();
+      curl_setopt($ch, CURLOPT_PORT, $port);
+      curl_setopt($ch, CURLOPT_URL, $apiURL);
+      curl_setopt($ch, CURLOPT_POST, 1);
+      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+      curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($variables));
+      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+      $response = curl_exec($ch);
+      $error_msg = curl_error($ch);
+      curl_close($ch);
+
+      // 결과 출력 (json)
+      if ($error_msg) {
+        return $this->response->setJSON(['error' => $error_msg]);
+      }
+
+      $result = json_decode($response, true);
+      return $this->response->setJSON($result);
+    }
+  }

+ 58 - 0
backend/app/Controllers/BaseController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\Controller;
+use CodeIgniter\HTTP\CLIRequest;
+use CodeIgniter\HTTP\IncomingRequest;
+use CodeIgniter\HTTP\RequestInterface;
+use CodeIgniter\HTTP\ResponseInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class BaseController
+ *
+ * BaseController provides a convenient place for loading components
+ * and performing functions that are needed by all your controllers.
+ * Extend this class in any new controllers:
+ *     class Home extends BaseController
+ *
+ * For security be sure to declare any new methods as protected or private.
+ */
+abstract class BaseController extends Controller
+{
+    /**
+     * Instance of the main Request object.
+     *
+     * @var CLIRequest|IncomingRequest
+     */
+    protected $request;
+
+    /**
+     * An array of helpers to be loaded automatically upon
+     * class instantiation. These helpers will be available
+     * to all other controllers that extend BaseController.
+     *
+     * @var list<string>
+     */
+    protected $helpers = [];
+
+    /**
+     * Be sure to declare properties for any property fetch you initialized.
+     * The creation of dynamic property is deprecated in PHP 8.2.
+     */
+    // protected $session;
+
+    /**
+     * @return void
+     */
+    public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
+    {
+        // Do Not Edit This Line
+        parent::initController($request, $response, $logger);
+
+        // Preload any models, libraries, etc, here.
+
+        // E.g.: $this->session = service('session');
+    }
+}

+ 113 - 0
backend/app/Controllers/Deli.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\RESTful\ResourceController;
+
+class Deli extends ResourceController
+{
+    //구매자 리스트
+    public function delilist()
+    {
+        $db = \Config\Database::connect();
+        $request = $this->request->getJSON(true);
+        $itemSeq = isset($request['item_seq']) ? $request['item_seq'] : null;
+        $infSeq = isset($request['inf_seq']) ? $request['inf_seq'] : null;
+        // 쿼리 빌더
+        // 인플루언서 시퀀스가 들어올 경우 해당 정보만 노출, 들어오지 않으면 전체 노출
+        if($infSeq){
+            $builder = $db->table('ITEM_ORDER_LIST')->where('ITEM_SEQ', $itemSeq )->where('INF_SEQ', $infSeq);
+        } else {
+            $builder = $db->table('ITEM_ORDER_LIST')->where('ITEM_SEQ', $itemSeq );
+        }
+
+        // 주문일 기준으로 정렬
+        $builder->orderBy('ORDER_DATE', 'DESC');
+        $lists = $builder->get()->getResultArray();
+
+        return $this->respond($lists, 200);
+    }
+
+    //구매자 등록
+    public function deliRegister()
+    {
+        $db = \Config\Database::connect();
+        $request = $this->request->getJSON(true);
+
+        $itemSeq = isset($request['item_seq']) ? $request['item_seq'] : null;
+        $infSeq = isset($request['inf_seq']) ? $request['inf_seq'] : null;
+        $deliveryList = $request['deliveryList'] ?? [];
+
+        $db->table('ITEM_ORDER_LIST')
+            ->where('ITEM_SEQ', $itemSeq)
+            ->where('INF_SEQ', $infSeq)
+            ->delete();
+
+        foreach ($deliveryList as $index => $delivery) {
+            $requiredFields = ['buyerName', 'address', 'phone', 'qty', 'total', 'orderDate'];
+            foreach ($requiredFields as $field) {
+                if (!isset($delivery[$field]) || $delivery[$field] === '') {
+                    return $this->fail("deliveryList[$index] 항목의 '{$field}' 값이 누락되었습니다.", 400);
+                }
+            }
+
+            // 삽입
+            $data = [
+                'ITEM_SEQ' => $itemSeq,
+                'INF_SEQ' => $infSeq,
+                'BUYER_NAME' => $delivery['buyerName'],
+                'ADDRESS' => $delivery['address'],
+                'PHONE' => $delivery['phone'],
+                'EMAIL' => $delivery['email'],
+                'QTY' => $delivery['qty'],
+                'TOTAL' => $delivery['total'],
+                'DELI_COMP' => $delivery['deliComp'] ?? null,
+                'DELI_NUMB' => $delivery['deliNumb'] ?? null,
+                'ORDER_DATE' => date('Y-m-d H:i:s', strtotime($delivery['orderDate'])),
+                'REG_DATE' => date('Y-m-d'),
+            ];
+
+            $db->table('ITEM_ORDER_LIST')->insert($data);
+        }
+
+        return $this->respond(['message' => '배송 데이터가 성공적으로 저장되었습니다.'], 200);
+    }
+
+    //아이템 상세
+    public function itemDetail($seq)
+    {
+        // DB 객체 얻기
+        $db = \Config\Database::connect();
+
+        $builder = $db->table('ITEM_LIST');
+        $item = $builder->where('seq', $seq)->get()->getRowArray();
+
+        if($item){
+            return $this->respond($item, 200);
+        } else {
+            return $this->respond([
+                'status' => 'fail',
+                'message' => '유효하지 않은 seq입니다.'
+            ], 404);
+        }
+    }
+
+    //아이템 삭제
+    public function itemDelete($seq)
+    {
+        $db = \Config\Database::connect();
+        $db->transBegin();
+
+        //아이템 삭제
+        $deleted = $db->table('ITEM_LIST')
+            ->where('SEQ', $seq)
+            ->update(['DEL_YN' => 'Y']);
+
+        if ($db->transStatus() === false || !$deleted) {
+            $db->transRollback();
+            return $this->respond(['status' => 'fail', 'message' => '이벤트 삭제 중 오류가 발생했습니다.']);
+        }
+        $db->transCommit();
+        return $this->respond(['status' => 'success', 'message' => '이벤트가 삭제되었습니다.'], 200);
+    }
+}

+ 19 - 0
backend/app/Controllers/Home.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Controllers;
+
+class Home extends BaseController
+{
+    public function index(): string
+    {
+      // DB에 연결
+      $db = \Config\Database::connect();
+
+      // USER_INFO 테이블에서 모든 데이터 가져오기
+      $builder = $db->table('USER_INFO');
+      $users = $builder->get()->getResultArray();
+
+      return view('welcome_message', ['users' => $users]);
+    }
+}
+

+ 54 - 46
backend/app/Controllers/InfluencerController.php

@@ -38,10 +38,12 @@ class InfluencerController extends ResourceController
             $request = $this->request->getJSON();
             
             $influencerSeq = $request->influencerSeq ?? null;
-            $name = $request->name ?? '';
+            $keyword = $request->keyword ?? '';
             $category = $request->category ?? '';
-            $page = $request->page ?? 1;
-            $size = $request->size ?? 10;
+            $region = $request->region ?? '';
+            $sortBy = $request->sortBy ?? 'latest';
+            $page = (int)($request->page ?? 1);
+            $size = (int)($request->size ?? 12);
 
             if (!$influencerSeq) {
                 return $this->response->setStatusCode(400)->setJSON([
@@ -50,18 +52,27 @@ class InfluencerController extends ResourceController
                 ]);
             }
 
+            // 필터 배열 구성 (VendorModel에 맞는 형식)
+            $filters = [
+                'keyword' => $keyword,
+                'category' => $category,
+                'region' => $region,
+                'sortBy' => $sortBy
+            ];
+
             // 벤더사 목록 조회
-            $vendors = $this->vendorModel->searchVendors($name, $category, $page, $size);
+            $vendors = $this->vendorModel->searchVendors($filters, $page, $size);
+            $totalCount = $this->vendorModel->countSearchResults($filters);
 
             // 각 벤더사와의 파트너십 상태 확인
-            foreach ($vendors['data'] as &$vendor) {
+            foreach ($vendors as &$vendor) {
                 $partnership = $this->vendorInfluencerModel
                     ->select('VENDOR_INFLUENCER_MAPPING.SEQ, VENDOR_INFLUENCER_MAPPING.REQUEST_TYPE, 
                              VENDOR_INFLUENCER_STATUS_HISTORY.STATUS as CURRENT_STATUS,
                              VENDOR_INFLUENCER_STATUS_HISTORY.STATUS_MESSAGE,
                              VENDOR_INFLUENCER_STATUS_HISTORY.CHANGED_DATE')
                     ->join('VENDOR_INFLUENCER_STATUS_HISTORY', 
-                           'VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ = VENDOR_INFLUENCER_MAPPING.SEQ AND VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT = "Y"')
+                           'VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ = VENDOR_INFLUENCER_MAPPING.SEQ AND VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT = "Y"', 'left')
                     ->where('VENDOR_SEQ', $vendor['SEQ'])
                     ->where('INFLUENCER_SEQ', $influencerSeq)
                     ->where('VENDOR_INFLUENCER_MAPPING.IS_ACT', 'Y')
@@ -70,25 +81,39 @@ class InfluencerController extends ResourceController
 
                 if ($partnership) {
                     $vendor['PARTNERSHIP_STATUS'] = $partnership['CURRENT_STATUS'];
-                    $vendor['PARTNERSHIP_MESSAGE'] = $partnership['STATUS_MESSAGE'];
-                    $vendor['PARTNERSHIP_DATE'] = $partnership['CHANGED_DATE'];
-                    $vendor['MAPPING_SEQ'] = $partnership['SEQ'];
+                    $vendor['PARTNERSHIP_SEQ'] = $partnership['SEQ'];
+                    $vendor['REQUEST_TYPE'] = $partnership['REQUEST_TYPE'];
+                    $vendor['STATUS_MESSAGE'] = $partnership['STATUS_MESSAGE'];
+                    $vendor['STATUS_DATE'] = $partnership['CHANGED_DATE'];
                 } else {
                     $vendor['PARTNERSHIP_STATUS'] = null;
-                    $vendor['PARTNERSHIP_MESSAGE'] = null;
-                    $vendor['PARTNERSHIP_DATE'] = null;
-                    $vendor['MAPPING_SEQ'] = null;
+                    $vendor['PARTNERSHIP_SEQ'] = null;
+                    $vendor['REQUEST_TYPE'] = null;
+                    $vendor['STATUS_MESSAGE'] = null;
+                    $vendor['STATUS_DATE'] = null;
                 }
             }
 
+            // 페이지네이션 정보 계산
+            $totalPages = ceil($totalCount / $size);
+
             return $this->response->setJSON([
                 'success' => true,
-                'data' => $vendors['data'],
-                'pagination' => $vendors['pagination']
+                'data' => [
+                    'items' => $vendors,
+                    'pagination' => [
+                        'currentPage' => $page,
+                        'totalPages' => $totalPages,
+                        'totalCount' => $totalCount,
+                        'pageSize' => $size
+                    ]
+                ]
             ]);
 
         } catch (\Exception $e) {
             log_message('error', '벤더사 검색 오류: ' . $e->getMessage());
+            log_message('error', '스택 트레이스: ' . $e->getTraceAsString());
+            
             return $this->response->setStatusCode(500)->setJSON([
                 'success' => false,
                 'message' => '벤더사 검색 중 오류가 발생했습니다.',
@@ -98,7 +123,7 @@ class InfluencerController extends ResourceController
     }
 
     /**
-     * 승인 요청 생성 (히스토리 테이블 기반)
+     * 승인 요청 생성
      */
     public function createApprovalRequest()
     {
@@ -107,7 +132,6 @@ class InfluencerController extends ResourceController
             
             $vendorSeq = $request->vendorSeq ?? null;
             $influencerSeq = $request->influencerSeq ?? null;
-            $requestType = $request->requestType ?? 'INFLUENCER_REQUEST';
             $requestMessage = $request->requestMessage ?? '';
             $requestedBy = $request->requestedBy ?? null;
             $commissionRate = $request->commissionRate ?? null;
@@ -120,51 +144,35 @@ class InfluencerController extends ResourceController
                 ]);
             }
 
-            // 중복 요청 확인 (PENDING 상태)
-            $existingRequest = $this->vendorInfluencerModel->checkExistingPendingRequest($vendorSeq, $influencerSeq);
-            
-            if ($existingRequest) {
-                return $this->response->setStatusCode(409)->setJSON([
-                    'success' => false,
-                    'message' => '이미 처리 중인 요청이 있습니다.'
-                ]);
-            }
-
-            // 요청 생성 (STATUS 컬럼 없이)
+            // 데이터 구성
             $data = [
                 'VENDOR_SEQ' => $vendorSeq,
                 'INFLUENCER_SEQ' => $influencerSeq,
-                'REQUEST_TYPE' => $requestType,
                 'REQUEST_MESSAGE' => $requestMessage,
                 'REQUESTED_BY' => $requestedBy,
                 'COMMISSION_RATE' => $commissionRate,
                 'SPECIAL_CONDITIONS' => $specialConditions
             ];
 
-            $mappingSeq = $this->vendorInfluencerModel->insert($data);
-            // afterInsert 콜백에서 자동으로 PENDING 상태 히스토리 생성됨
+            // InfluencerPartnershipModel을 통해 요청 생성
+            $mappingSeq = $this->influencerPartnershipModel->createApprovalRequest($data);
 
-            if ($mappingSeq) {
-                return $this->response->setStatusCode(201)->setJSON([
-                    'success' => true,
-                    'message' => '승인 요청이 성공적으로 생성되었습니다.',
-                    'data' => [
-                        'mappingSeq' => $mappingSeq,
-                        'status' => 'PENDING'
-                    ]
-                ]);
-            } else {
-                return $this->response->setStatusCode(500)->setJSON([
-                    'success' => false,
-                    'message' => '승인 요청 생성에 실패했습니다.'
-                ]);
-            }
+            return $this->response->setStatusCode(201)->setJSON([
+                'success' => true,
+                'message' => '승인 요청이 성공적으로 생성되었습니다.',
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'status' => 'PENDING'
+                ]
+            ]);
 
         } catch (\Exception $e) {
             log_message('error', '승인 요청 생성 오류: ' . $e->getMessage());
+            log_message('error', '스택 트레이스: ' . $e->getTraceAsString());
+            
             return $this->response->setStatusCode(500)->setJSON([
                 'success' => false,
-                'message' => '승인 요청 생성 중 오류가 발생했습니다.',
+                'message' => '승인 요청 생성에 실패했습니다.',
                 'error' => $e->getMessage()
             ]);
         }

+ 368 - 0
backend/app/Controllers/InfluencerControllerV2.php

@@ -0,0 +1,368 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\RESTful\ResourceController;
+use App\Models\VendorInfluencerMappingModel;
+use App\Models\VendorInfluencerStatusHistoryModel;
+use App\Models\VendorModel;
+use App\Models\UserModel;
+
+class InfluencerControllerV2 extends ResourceController
+{
+    protected $modelName = 'App\Models\VendorInfluencerMappingModel';
+    protected $format = 'json';
+    
+    protected $vendorInfluencerModel;
+    protected $statusHistoryModel;
+    protected $vendorModel;
+    protected $userModel;
+
+    public function __construct()
+    {
+        $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+        $this->statusHistoryModel = new VendorInfluencerStatusHistoryModel();
+        $this->vendorModel = new VendorModel();
+        $this->userModel = new UserModel();
+    }
+
+    /**
+     * 벤더사 검색 (상태 정보 포함)
+     */
+    public function searchVendors()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $influencerSeq = $request->influencerSeq ?? null;
+            $name = $request->name ?? '';
+            $category = $request->category ?? '';
+            $page = $request->page ?? 1;
+            $size = $request->size ?? 10;
+
+            if (!$influencerSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '인플루언서 SEQ는 필수입니다.'
+                ]);
+            }
+
+            // 벤더사 목록 조회
+            $vendors = $this->vendorModel->searchVendors($name, $category, $page, $size);
+
+            // 각 벤더사와의 파트너십 상태 확인
+            foreach ($vendors['data'] as &$vendor) {
+                $partnership = $this->vendorInfluencerModel
+                    ->select('VENDOR_INFLUENCER_MAPPING.SEQ, VENDOR_INFLUENCER_MAPPING.REQUEST_TYPE, 
+                             VENDOR_INFLUENCER_STATUS_HISTORY.STATUS as CURRENT_STATUS,
+                             VENDOR_INFLUENCER_STATUS_HISTORY.STATUS_MESSAGE,
+                             VENDOR_INFLUENCER_STATUS_HISTORY.CHANGED_DATE')
+                    ->join('VENDOR_INFLUENCER_STATUS_HISTORY', 
+                           'VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ = VENDOR_INFLUENCER_MAPPING.SEQ AND VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT = "Y"')
+                    ->where('VENDOR_SEQ', $vendor['SEQ'])
+                    ->where('INFLUENCER_SEQ', $influencerSeq)
+                    ->where('VENDOR_INFLUENCER_MAPPING.IS_ACT', 'Y')
+                    ->orderBy('VENDOR_INFLUENCER_MAPPING.REG_DATE', 'DESC')
+                    ->first();
+
+                if ($partnership) {
+                    $vendor['PARTNERSHIP_STATUS'] = $partnership['CURRENT_STATUS'];
+                    $vendor['PARTNERSHIP_MESSAGE'] = $partnership['STATUS_MESSAGE'];
+                    $vendor['PARTNERSHIP_DATE'] = $partnership['CHANGED_DATE'];
+                    $vendor['MAPPING_SEQ'] = $partnership['SEQ'];
+                } else {
+                    $vendor['PARTNERSHIP_STATUS'] = null;
+                    $vendor['PARTNERSHIP_MESSAGE'] = null;
+                    $vendor['PARTNERSHIP_DATE'] = null;
+                    $vendor['MAPPING_SEQ'] = null;
+                }
+            }
+
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => $vendors['data'],
+                'pagination' => $vendors['pagination']
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '벤더사 검색 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '벤더사 검색 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 승인 요청 생성 (히스토리 테이블 기반)
+     */
+    public function createApprovalRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $vendorSeq = $request->vendorSeq ?? null;
+            $influencerSeq = $request->influencerSeq ?? null;
+            $requestType = $request->requestType ?? 'INFLUENCER_REQUEST';
+            $requestMessage = $request->requestMessage ?? '';
+            $requestedBy = $request->requestedBy ?? null;
+            $commissionRate = $request->commissionRate ?? null;
+            $specialConditions = $request->specialConditions ?? '';
+
+            if (!$vendorSeq || !$influencerSeq || !$requestedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+
+            // 중복 요청 확인 (PENDING 상태)
+            $existingRequest = $this->vendorInfluencerModel->checkExistingPendingRequest($vendorSeq, $influencerSeq);
+            
+            if ($existingRequest) {
+                return $this->response->setStatusCode(409)->setJSON([
+                    'success' => false,
+                    'message' => '이미 처리 중인 요청이 있습니다.'
+                ]);
+            }
+
+            // 요청 생성 (STATUS 컬럼 없이)
+            $data = [
+                'VENDOR_SEQ' => $vendorSeq,
+                'INFLUENCER_SEQ' => $influencerSeq,
+                'REQUEST_TYPE' => $requestType,
+                'REQUEST_MESSAGE' => $requestMessage,
+                'REQUESTED_BY' => $requestedBy,
+                'COMMISSION_RATE' => $commissionRate,
+                'SPECIAL_CONDITIONS' => $specialConditions
+            ];
+
+            $mappingSeq = $this->vendorInfluencerModel->insert($data);
+            // afterInsert 콜백에서 자동으로 PENDING 상태 히스토리 생성됨
+
+            if ($mappingSeq) {
+                return $this->response->setStatusCode(201)->setJSON([
+                    'success' => true,
+                    'message' => '승인 요청이 성공적으로 생성되었습니다.',
+                    'data' => [
+                        'mappingSeq' => $mappingSeq,
+                        'status' => 'PENDING'
+                    ]
+                ]);
+            } else {
+                return $this->response->setStatusCode(500)->setJSON([
+                    'success' => false,
+                    'message' => '승인 요청 생성에 실패했습니다.'
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            log_message('error', '승인 요청 생성 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '승인 요청 생성 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 재승인 요청 생성 (히스토리 테이블 기반)
+     */
+    public function createReapplyRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $vendorSeq = $request->vendorSeq ?? null;
+            $influencerSeq = $request->influencerSeq ?? null;
+            $requestMessage = $request->requestMessage ?? '';
+            $requestedBy = $request->requestedBy ?? null;
+            $commissionRate = $request->commissionRate ?? null;
+            $specialConditions = $request->specialConditions ?? '';
+
+            log_message('debug', '재승인 요청 파라미터: ' . json_encode([
+                'vendorSeq' => $vendorSeq,
+                'influencerSeq' => $influencerSeq,
+                'requestedBy' => $requestedBy
+            ]));
+
+            if (!$vendorSeq || !$influencerSeq || !$requestedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+
+            // 재승인 가능한 파트너십 확인 (TERMINATED 또는 REJECTED 상태)
+            $eligiblePartnership = $this->vendorInfluencerModel->checkReapplyEligiblePartnership($vendorSeq, $influencerSeq);
+            
+            if (!$eligiblePartnership) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '재승인을 요청할 수 있는 이전 파트너십이 없습니다.'
+                ]);
+            }
+
+            // 이미 재승인 요청 중인지 확인
+            $existingReapply = $this->vendorInfluencerModel->checkExistingPendingRequest($vendorSeq, $influencerSeq);
+            
+            if ($existingReapply) {
+                return $this->response->setStatusCode(409)->setJSON([
+                    'success' => false,
+                    'message' => '이미 재승인 요청이 진행 중입니다.'
+                ]);
+            }
+
+            // 재승인 요청 생성
+            $data = [
+                'VENDOR_SEQ' => $vendorSeq,
+                'INFLUENCER_SEQ' => $influencerSeq,
+                'REQUEST_TYPE' => 'INFLUENCER_REAPPLY',
+                'REQUEST_MESSAGE' => $requestMessage,
+                'REQUESTED_BY' => $requestedBy,
+                'COMMISSION_RATE' => $commissionRate ?: $eligiblePartnership['COMMISSION_RATE'],
+                'SPECIAL_CONDITIONS' => $specialConditions ?: $eligiblePartnership['SPECIAL_CONDITIONS'],
+                'ADD_INFO1' => 'REAPPLY',
+                'ADD_INFO2' => $eligiblePartnership['SEQ'], // 이전 파트너십 SEQ
+                'ADD_INFO3' => date('Y-m-d H:i:s') // 재신청 일시
+            ];
+
+            $mappingSeq = $this->vendorInfluencerModel->insert($data);
+            // afterInsert 콜백에서 자동으로 PENDING 상태 히스토리 생성됨
+
+            if ($mappingSeq) {
+                log_message('debug', "재승인 요청 성공 - 새 매핑 SEQ: " . $mappingSeq);
+                
+                return $this->response->setStatusCode(201)->setJSON([
+                    'success' => true,
+                    'message' => '재승인 요청이 성공적으로 생성되었습니다.',
+                    'data' => [
+                        'mappingSeq' => $mappingSeq,
+                        'status' => 'PENDING',
+                        'isReapply' => true,
+                        'previousPartnership' => $eligiblePartnership['SEQ']
+                    ]
+                ]);
+            } else {
+                log_message('error', '재승인 요청 삽입 실패');
+                return $this->response->setStatusCode(500)->setJSON([
+                    'success' => false,
+                    'message' => '재승인 요청 데이터 삽입에 실패했습니다.'
+                ]);
+            }
+
+        } catch (\Exception $e) {
+            log_message('error', '재승인 요청 처리 중 예외 발생: ' . $e->getMessage());
+            log_message('error', '재승인 요청 스택 트레이스: ' . $e->getTraceAsString());
+            
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '재승인 요청 생성 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 내 파트너십 목록 조회 (상태 히스토리 포함)
+     */
+    public function getMyPartnerships()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $influencerSeq = $request->influencerSeq ?? null;
+            $status = $request->status ?? null;
+            $page = $request->page ?? 1;
+            $size = $request->size ?? 20;
+
+            if (!$influencerSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '인플루언서 SEQ는 필수입니다.'
+                ]);
+            }
+
+            $result = $this->vendorInfluencerModel->getVendorPartnershipsByInfluencer($influencerSeq, $page, $size, $status);
+
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => $result['data'],
+                'pagination' => $result['pagination']
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '파트너십 목록 조회 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '파트너십 목록 조회 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 파트너십 해지 (히스토리 테이블 기반)
+     */
+    public function terminatePartnership()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $reason = $request->reason ?? '';
+            $terminatedBy = $request->terminatedBy ?? null;
+
+            if (!$mappingSeq || !$terminatedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+
+            // 현재 상태 확인
+            $mapping = $this->vendorInfluencerModel->getWithCurrentStatus($mappingSeq);
+            
+            if (!$mapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '해당 파트너십을 찾을 수 없습니다.'
+                ]);
+            }
+
+            if ($mapping['CURRENT_STATUS'] !== 'APPROVED') {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '승인된 파트너십만 해지할 수 있습니다.'
+                ]);
+            }
+
+            // 상태를 TERMINATED로 변경
+            $this->statusHistoryModel->changeStatus($mappingSeq, 'TERMINATED', '파트너십 해지: ' . $reason, $terminatedBy);
+
+            // 해지 날짜 업데이트
+            $this->vendorInfluencerModel->update($mappingSeq, [
+                'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s')
+            ]);
+
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => '파트너십이 해지되었습니다.',
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'status' => 'TERMINATED'
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            log_message('error', '파트너십 해지 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '파트너십 해지 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+} 

+ 322 - 0
backend/app/Controllers/Item.php

@@ -0,0 +1,322 @@
+<?php
+
+  namespace App\Controllers;
+
+  use CodeIgniter\RESTful\ResourceController;
+
+  class Item extends ResourceController
+  {
+    //이벤트 리스트
+    public function itemlist()
+    {
+        $db = \Config\Database::connect();
+
+        // POST JSON 파라미터 받기
+        $request = $this->request->getJSON(true);
+
+        $showYn = isset($request['SHOW_YN']) ? $request['SHOW_YN'] : null;
+
+        // 쿼리 빌더
+        $builder = $db->table('ITEM_LIST')->where('DEL_YN', 'N');
+
+        // 노출중, 비노출 여부 확인
+        if (!is_null($showYn) && $showYn !== '') {
+            $builder->where('SHOW_YN', $showYn);
+        }
+
+        // 업데이트 날짜 기준으로 정렬
+        $builder->orderBy('UDPDATE', 'DESC');
+        $lists = $builder->get()->getResultArray();
+
+        return $this->respond($lists, 200);
+    }
+
+    //아이템 검색
+    public function itemSearch()
+    {
+        $db = \Config\Database::connect();
+
+        // 요청 바디에서 filter와 keyword 추출 (예: {filter: "id", keyword: "admin"})
+        $request = $this->request->getJSON(true);
+        $filter = isset($request['filter']) ? $request['filter'] : null;
+        $keyword = isset($request['keyword']) ? $request['keyword'] : null;
+        $startDate = $request['startDate'] ?? null;
+        $endDate = $request['endDate'] ?? null;
+        $showYN = $request['showYN'] ?? null; 
+
+        $filterMap = [
+            'name' => 'NAME',
+        ];
+
+//        if (!isset($request['compId'])) {
+//          return $this->respond([
+//            'status' => 'fail',
+//            'message' => 'filter(compId)가 누락되었습니다.'
+//          ], 400);
+//        }
+//        $compId = $request['compId'];
+
+
+
+        // 평문 검색 (LIKE 연산 사용)
+        $builder = $db->table('ITEM_LIST');
+        if (!empty($keyword)) {
+            if (empty($filter)) {
+                // 필터를 선택 안했으면 전체 검색
+                $first = true;
+                foreach ($filterMap as $column) {
+                    if ($first) {
+                        $builder->like($column, $keyword);
+                        $first = false;
+                    } else {
+                        $builder->orLike($column, $keyword);
+                    }
+                }
+            } elseif (isset($filterMap[$filter])) {
+                // 특정 필터 검색
+                $builder->like($filterMap[$filter], $keyword);
+            }
+        }
+
+        // 인플루언서의 경우는 비노출 항목 가림
+        if (!empty($showYN)) {
+            $builder->where('SHOW_YN', $showYN);
+        }
+        // 정렬: UPDATE 기준 최신순
+        $builder->where('UDPDATE >=', $startDate . ' 00:00:00');
+        $builder->where('UDPDATE <=', $endDate . ' 23:59:59');
+        $builder->where('DEL_YN =', 'N');
+        $builder->orderBy('UDPDATE', 'DESC');
+
+        // 조회
+        $lists = $builder->get()->getResultArray();
+
+        return $this->respond($lists, 200);
+    }
+
+    //아이템 등록
+    public function itemRegister()
+    {
+      $db = \Config\Database::connect();
+      $request = \Config\Services::request();
+      $regdate = date('Y-m-d H:i:s');
+      $thumb = $request->getFile('thumb_file');
+      $zip = $request->getFile('zip_file');
+      $zipOrigin = $zip ? $zip->getClientName() : null;
+
+      // 기본 유효성 검사
+      if (
+          !$request->getPost('name') ||
+          !$request->getPost('price1') ||
+          !$request->getPost('price2') ||
+          !$request->getPost('deli_fee') ||
+          !$request->getPost('sub_title') ||
+          !$request->getPost('detail') ||
+          !$request->getPost('company_number')
+      ) {
+          return $this->respond([
+              'status' => 'fail',
+              'message' => '필수 값이 누락됐습니다.'
+          ], 400);
+      }
+
+      $db->transBegin(); // 트랜잭션 시작
+
+      try {
+        // 썸네일 파일 처리
+        $thumbFileName = null;
+        if ($thumb && $thumb->isValid() && !$thumb->hasMoved()) {
+          $thumbFileName = $thumb->getRandomName(); // 랜덤파일명 생성
+          $thumb->move(WRITEPATH . 'uploads/item/thumb/', $thumbFileName); // 저장
+        }
+        // 상세 zip 파일 처리
+        $zipFileName = null;
+        if ($zip && $zip->isValid() && !$zip->hasMoved()) {
+          $zipFileName = $zip->getRandomName();
+          $zip->move(WRITEPATH . 'uploads/item/detail/', $zipFileName);
+        }
+        // 1. ITEM_LIST에 아이템 정보 등록
+        $itemData = [
+            'NAME' => $request->getPost('name'),
+            'PRICE1' => $request->getPost('price1'),
+            'PRICE2' => $request->getPost('price2'),
+            'DELI_FEE' => $request->getPost('deli_fee'),
+            'SUB_TITLE' => $request->getPost('sub_title'),
+            'DETAIL' => $request->getPost('detail'),
+            'STATUS' => $request->getPost('status'),
+            'SHOW_YN' => $request->getPost('show_yn'),
+            'ADD_INFO' => $request->getPost('add_info') ?? 0,
+            'COMPANY_NUMBER' => "1",
+            'REGDATE' => $regdate,
+            'UDPDATE' => $regdate,
+            'THUMB_FILE' => $thumbFileName, // 파일명 저장
+            'ZIP_FILE' => $zipFileName, // 파일명 저장
+            'ZIP_FILE_ORIGIN' => $zipOrigin, // 원본 파일명 저장
+        ];
+
+        $insertResult = $db->table('ITEM_LIST')->insert($itemData);
+          if (!$insertResult) {
+              $error = $db->error();
+              return $this->respond([
+                  'status' => 'fail',
+                  'message' => 'Insert 실패: ' . $error['message']
+              ], 500);
+          }
+        $itemSeq = $db->insertID(); // 생성된 이벤트 SEQ값
+
+
+
+        $db->transCommit();
+        return $this->respond([
+          'status' => 'success',
+          'item_seq' => $itemSeq
+        ], 201);
+      } catch (\Exception $e) {
+          $db->transRollback();
+          return $this->respond([
+              'status' => 'fail',
+              'message' => 'DB 오류: ' . $e->getMessage()
+          ], 500);
+      }
+    }
+
+    //아이템 상세
+    public function itemDetail($seq)
+    {
+      // DB 객체 얻기
+      $db = \Config\Database::connect();
+
+      $builder = $db->table('ITEM_LIST');
+      $item = $builder->where('seq', $seq)->get()->getRowArray();
+
+      if($item){
+          return $this->respond($item, 200);
+      } else {
+          return $this->respond([
+              'status' => 'fail',
+              'message' => '유효하지 않은 seq입니다.'
+          ], 404);
+      }
+    }
+
+    //상세 다운로드
+    public function file($fileName)
+      {
+          helper('filesystem');
+
+          $path = WRITEPATH . 'uploads/item/detail/' . $fileName;
+
+          if (!file_exists($path)) {
+              return $this->failNotFound('파일을 찾을 수 없습니다.');
+          }
+
+          return $this->response
+              ->download($path, null)
+              ->setFileName($fileName);
+      }
+
+    //아이템 수정
+    public function ItemUpdate($seq)
+    {
+        $db = \Config\Database::connect();
+        $request = $this->request;
+        $upddate = date('Y-m-d H:i:s');
+        $thumb = $request->getFile('thumb_file');
+        $zip = $request->getFile('zip_file');
+
+        // 기본 유효성 검사
+        if (
+            !$request->getPost('name') ||
+            !$request->getPost('price1') ||
+            !$request->getPost('price2') ||
+            !$request->getPost('deli_fee') ||
+            !$request->getPost('sub_title') ||
+            !$request->getPost('detail') ||
+            !$request->getPost('company_number')
+        ) {
+            return $this->respond([
+                'status' => 'fail',
+                'message' => '필수 값이 누락됐습니다. '
+            ], 400);
+        }
+
+        $existingItem = $db->table('ITEM_LIST')->where('SEQ', $seq)->get()->getRow();
+
+        if (!$existingItem) {
+            return $this->respond([
+                'status' => 'fail',
+                'message' => '해당 아이템이 존재하지 않습니다.'
+            ], 404);
+        }
+
+        // 파일명 유지 혹은 새 파일 저장
+        $thumbFileName = $existingItem->THUMB_FILE;
+        if ($thumb && $thumb->isValid() && !$thumb->hasMoved()) {
+            $thumbFileName = $thumb->getRandomName();
+            $thumb->move(WRITEPATH . 'uploads/item/thumb/', $thumbFileName);
+        }
+
+        $zipFileName = $existingItem->ZIP_FILE;
+        $zipOrigin = $existingItem->ZIP_FILE_ORIGIN;
+        if ($zip && $zip->isValid() && !$zip->hasMoved()) {
+            $zipFileName = $zip->getRandomName();
+            $zipOrigin = $zip->getClientName();
+            $zip->move(WRITEPATH . 'uploads/item/detail/', $zipFileName);
+        }
+
+
+        // 업데이트 데이터 준비
+        $itemData = [
+            'NAME' => $request->getPost('name'),
+            'PRICE1' => $request->getPost('price1'),
+            'PRICE2' => $request->getPost('price2'),
+            'DELI_FEE' => $request->getPost('deli_fee'),
+            'SUB_TITLE' => $request->getPost('sub_title'),
+            'DETAIL' => $request->getPost('detail'),
+            'STATUS' => $request->getPost('status'),
+            'SHOW_YN' => $request->getPost('show_yn'),
+            'ADD_INFO' => $request->getPost('add_info') ?? 0,
+            'COMPANY_NUMBER' => "1",
+            'UDPDATE' => $upddate,
+            'THUMB_FILE' => $thumbFileName,
+            'ZIP_FILE' => $zipFileName,
+            'ZIP_FILE_ORIGIN' => $zipOrigin,
+        ];
+
+        $db->transBegin();
+
+        try {
+            $db->table('ITEM_LIST')->where('SEQ', $seq)->update($itemData);
+            $db->transCommit();
+            return $this->respond([
+                'status' => 'success'
+            ], 200);
+        } catch (\Exception $e) {
+            $db->transRollback();
+            return $this->respond([
+                'status' => 'fail',
+                'message' => 'DB 오류: ' . $e->getMessage()
+            ], 500);
+        }
+
+    }
+
+    //아이템 삭제
+    public function itemDelete($seq)
+    {
+      $db = \Config\Database::connect();
+      $db->transBegin();
+
+      //아이템 삭제
+      $deleted = $db->table('ITEM_LIST')
+            ->where('SEQ', $seq)
+            ->update(['DEL_YN' => 'Y']);
+
+      if ($db->transStatus() === false || !$deleted) {
+          $db->transRollback();
+          return $this->respond(['status' => 'fail', 'message' => '이벤트 삭제 중 오류가 발생했습니다.']);
+      }
+      $db->transCommit();
+      return $this->respond(['status' => 'success', 'message' => '이벤트가 삭제되었습니다.'], 200);
+    }
+  }

+ 303 - 0
backend/app/Controllers/Mng.php

@@ -0,0 +1,303 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\RESTful\ResourceController;
+
+class Mng extends ResourceController
+{
+    //관리자 리스트
+    public function mnglist()
+    {
+        // DB 객체 얻기
+        $db = \Config\Database::connect();
+
+        $request = $this->request->getJSON(true);
+        if (!isset($request['compId'])) {
+          return $this->respond([
+            'status' => 'fail',
+            'message' => 'filter(compId)가 누락되었습니다.'
+          ], 400);
+        }
+
+        // ADM_LIST 테이블 모든 레코드 가져오기
+        $status = isset($request['status']) ? $request['status'] : null;
+        $compId = $request['compId'];
+        $builder = $db->table('ADM_LIST');
+
+        if ($compId !== '0-000000') {
+          $builder->where('COMP_ID', $compId);
+        }
+
+        if ($status === '-1') {
+          // 삭제된 관리자만 조회
+          $builder->where('status', '-1');
+        } else {
+          // 디폴트 :: 사용중(사용중,정지)인 관리자만 조회
+          $builder->whereIn('status', ['0', '1']);
+        }
+
+        $lists = $builder->get()->getResultArray();
+
+        // PASSWORD 필드 제거
+        $filtered = array_map(function($row) {
+            unset($row['PASSWORD']);
+            return $row;
+        }, $lists);
+
+        // 역순으로 정렬
+        $filtered = array_reverse($filtered);
+
+        // JSON 응답
+        return $this->respond($filtered, 200);
+    }
+  public function mngSearch()
+  {
+    $db = \Config\Database::connect();
+
+    // 요청 바디에서 filter와 keyword 추출 (예: {filter: "id", keyword: "admin"})
+    $request = $this->request->getJSON(true);
+
+    // 필수값 체크
+    if (
+      !isset($request['compId']) ||
+      !isset($request['filter']) ||
+      !isset($request['keyword']) ||
+      !in_array($request['filter'], ['id', 'name', 'status'])
+    ) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => 'filter(id, name, status)와 keyword가 필요합니다.'
+      ], 400);
+    }
+
+    $filterMap = [
+      'id' => 'ID',
+      'name' => 'NAME',
+      'status' => 'STATUS'
+    ];
+    $filterColumn = $filterMap[$request['filter']];
+    $keyword = $request['keyword'];
+    $status = isset($request['status']) ? $request['status'] : null;
+    if (!isset($request['compId'])) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => 'filter(compId)가 누락되었습니다.'
+      ], 400);
+    }
+    $compId = $request['compId'];
+
+    // 평문 검색 (LIKE 연산 사용)
+    $builder = $db->table('ADM_LIST');
+    if ($compId !== '0-000000') {
+      $builder->where('COMP_ID', $compId);
+    }
+
+    // 사용중 관리자 리스트 검색, 삭제 관리자 리스트 검색 분리
+    if ($status === '-1') {
+      $builder->where('STATUS', '-1');
+    } else {
+      $builder->whereIn('STATUS', ['0', '1']);
+    }
+    $builder->like($filterColumn, $keyword);
+
+    $lists = $builder->get()->getResultArray();
+
+    // PASSWORD 제거
+    $filtered = array_map(function($row) {
+      unset($row['PASSWORD']);
+      return $row;
+    }, $lists);
+
+    // 최신순(역순) 정렬 (ID 기준 또는 원하는 기준으로 변경 가능)
+    $filtered = array_reverse($filtered);
+
+    return $this->respond($filtered, 200);
+  }
+
+  //관리자 등록
+  public function mngRegister()
+  {
+    // DB 객체 얻기
+    $db = \Config\Database::connect();
+    $request = $this->request->getJSON(true);
+
+    if (
+      !isset($request['id']) ||
+      !isset($request['password']) ||
+      !isset($request['name']) ||
+      !isset($request['email']) ||
+      !isset($request['phone'])
+    ) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => '필수 값이 누락됐습니다.(id, password, name, email, phone)'
+      ], 400);
+    }
+
+    // 비밀번호 해시
+    $hashedPassword = password_hash($request['password'], PASSWORD_DEFAULT);
+
+    $mngData = [
+      'id' => $request['id'],
+      'password' => $hashedPassword,
+      'name' => $request['name'],
+      'email' => $request['email'],
+      'regdate' => date('Y-m-d', strtotime($request['regdate'])),
+      'phone' => $request['phone'],
+      'status' => 0,
+      'comp_name' => $request['comp_name'],
+      'comp_id' => $request['comp_id'],
+    ];
+
+    $builder = $db->table('ADM_LIST');
+
+    if ($builder->insert($mngData)) {
+      return $this->respond(['message' => '관리자 등록 성공'], 201);
+    } else {
+      return $this->respond(['error' => '등록 실패'], 500);
+    }
+  }
+  //아이디 중복체크
+  public function mngIDChk(){
+    $db = \Config\Database::connect();
+    $request = $this->request->getJSON(true);
+
+    if (!isset($request['id']) || trim($request['id']) === '') {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => 'ID가 없습니다.'
+      ], 400);
+    }
+    $id = $request['id'];
+
+    // 영어 소문자와 숫자만 허용 (정규식 체크)
+    if (!preg_match('/^[a-z0-9]+$/', $id)) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => 'ID는 영어 소문자와 숫자만 사용할 수 있습니다.'
+      ], 400);
+    }
+
+    $builder = $db->table('ADM_LIST');
+    $exists = $builder->where('id', $id)->countAllResults();
+
+    if ($exists > 0) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => '이미 사용 중인 ID입니다.'
+      ], 409);
+    }
+    return $this->respond([
+      'status' => 'success',
+      'message' => '사용 가능한 ID입니다.'
+    ], 200);
+  }
+  //관리자 수정
+  public function mngUpdate(){
+    $db = \Config\Database::connect();
+    $request = $this->request->getJSON(true);
+
+    if (
+      !isset($request['id']) ||
+      !isset($request['name']) ||
+      !isset($request['phone']) ||
+      !isset($request['email']) ||
+      !isset($request['regdate']) ||
+      !isset($request['status'])
+    ) {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => '필수 값이 누락됐습니다.(id, name, phone, email, regdate, status)'
+      ], 400);
+    }
+    $id = $request['id'];
+
+    $mngData = [
+      'EMAIL' => $request['email'],
+      'REGDATE' => date('Y-m-d', strtotime($request['regdate'])),
+      'PHONE' => $request['phone'],
+      'STATUS' => $request['status'],
+      'COMP_NAME' => $request['comp_name'],
+      'COMP_ID' => $request['comp_id'],
+    ];
+
+    //비밀번호 변경시
+    if (!empty($request['password'])) {
+      $mngData['PASSWORD'] = password_hash($request['password'], PASSWORD_DEFAULT);
+    }
+
+    $updated = $db->table('ADM_LIST')->where('ID', $id)->update($mngData);
+
+    if ($updated) {
+      return $this->respond([
+        'status' => 'success',
+        'message' => '관리자 정보가 수정되었습니다.'
+      ], 200);
+    } else {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => '수정에 실패했습니다.'
+      ], 500);
+    }
+  }
+  //관리자 상세
+  public function mngDetail($id)
+  {
+    // DB 객체 얻기
+    $db = \Config\Database::connect();
+
+    $builder = $db->table('ADM_LIST');
+    $mng = $builder->where('id', $id)->get()->getRowArray();
+
+    if($mng){
+      // 보안상 패스워드는 반환 X
+      unset($mng['password']);
+
+      return $this->respond($mng, 200);
+    } else {
+      return $this->respond([
+        'status' => 'fail',
+        'message' => '해당 ID의 관리자가 존재하지 않습니다.'
+      ], 404);
+    }
+  }
+
+  //관리자 상태 변경(삭제, 복원)
+  public function mngStatusUpdate($id){
+    $db = \Config\Database::connect();
+    $mng = $db->table('ADM_LIST')->select('STATUS')->where('ID', $id)->get()->getRowArray();
+    $currentStatus = (int) $mng['STATUS'];
+    if ($currentStatus == -1 ){
+      // 복원 시 사용중 상태로 변경
+      $nextStatus = 0;
+    } else {
+      // 삭제 시 삭제 상태로 변경
+      $nextStatus = -1;
+    }
+    $updated = $db->table('ADM_LIST')
+      ->where('ID', $id)
+      ->update(['STATUS' => $nextStatus, 'REGDATE' => date('Y-m-d') ]);
+
+    if ($updated) {
+      return $this->respond(['status' => 'success', 'message' => '상태 변경 완료', 'new_status' => $nextStatus], 200);
+    } else {
+      return $this->respond(['status' => 'fail', 'message' => '상태 변경 실패']);
+    }
+  }
+
+  //관리자 삭제
+  public function mngDelete($id)
+  {
+    $db = \Config\Database::connect();
+
+    //관리자 삭제
+    $deleted = $db->table('ADM_LIST')->where('ID', $id)->delete();
+
+    if ($deleted) {
+      return $this->respond(['status' => 'success', 'message' => '관리자 영구 삭제!'], 200);
+    } else {
+      return $this->respond(['status' => 'fail', 'message' => '삭제 실패!'], 500);
+    }
+  }
+}

+ 282 - 19
backend/app/Controllers/VendorController.php

@@ -56,7 +56,7 @@ class VendorController extends ResourceController
                 ]);
             }
 
-            $result = $this->vendorPartnershipModel->getVendorRequests($vendorSeq, $page, $size, $status);
+            $result = $this->vendorPartnershipModel->getVendorRequestsWithPagination($vendorSeq, $page, $size, $status);
 
             // 통계 계산 (히스토리 테이블이 없을 경우를 대비한 안전장치)
             $statsFormatted = [
@@ -284,7 +284,7 @@ class VendorController extends ResourceController
     }
 
     /**
-     * 파트너십 해지 (히스토리 테이블 기반)
+     * 파트너십 해지 (벤더사 권한)
      */
     public function terminatePartnership()
     {
@@ -293,7 +293,13 @@ class VendorController extends ResourceController
             
             $mappingSeq = $request->mappingSeq ?? null;
             $terminatedBy = $request->terminatedBy ?? null;
-            $terminationReason = $request->terminationReason ?? '';
+            $terminateReason = $request->terminateReason ?? ''; // 프론트엔드와 일치
+
+            log_message('debug', '파트너십 해지 요청: ' . json_encode([
+                'mappingSeq' => $mappingSeq,
+                'terminatedBy' => $terminatedBy,
+                'terminateReason' => $terminateReason
+            ]));
 
             if (!$mappingSeq || !$terminatedBy) {
                 return $this->response->setStatusCode(400)->setJSON([
@@ -312,6 +318,8 @@ class VendorController extends ResourceController
                 ]);
             }
 
+            log_message('debug', '현재 매핑 상태: ' . json_encode($mapping));
+
             // 현재 상태가 APPROVED인지 확인
             if ($mapping['CURRENT_STATUS'] !== 'APPROVED') {
                 return $this->response->setStatusCode(400)->setJSON([
@@ -326,27 +334,59 @@ class VendorController extends ResourceController
                 return $this->response->setStatusCode(400)->setJSON($processingUser);
             }
 
-            // 상태를 TERMINATED로 변경
-            $statusMessage = '파트너십 해지: ' . $terminationReason;
-            $this->statusHistoryModel->changeStatus($mappingSeq, 'TERMINATED', $statusMessage, $terminatedBy);
+            log_message('debug', '처리자 검증 완료: ' . json_encode($processingUser['data']));
 
-            // 해지 날짜 업데이트
-            $this->vendorInfluencerModel->update($mappingSeq, [
-                'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s')
-            ]);
+            // VendorPartnershipModel을 통한 해지 처리
+            $statusMessage = '파트너십 해지: ' . $terminateReason;
+            
+            // CHANGED_BY 값을 확실하게 설정 (processingUser에서 가져온 실제 SEQ 사용)
+            $actualChangedBy = $processingUser['data']['seq'] ?? $terminatedBy;
+            
+            // CHANGED_BY가 여전히 null이면 기본값 설정
+            if (!$actualChangedBy) {
+                log_message('warning', 'CHANGED_BY가 여전히 null - 원본 terminatedBy 사용: ' . $terminatedBy);
+                $actualChangedBy = $terminatedBy ?: 1; // 최종 기본값 1
+            }
+            
+            log_message('debug', "해지 처리 준비: mappingSeq={$mappingSeq}, changedBy={$actualChangedBy} (원본: {$terminatedBy})");
+            
+            try {
+                // 상태를 TERMINATED로 변경
+                $this->statusHistoryModel->changeStatus($mappingSeq, 'TERMINATED', $statusMessage, $actualChangedBy);
 
-            return $this->response->setJSON([
-                'success' => true,
-                'message' => '파트너십이 해지되었습니다.',
-                'data' => [
-                    'mappingSeq' => $mappingSeq,
-                    'status' => 'TERMINATED',
-                    'terminatedBy' => $processingUser['data']['name']
-                ]
-            ]);
+                // 해지 날짜 업데이트
+                $this->vendorInfluencerModel->update($mappingSeq, [
+                    'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s')
+                ]);
+
+                log_message('debug', '파트너십 해지 완료: mappingSeq=' . $mappingSeq);
+
+                return $this->response->setJSON([
+                    'success' => true,
+                    'message' => '파트너십이 해지되었습니다.',
+                    'data' => [
+                        'mappingSeq' => $mappingSeq,
+                        'status' => 'TERMINATED',
+                        'terminatedBy' => $processingUser['data']['name'],
+                        'terminateReason' => $terminateReason
+                    ]
+                ]);
+
+            } catch (\Exception $statusError) {
+                log_message('error', '상태 변경 실패: ' . $statusError->getMessage());
+                log_message('error', '상태 변경 스택 트레이스: ' . $statusError->getTraceAsString());
+                
+                return $this->response->setStatusCode(500)->setJSON([
+                    'success' => false,
+                    'message' => '파트너십 해지 중 오류가 발생했습니다.',
+                    'error' => $statusError->getMessage()
+                ]);
+            }
             
         } catch (\Exception $e) {
             log_message('error', '파트너십 해지 오류: ' . $e->getMessage());
+            log_message('error', '파트너십 해지 스택 트레이스: ' . $e->getTraceAsString());
+            
             return $this->response->setStatusCode(500)->setJSON([
                 'success' => false,
                 'message' => '파트너십 해지 중 오류가 발생했습니다.',
@@ -387,4 +427,227 @@ class VendorController extends ResourceController
             ]);
         }
     }
+
+    /**
+     * 인플루언서 요청 승인/거절 (프론트엔드 호환용)
+     * 프론트엔드에서 /api/vendor-influencer/approve 호출에 대응
+     */
+    public function approveInfluencerRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $action = $request->action ?? null; // 'APPROVE' or 'REJECT'
+            $processedBy = $request->processedBy ?? null;
+            $responseMessage = $request->responseMessage ?? '';
+            
+            log_message('debug', '프론트엔드 승인 처리 요청: ' . json_encode([
+                'mappingSeq' => $mappingSeq,
+                'action' => $action,
+                'processedBy' => $processedBy,
+                'responseMessage' => $responseMessage
+            ]));
+
+            if (!$mappingSeq || !$action || !$processedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다. (mappingSeq, action, processedBy 필요)'
+                ]);
+            }
+
+            // action 값 정규화 (프론트엔드에서는 대문자로 전송)
+            $normalizedAction = strtolower($action);
+            if (!in_array($normalizedAction, ['approve', 'reject'])) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => 'action은 APPROVE 또는 REJECT만 가능합니다.'
+                ]);
+            }
+
+            // 매핑 정보와 현재 상태 확인
+            $mapping = $this->vendorInfluencerModel->getWithCurrentStatus($mappingSeq);
+            
+            if (!$mapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '요청을 찾을 수 없습니다.'
+                ]);
+            }
+
+            // 현재 상태가 PENDING인지 확인
+            if ($mapping['CURRENT_STATUS'] !== 'PENDING') {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '이미 처리된 요청입니다. 현재 상태: ' . $mapping['CURRENT_STATUS']
+                ]);
+            }
+
+            // 처리자 확인
+            $processingUser = $this->validateProcessor($processedBy);
+            if (!$processingUser['success']) {
+                return $this->response->setStatusCode(400)->setJSON($processingUser);
+            }
+
+            // 상태 변경
+            $newStatus = ($normalizedAction === 'approve') ? 'APPROVED' : 'REJECTED';
+            $statusMessage = $responseMessage ?: ($normalizedAction === 'approve' ? '승인 처리됨' : '거부 처리됨');
+            
+            log_message('debug', "프론트엔드 상태 변경: {$mapping['CURRENT_STATUS']} → {$newStatus}");
+
+            // 히스토리 테이블에 상태 변경 기록
+            $this->statusHistoryModel->changeStatus($mappingSeq, $newStatus, $statusMessage, $processedBy);
+
+            // 메인 테이블 업데이트 (응답 관련 정보)
+            $this->vendorInfluencerModel->update($mappingSeq, [
+                'RESPONSE_MESSAGE' => $responseMessage,
+                'RESPONSE_DATE' => date('Y-m-d H:i:s'),
+                'APPROVED_BY' => $processedBy
+            ]);
+
+            // 승인인 경우 파트너십 시작일 설정
+            if ($normalizedAction === 'approve') {
+                $this->vendorInfluencerModel->update($mappingSeq, [
+                    'PARTNERSHIP_START_DATE' => date('Y-m-d H:i:s')
+                ]);
+            }
+
+            log_message('debug', "프론트엔드 승인 처리 완료: action={$normalizedAction}, newStatus={$newStatus}");
+
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => $normalizedAction === 'approve' ? '요청이 승인되었습니다.' : '요청이 거부되었습니다.',
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'action' => $action,
+                    'status' => $newStatus,
+                    'processedBy' => $processingUser['data']['name'],
+                    'responseMessage' => $responseMessage
+                ]
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '프론트엔드 승인 처리 중 예외 발생: ' . $e->getMessage());
+            log_message('error', '프론트엔드 승인 처리 스택 트레이스: ' . $e->getTraceAsString());
+            
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '요청 처리 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 디버깅용: 특정 매핑의 상태 정보 확인
+     */
+    public function debugMappingStatus($mappingSeq = null)
+    {
+        try {
+            $mappingSeq = $mappingSeq ?: $this->request->getGet('mappingSeq');
+            
+            if (!$mappingSeq) {
+                return $this->response->setJSON([
+                    'success' => false,
+                    'message' => 'mappingSeq 파라미터가 필요합니다.'
+                ]);
+            }
+
+            // 1. 메인 매핑 정보
+            $mapping = $this->vendorInfluencerModel->find($mappingSeq);
+            
+            // 2. 현재 상태
+            $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq);
+            
+            // 3. 모든 히스토리
+            $allHistory = $this->statusHistoryModel->getStatusHistory($mappingSeq, 50);
+            
+            // 4. 현재 상태가 여러 개인지 확인
+            $currentStatusCount = $this->statusHistoryModel
+                ->where('MAPPING_SEQ', $mappingSeq)
+                ->where('IS_CURRENT', 'Y')
+                ->countAllResults();
+
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'mapping' => $mapping,
+                    'currentStatus' => $currentStatus,
+                    'currentStatusCount' => $currentStatusCount,
+                    'statusHistory' => $allHistory,
+                    'historyCount' => count($allHistory)
+                ]
+            ]);
+
+        } catch (\Exception $e) {
+            return $this->response->setJSON([
+                'success' => false,
+                'message' => '상태 확인 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 디버깅용: 히스토리 테이블 insert 테스트
+     */
+    public function debugHistoryInsert()
+    {
+        try {
+            $request = $this->request->getJSON();
+            $mappingSeq = $request->mappingSeq ?? 1;
+            
+            // 최소한의 데이터로 테스트 insert
+            $testData = [
+                'MAPPING_SEQ' => (int)$mappingSeq,
+                'STATUS' => 'PENDING',
+                'PREVIOUS_STATUS' => null,
+                'STATUS_MESSAGE' => 'Test insert',
+                'CHANGED_BY' => 1,
+                'IS_CURRENT' => 'N', // 테스트용이므로 N으로 설정
+                'CHANGED_DATE' => date('Y-m-d H:i:s')
+            ];
+
+            log_message('debug', '테스트 insert 데이터: ' . json_encode($testData));
+
+            // validation 체크
+            if (!$this->statusHistoryModel->validate($testData)) {
+                $validationErrors = $this->statusHistoryModel->errors();
+                return $this->response->setJSON([
+                    'success' => false,
+                    'message' => 'Validation 실패',
+                    'errors' => $validationErrors,
+                    'data' => $testData
+                ]);
+            }
+
+            $result = $this->statusHistoryModel->insert($testData, false);
+            
+            if (!$result) {
+                $dbError = $this->statusHistoryModel->db->error();
+                return $this->response->setJSON([
+                    'success' => false,
+                    'message' => 'DB Insert 실패',
+                    'dbError' => $dbError,
+                    'data' => $testData
+                ]);
+            }
+
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => '테스트 insert 성공',
+                'insertId' => $result,
+                'data' => $testData
+            ]);
+
+        } catch (\Exception $e) {
+            return $this->response->setJSON([
+                'success' => false,
+                'message' => '테스트 insert 중 오류',
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+        }
+    }
 } 

+ 370 - 0
backend/app/Controllers/VendorControllerV2.php

@@ -0,0 +1,370 @@
+<?php
+
+namespace App\Controllers;
+
+use CodeIgniter\RESTful\ResourceController;
+use App\Models\VendorInfluencerMappingModel;
+use App\Models\VendorInfluencerStatusHistoryModel;
+use App\Models\VendorModel;
+use App\Models\UserModel;
+
+class VendorControllerV2 extends ResourceController
+{
+    protected $modelName = 'App\Models\VendorInfluencerMappingModel';
+    protected $format = 'json';
+    
+    protected $vendorInfluencerModel;
+    protected $statusHistoryModel;
+    protected $vendorModel;
+    protected $userModel;
+
+    public function __construct()
+    {
+        $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+        $this->statusHistoryModel = new VendorInfluencerStatusHistoryModel();
+        $this->vendorModel = new VendorModel();
+        $this->userModel = new UserModel();
+    }
+
+    /**
+     * 벤더사의 인플루언서 요청 목록 조회 (히스토리 테이블 기반)
+     */
+    public function getInfluencerRequests()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $vendorSeq = $request->vendorSeq ?? null;
+            $status = $request->status ?? null;
+            $page = $request->page ?? 1;
+            $size = $request->size ?? 20;
+
+            log_message('debug', 'getInfluencerRequests 호출: ' . json_encode([
+                'vendorSeq' => $vendorSeq,
+                'status' => $status,
+                'page' => $page,
+                'size' => $size
+            ]));
+
+            if (!$vendorSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '벤더사 SEQ는 필수입니다.'
+                ]);
+            }
+
+            $result = $this->vendorInfluencerModel->getInfluencerRequestsByVendor($vendorSeq, $page, $size, $status);
+
+            // 통계 계산
+            $stats = $this->statusHistoryModel->getStatusStatsByVendor($vendorSeq);
+            $statsFormatted = [
+                'pending' => 0,
+                'approved' => 0,
+                'rejected' => 0,
+                'total' => 0
+            ];
+
+            foreach ($stats as $stat) {
+                $statsFormatted['total'] += $stat['count'];
+                switch ($stat['STATUS']) {
+                    case 'PENDING':
+                        $statsFormatted['pending'] = $stat['count'];
+                        break;
+                    case 'APPROVED':
+                        $statsFormatted['approved'] = $stat['count'];
+                        break;
+                    case 'REJECTED':
+                        $statsFormatted['rejected'] = $stat['count'];
+                        break;
+                }
+            }
+
+            log_message('debug', 'API 응답 데이터: ' . json_encode([
+                'items_count' => count($result['data']),
+                'pagination' => $result['pagination'],
+                'stats' => $statsFormatted
+            ]));
+
+            // 프론트엔드에서 기대하는 응답 구조에 맞춤
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => [
+                    'items' => $result['data'],  // 프론트엔드에서 data.items로 접근
+                    'total' => $result['pagination']['total'],
+                    'page' => $result['pagination']['currentPage'],
+                    'totalPages' => $result['pagination']['totalPages'],
+                    'size' => $result['pagination']['limit'],
+                    'stats' => $statsFormatted
+                ]
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '인플루언서 요청 목록 조회 오류: ' . $e->getMessage());
+            log_message('error', '스택 트레이스: ' . $e->getTraceAsString());
+            
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '요청 목록 조회 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 인플루언서 요청 승인/거절 처리 (히스토리 테이블 기반)
+     */
+    public function processInfluencerRequest()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $action = $request->action ?? null; // 'approve' or 'reject'
+            $processedBy = $request->processedBy ?? null;
+            $responseMessage = $request->responseMessage ?? '';
+            
+            log_message('debug', '승인 처리 요청: ' . json_encode([
+                'mappingSeq' => $mappingSeq,
+                'action' => $action,
+                'processedBy' => $processedBy,
+                'responseMessage' => $responseMessage
+            ]));
+
+            if (!$mappingSeq || !$action || !$processedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다. (mappingSeq, action, processedBy 필요)'
+                ]);
+            }
+
+            // action 검증
+            if (!in_array($action, ['approve', 'reject'])) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => 'action은 approve 또는 reject만 가능합니다.'
+                ]);
+            }
+
+            // 매핑 정보와 현재 상태 확인
+            $mapping = $this->vendorInfluencerModel->getWithCurrentStatus($mappingSeq);
+            
+            if (!$mapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '요청을 찾을 수 없습니다.'
+                ]);
+            }
+
+            // 현재 상태가 PENDING인지 확인
+            if ($mapping['CURRENT_STATUS'] !== 'PENDING') {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '이미 처리된 요청입니다. 현재 상태: ' . $mapping['CURRENT_STATUS']
+                ]);
+            }
+
+            // 처리자 확인
+            $processingUser = $this->validateProcessor($processedBy);
+            if (!$processingUser['success']) {
+                return $this->response->setStatusCode(400)->setJSON($processingUser);
+            }
+
+            // 상태 변경
+            $newStatus = ($action === 'approve') ? 'APPROVED' : 'REJECTED';
+            $statusMessage = $responseMessage ?: ($action === 'approve' ? '승인 처리됨' : '거부 처리됨');
+            
+            log_message('debug', "상태 변경: {$mapping['CURRENT_STATUS']} → {$newStatus}");
+
+            // 히스토리 테이블에 상태 변경 기록
+            $this->statusHistoryModel->changeStatus($mappingSeq, $newStatus, $statusMessage, $processedBy);
+
+            // 메인 테이블 업데이트 (응답 관련 정보)
+            $this->vendorInfluencerModel->update($mappingSeq, [
+                'RESPONSE_MESSAGE' => $responseMessage,
+                'RESPONSE_DATE' => date('Y-m-d H:i:s'),
+                'APPROVED_BY' => $processedBy
+            ]);
+
+            // 승인인 경우 파트너십 시작일 설정
+            if ($action === 'approve') {
+                $this->vendorInfluencerModel->update($mappingSeq, [
+                    'PARTNERSHIP_START_DATE' => date('Y-m-d H:i:s')
+                ]);
+            }
+
+            log_message('debug', "승인 처리 완료: action={$action}, newStatus={$newStatus}");
+
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => $action === 'approve' ? '요청이 승인되었습니다.' : '요청이 거부되었습니다.',
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'action' => $action,
+                    'status' => $newStatus,
+                    'processedBy' => $processingUser['data']['name'],
+                    'responseMessage' => $responseMessage
+                ]
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '승인 처리 중 예외 발생: ' . $e->getMessage());
+            log_message('error', '승인 처리 스택 트레이스: ' . $e->getTraceAsString());
+            
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '요청 처리 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 처리자 검증 (벤더사 또는 사용자)
+     */
+    private function validateProcessor($processedBy)
+    {
+        // 1. 먼저 USER_LIST에서 확인 (인플루언서)
+        $user = $this->userModel
+            ->where('SEQ', $processedBy)
+            ->where('IS_ACT', 'Y')
+            ->first();
+
+        if ($user) {
+            return [
+                'success' => true,
+                'data' => [
+                    'type' => 'user',
+                    'seq' => $user['SEQ'],
+                    'name' => $user['NICK_NAME'] ?: $user['NAME']
+                ]
+            ];
+        }
+
+        // 2. VENDOR_LIST에서 확인 (벤더사)
+        $vendor = $this->vendorModel
+            ->where('SEQ', $processedBy)
+            ->where('IS_ACT', 'Y')
+            ->first();
+
+        if ($vendor) {
+            return [
+                'success' => true,
+                'data' => [
+                    'type' => 'vendor',
+                    'seq' => $vendor['SEQ'],
+                    'name' => $vendor['COMPANY_NAME'] . ' (벤더사)'
+                ]
+            ];
+        }
+
+        return [
+            'success' => false,
+            'message' => "처리자 SEQ {$processedBy}는 USER_LIST나 VENDOR_LIST에서 찾을 수 없습니다."
+        ];
+    }
+
+    /**
+     * 파트너십 해지 (히스토리 테이블 기반)
+     */
+    public function terminatePartnership()
+    {
+        try {
+            $request = $this->request->getJSON();
+            
+            $mappingSeq = $request->mappingSeq ?? null;
+            $terminatedBy = $request->terminatedBy ?? null;
+            $terminationReason = $request->terminationReason ?? '';
+
+            if (!$mappingSeq || !$terminatedBy) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '필수 파라미터가 누락되었습니다.'
+                ]);
+            }
+
+            // 매핑 정보와 현재 상태 확인
+            $mapping = $this->vendorInfluencerModel->getWithCurrentStatus($mappingSeq);
+            
+            if (!$mapping) {
+                return $this->response->setStatusCode(404)->setJSON([
+                    'success' => false,
+                    'message' => '파트너십을 찾을 수 없습니다.'
+                ]);
+            }
+
+            // 현재 상태가 APPROVED인지 확인
+            if ($mapping['CURRENT_STATUS'] !== 'APPROVED') {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '승인된 파트너십만 해지할 수 있습니다. 현재 상태: ' . $mapping['CURRENT_STATUS']
+                ]);
+            }
+
+            // 처리자 확인
+            $processingUser = $this->validateProcessor($terminatedBy);
+            if (!$processingUser['success']) {
+                return $this->response->setStatusCode(400)->setJSON($processingUser);
+            }
+
+            // 상태를 TERMINATED로 변경
+            $statusMessage = '파트너십 해지: ' . $terminationReason;
+            $this->statusHistoryModel->changeStatus($mappingSeq, 'TERMINATED', $statusMessage, $terminatedBy);
+
+            // 해지 날짜 업데이트
+            $this->vendorInfluencerModel->update($mappingSeq, [
+                'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s')
+            ]);
+
+            return $this->response->setJSON([
+                'success' => true,
+                'message' => '파트너십이 해지되었습니다.',
+                'data' => [
+                    'mappingSeq' => $mappingSeq,
+                    'status' => 'TERMINATED',
+                    'terminatedBy' => $processingUser['data']['name']
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            log_message('error', '파트너십 해지 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '파트너십 해지 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+
+    /**
+     * 벤더사 상태 통계 조회
+     */
+    public function getStatusStats()
+    {
+        try {
+            $request = $this->request->getJSON();
+            $vendorSeq = $request->vendorSeq ?? null;
+
+            if (!$vendorSeq) {
+                return $this->response->setStatusCode(400)->setJSON([
+                    'success' => false,
+                    'message' => '벤더사 SEQ는 필수입니다.'
+                ]);
+            }
+
+            $stats = $this->statusHistoryModel->getStatusStatsByVendor($vendorSeq);
+
+            return $this->response->setJSON([
+                'success' => true,
+                'data' => $stats
+            ]);
+
+        } catch (\Exception $e) {
+            log_message('error', '상태 통계 조회 오류: ' . $e->getMessage());
+            return $this->response->setStatusCode(500)->setJSON([
+                'success' => false,
+                'message' => '상태 통계 조회 중 오류가 발생했습니다.',
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+} 

+ 203 - 0
backend/app/Controllers/VendorInfluencerTerminate.php

@@ -0,0 +1,203 @@
+<?php
+  
+  namespace App\Controllers;
+  
+  use App\Controllers\BaseController;
+  use App\Models\VendorInfluencerMappingModel;
+  use App\Models\UserModel;
+  use CodeIgniter\HTTP\ResponseInterface;
+  
+  /**
+   * 벤더-인플루언서 파트너십 해지 API 예제
+   * 경로: POST /api/vendor-influencer/terminate
+   */
+  class VendorInfluencerTerminate extends BaseController
+  {
+    protected $vendorInfluencerModel;
+    protected $userModel;
+    
+    public function __construct()
+    {
+      $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+      $this->userModel = new UserModel();
+    }
+    
+    /**
+     * 승인된 파트너십 해지 처리
+     */
+    public function terminate()
+    {
+      try {
+        $request = $this->request->getJSON();
+        
+        $mappingSeq = $request->mappingSeq ?? null;
+        $terminateReason = $request->terminateReason ?? null;
+        $terminatedBy = $request->terminatedBy ?? null;
+        
+        // 필수 파라미터 검증
+        if (!$mappingSeq || !$terminateReason || !$terminatedBy) {
+          return $this->response->setStatusCode(400)->setJSON([
+            'success' => false,
+            'message' => '필수 파라미터가 누락되었습니다. (mappingSeq, terminateReason, terminatedBy 필요)'
+          ]);
+        }
+        
+        // 해지 사유 길이 검증
+        if (strlen($terminateReason) > 500) {
+          return $this->response->setStatusCode(400)->setJSON([
+            'success' => false,
+            'message' => '해지 사유는 500자를 초과할 수 없습니다.'
+          ]);
+        }
+        
+        // 기존 매핑 확인 (승인된 상태여야 함)
+        $existingMapping = $this->vendorInfluencerModel
+          ->where('SEQ', $mappingSeq)
+          ->where('STATUS', 'APPROVED')
+          ->where('IS_ACT', 'Y')
+          ->first();
+        
+        if (!$existingMapping) {
+          return $this->response->setStatusCode(404)->setJSON([
+            'success' => false,
+            'message' => '해지할 수 있는 승인된 파트너십을 찾을 수 없습니다.'
+          ]);
+        }
+        
+        // 해지 권한 확인 (벤더사 또는 관련 사용자만 해지 가능)
+        $terminatingUser = $this->userModel
+          ->where('SEQ', $terminatedBy)
+          ->where('IS_ACT', 'Y')
+          ->first();
+        
+        if (!$terminatingUser) {
+          return $this->response->setStatusCode(400)->setJSON([
+            'success' => false,
+            'message' => '해지 처리자 정보를 찾을 수 없습니다.'
+          ]);
+        }
+        
+        // 해지 처리 데이터 준비
+        $terminateData = [
+          'STATUS' => 'TERMINATED',
+          'RESPONSE_MESSAGE' => '파트너십 해지: ' . $terminateReason,
+          'RESPONSE_DATE' => date('Y-m-d H:i:s'),
+          'APPROVED_BY' => $terminatedBy, // 해지 처리자
+          'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s'), // 파트너십 종료일
+          'MOD_DATE' => date('Y-m-d H:i:s')
+        ];
+        
+        log_message('info', "파트너십 해지 처리 시작 - 매핑 SEQ: {$mappingSeq}, 해지자: {$terminatedBy}");
+        
+        // 해지 처리 실행
+        $result = $this->vendorInfluencerModel->update($mappingSeq, $terminateData);
+        
+        if (!$result) {
+          log_message('error', "파트너십 해지 업데이트 실패 - 매핑 SEQ: {$mappingSeq}");
+          return $this->response->setStatusCode(500)->setJSON([
+            'success' => false,
+            'message' => '파트너십 해지 처리 중 데이터베이스 오류가 발생했습니다.'
+          ]);
+        }
+        
+        // 해지된 매핑 정보 조회
+        $terminatedMapping = $this->vendorInfluencerModel
+          ->select('vim.SEQ, vim.VENDOR_SEQ, vim.INFLUENCER_SEQ, vim.STATUS,
+                         vim.RESPONSE_MESSAGE, vim.RESPONSE_DATE, vim.PARTNERSHIP_END_DATE,
+                         v.COMPANY_NAME as vendorName,
+                         inf.NICK_NAME as influencerNickname, inf.NAME as influencerName')
+          ->from('VENDOR_INFLUENCER_MAPPING vim')
+          ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+          ->join('USER_LIST inf', 'vim.INFLUENCER_SEQ = inf.SEQ', 'left')
+          ->where('vim.SEQ', $mappingSeq)
+          ->get()
+          ->getRowArray();
+        
+        log_message('info', "파트너십 해지 완료 - 매핑 SEQ: {$mappingSeq}");
+        
+        return $this->response->setJSON([
+          'success' => true,
+          'message' => '파트너십이 성공적으로 해지되었습니다.',
+          'data' => [
+            'terminatedMapping' => $terminatedMapping,
+            'terminateDate' => date('Y-m-d H:i:s'),
+            'terminatedBy' => $terminatingUser['NICK_NAME'] ?? $terminatingUser['NAME']
+          ]
+        ]);
+        
+      } catch (\Exception $e) {
+        log_message('error', "파트너십 해지 처리 중 예외 발생: " . $e->getMessage());
+        
+        return $this->response->setStatusCode(500)->setJSON([
+          'success' => false,
+          'message' => '파트너십 해지 처리 중 오류가 발생했습니다.',
+          'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+        ]);
+      }
+    }
+  }
+  
+  /**
+   * 라우터 설정 예제 (routes.php에 추가)
+   *
+   * $routes->group('api/vendor-influencer', ['namespace' => 'App\Controllers'], function($routes) {
+   *     $routes->post('terminate', 'VendorInfluencerController::terminate');
+   * });
+   */
+  
+  /**
+   * 프론트엔드에서 호출 예제
+   *
+   * const params = {
+   *   mappingSeq: 123,
+   *   terminateReason: "계약 조건 위반으로 인한 해지",
+   *   terminatedBy: 8 // 해지 처리자 USER SEQ
+   * };
+   *
+   * useAxios()
+   *   .post('/api/vendor-influencer/terminate', params)
+   *   .then((res) => {
+   *     if (res.data.success) {
+   *       console.log('해지 완료:', res.data.data);
+   *       // 성공 처리
+   *     } else {
+   *       console.error('해지 실패:', res.data.message);
+   *       // 실패 처리
+   *     }
+   *   })
+   *   .catch((err) => {
+   *     console.error('해지 오류:', err);
+   *   });
+   */
+  
+  /**
+   * 응답 예제
+   *
+   * 성공시:
+   * {
+   *   "success": true,
+   *   "message": "파트너십이 성공적으로 해지되었습니다.",
+   *   "data": {
+   *     "terminatedMapping": {
+   *       "SEQ": 123,
+   *       "VENDOR_SEQ": 8,
+   *       "INFLUENCER_SEQ": 23,
+   *       "STATUS": "TERMINATED",
+   *       "RESPONSE_MESSAGE": "파트너십 해지: 계약 조건 위반으로 인한 해지",
+   *       "RESPONSE_DATE": "2025-07-23 10:30:00",
+   *       "PARTNERSHIP_END_DATE": "2025-07-23 10:30:00",
+   *       "vendorName": "테스트 벤더사",
+   *       "influencerNickname": "인플루언서닉네임",
+   *       "influencerName": "인플루언서이름"
+   *     },
+   *     "terminateDate": "2025-07-23 10:30:00",
+   *     "terminatedBy": "벤더관리자"
+   *   }
+   * }
+   *
+   * 실패시:
+   * {
+   *   "success": false,
+   *   "message": "해지할 수 있는 승인된 파트너십을 찾을 수 없습니다."
+   * }
+   */

+ 484 - 0
backend/app/Controllers/Winner.php

@@ -0,0 +1,484 @@
+<?php
+
+  namespace App\Controllers;
+
+  use CodeIgniter\RESTful\ResourceController;
+
+  class Winner extends ResourceController
+  {
+    //당첨자 리스트
+    public function winnerlist()
+    {      
+      $db = \Config\Database::connect();
+
+      // POST JSON 파라미터 받기
+      $request = $this->request->getJSON(true);
+      if (!isset($request['compId'])) {
+        return $this->respond([
+          'status' => 'fail',
+          'message' => 'filter(compId)가 누락되었습니다.'
+        ], 400);
+      }
+      $status = isset($request['status']) ? $request['status'] : null;
+      $compId = $request['compId'];
+      // 쿼리 빌더
+      $builder = $db->table('EVT_LIST');
+      // compId가 '0-000000'이 아닐 때만 COMP_ID 조건 추가
+      if ($compId !== '0-000000') {
+        $builder->where('COMP_ID', $compId);
+      }
+      if ($status !== null) {
+        // status가 넘어오면 해당 값만 검색
+        $builder->where('status', $status);
+      }
+
+      $lists = $builder->get()->getResultArray();
+
+      foreach ($lists as &$row) {
+        if (isset($row['STARTDATE']) && !empty($row['STARTDATE'])) {
+          $row['STARTDATE'] = date('Y-m-d', strtotime($row['STARTDATE']));
+        }
+        if (isset($row['ENDDATE']) && !empty($row['ENDDATE'])) {
+          $row['ENDDATE'] = date('Y-m-d', strtotime($row['ENDDATE']));
+        }
+      }
+
+
+
+
+      // 반환 데이터 가공 (필요시)
+      $filtered = array_reverse($lists);
+
+      return $this->respond($filtered, 200);
+
+    }
+
+    //이벤트 마감 체크
+    public function winnerChk()
+    {
+      $db = \Config\Database::connect();
+
+      $seq = null;
+      $requestJson = $this->request->getJSON(true);
+      if (is_array($requestJson) && isset($requestJson['seq'])) {
+        $seq = $requestJson['seq'];
+      }
+      
+      if (empty($seq)) {
+        return $this->respond(['status' => 'fail', 'message' => '필수 파라미터 누락'], 400);
+      }
+
+
+
+      // EVT_ITEM에서 해당 seq 로 아이템과 WIN_QTY 조회
+      $items = $db->table('EVT_ITEM')
+        ->select('ITEM_SEQ, WIN_QTY')
+        ->where('EVT_SEQ', $seq)
+        ->get()->getResultArray();
+
+      // 아이템별 최대 당첨자수 배열 구성
+      $maxWinners = [];
+      foreach ($items as $idx => $item) {
+        $rank = $idx + 1; // 1등부터 시작
+        $maxWinners[$rank] = (int)$item['WIN_QTY'];
+      }
+
+      // PARTICIPATION_LIST에서 seq로 당첨자 집계 (RANK별 COUNT)
+      $counts = [];
+      $results = $db->table('PARTICIPATION_LIST')
+        ->select('RANK, COUNT(*) as cnt')
+        ->where('EVT_SEQ', $seq)
+        ->groupBy('RANK')
+        ->get()->getResultArray();
+      foreach ($results as $row) {
+        $counts[(int)$row['RANK']] = (int)$row['cnt'];
+      }
+
+      // 모든 RANK의 당첨자수가 max와 같거나 크면 "마감"
+      $isClosed = true;
+      foreach ($maxWinners as $rank => $qty) {
+        $curr = isset($counts[$rank]) ? $counts[$rank] : 0;
+        if ($curr < $qty) {
+          $isClosed = false;
+          break;
+        }
+      }
+
+      if ($isClosed) {
+        return $this->respond(['status' => 'closed', 'message' => '마감되었습니다'], 200);
+      } else {
+        return $this->respond(['status' => 'open', 'message' => '아직 마감 아님'], 200);
+      }
+
+    }
+
+    //당첨자 등록 및 랭크 반환
+    public function winnerReg()
+    {
+      $db = \Config\Database::connect();
+
+      // 파라미터 추출
+      $seq = null;
+      $name = null;
+      $phone = null;
+
+      $request = $this->request->getJSON(true);
+      if (is_array($request) && isset($request['seq'])) {
+          $seq = $request['seq'];
+          $name = $request['name'];
+          $phone = $request['phone'];
+      } else {
+          $seq = $this->request->getPost('seq');
+          $name = $this->request->getPost('name');
+          $phone = $this->request->getPost('phone');
+      }
+
+      // 등수별 설정 및 ITEM_NAME 매핑
+      $rankConfigs = [];
+      $itemNames = [];
+
+      if (!empty($seq)) {
+          $builder = $db->table('EVT_ITEM');
+          $builder->select('WIN_RATE, WIN_QTY, ITEM_NAME');
+          $builder->where('EVT_SEQ', $seq);
+          $builder->orderBy('ITEM_SEQ', 'ASC');
+          $rows = $builder->get()->getResultArray();
+
+          foreach ($rows as $i => $row) {
+              $rank = $i + 1; // 1등부터 시작
+              $rankConfigs[$rank] = [
+                  'percent' => (int)$row['WIN_RATE'],
+                  'max' => (int)$row['WIN_QTY']
+              ];
+              $itemNames[$rank] = $row['ITEM_NAME'];
+          }
+      }
+
+      // 등수별 참여자 집계
+      $builder = $db->table('PARTICIPATION_LIST')
+          ->select('RANK, COUNT(*) AS cnt')
+          ->where('EVT_SEQ', $seq)
+          ->groupBy('RANK');
+      $result = $builder->get()->getResultArray();
+
+      $winnerCounts = [];
+      foreach ($result as $row) {
+          $winnerCounts[(int)$row['RANK']] = (int)$row['cnt'];
+      }
+
+      // 가능한 등수 구하기
+      $availableRanks = [];
+      $totalPercent = 0;
+      foreach ($rankConfigs as $rank => $cfg) {
+          $cnt = isset($winnerCounts[$rank]) ? $winnerCounts[$rank] : 0;
+          if ($cnt < $cfg['max']) {
+              $availableRanks[] = [
+                  'rank' => $rank,
+                  'percent' => $cfg['percent']
+              ];
+              $totalPercent += $cfg['percent'];
+          }
+      }
+
+      //      if (empty($availableRanks)) {
+      //          return $this->respond(['status' => 'fail', 'message' => '모든 등수 당첨자가 마감되었습니다.'], 200);
+      //      }
+
+      if (empty($availableRanks)) {
+          // 등수 마감 상태 => rank 0으로 처리
+          $selectedRank = 0;
+          $selectedItemName = "꽝";
+          $rankIndex = 0;
+      } else {
+          // 기존 랜덤 등수 추출
+          $rand = mt_rand() / mt_getrandmax() * $totalPercent;
+          foreach ($availableRanks as $cfg) {
+              if ($rand < $cfg['percent']) {
+                  $selectedRank = $cfg['rank'];
+                  break;
+              }
+              $rand -= $cfg['percent'];
+          }
+          if (!isset($selectedRank)) {
+              $selectedRank = $availableRanks[count($availableRanks) - 1]['rank'];
+          }
+          $selectedItemName = isset($itemNames[$selectedRank]) ? $itemNames[$selectedRank] : null;
+
+          // RANK_INDEX 구하기
+          $builder = $db->table('PARTICIPATION_LIST');
+          $builder->selectMax('RANK_INDEX');
+          $builder->where('EVT_SEQ', $seq);
+          $builder->where('RANK', $selectedRank);
+          $rankIndexRow = $builder->get()->getRowArray();
+          $rankIndex = isset($rankIndexRow['RANK_INDEX']) && $rankIndexRow['RANK_INDEX'] ? ((int)$rankIndexRow['RANK_INDEX']) + 1 : 1;
+      }
+
+      // DB 저장
+      $insertData = [
+          'EVT_SEQ'     => $seq,
+          'RANK'        => $selectedRank,
+          'ITEM_NAME'   => $selectedItemName,
+          'NAME'        => $name,
+          'PHONE'       => $phone,
+          'RANK_INDEX'  => $rankIndex,
+          'PRIVACY_AGREE' => 1,
+          'THIRDPARTY_AGREE' => 1
+      ];
+      $db->table('PARTICIPATION_LIST')->insert($insertData);
+
+      // 응답
+      return $this->respond([
+          'rank' => $selectedRank,
+          'item_name' => $selectedItemName,
+          'rank_index' => $rankIndex,
+          'name' => $name,
+          'phone' => $phone,
+      ], 200);
+    }
+
+    //아이템 리스트
+    public  function itemCount()
+    {
+      $db = \Config\Database::connect();
+      // POST 파라미터 JSON 또는 폼에서 받기 (JSON 우선)
+      $seq = null;
+      $request = $this->request->getJSON(true);
+      if (is_array($request) && isset($request['seq'])) {
+        $seq = $request['seq'];
+      } else {
+        $seq = $this->request->getPost('seq'); // 폼 방식 대비
+      }
+
+      if (empty($seq)) {
+        return $this->respond(['status' => 'fail', 'message' => 'seq 파라미터가 필요합니다.'], 400);
+      }
+
+      $builder = $db->table('EVT_ITEM');
+      $builder->select('ITEM_NAME');
+      $builder->where('EVT_SEQ', $seq);
+      $query = $builder->get();
+      $items = [];
+      foreach ($query->getResultArray() as $row) {
+        $items[] = $row['ITEM_NAME'];
+      }
+
+      $count = count($items);
+
+      return $this->respond([
+        'count' => $count,
+        'items'  => $items
+      ], 200);
+
+    }
+
+    //당첨자 상세
+    public  function winnerDetail($seq)
+    {
+      $db = \Config\Database::connect();
+
+      // 이벤트 + 아이템 목록 조인 조회
+      $builder = $db->table('EVT_LIST E');
+      $builder->join('EVT_ITEM I', 'E.SEQ = I.EVT_SEQ', 'left');
+      $builder->join('PARTICIPATION_LIST P', 'E.SEQ = P.EVT_SEQ', 'left');
+      $builder->select(
+        'E.SEQ, E.TITLE, E.STARTDATE, E.ENDDATE, E.REGDATE, ' .
+        'I.ITEM_SEQ, I.ITEM_NAME AS ITEM_ITEM_NAME, I.WIN_QTY, I.WIN_RATE,' .
+        'P.PART_SEQ, P.RANK, P.ITEM_NAME AS PART_ITEM_NAME, P.ID, P.NAME, P.PHONE, P.WINNER_DATE, P.RANK_INDEX, P.PRIVACY_AGREE, P.THIRDPARTY_AGREE'
+      );
+      $builder->where('E.SEQ', $seq);
+
+      $rows = $builder->get()->getResultArray();
+
+      if (empty($rows)) {
+        return $this->respond(['status' => 'fail', 'message' => '해당 이벤트가 없습니다.'], 404);
+      }
+
+      // 이벤트(게시글) 정보와 아이템 배열로 가공하여 응답
+      $event = [
+        'seq' => $rows[0]['SEQ'],
+        'title' => $rows[0]['TITLE'],
+        'startdate' => date('Y-m-d', strtotime( $rows[0]['STARTDATE'])),
+        'enddate' => date('Y-m-d', strtotime( $rows[0]['ENDDATE'])),
+        'regdate' =>date('Y-m-d', strtotime( $rows[0]['REGDATE'])),
+        'items' => [],
+        'participations' => [],
+        'participations_cal' => []
+      ];
+
+      // 중복방지용
+      $itemSeqSet = [];
+      $partSeqSet = [];
+
+      foreach ($rows as $row) {
+        // 아이템 중복 체크 및 추가
+        if ($row['ITEM_SEQ'] !== null && !isset($itemSeqSet[$row['ITEM_SEQ']])) {
+          $event['items'][] = [
+            'item_seq' => $row['ITEM_SEQ'],
+            'name' => $row['ITEM_ITEM_NAME'], // 반드시! EVT_ITEM 테이블의 컬럼명 별칭
+            'qty' => $row['WIN_QTY'],
+            'rate' => $row['WIN_RATE']
+          ];
+          $itemSeqSet[$row['ITEM_SEQ']] = true;
+        }
+
+
+        // 참여자 중복 체크 및 추가
+        if ($row['PART_SEQ'] !== null && !isset($partSeqSet[$row['PART_SEQ']])) {
+          $event['participations'][] = [
+            'part_seq' => $row['PART_SEQ'],
+            'rank' => $row['RANK'],
+            'item_name' => $row['PART_ITEM_NAME'],
+            'id' => $row['ID'],
+            'name' => $row['NAME'],
+            'phone' => $row['PHONE'],
+            'winner_date' => $row['WINNER_DATE'] = date('Y-m-d', strtotime($row['WINNER_DATE'])),
+            'rank_index' => $row['RANK_INDEX'],
+            'privacy_agree' => $row['PRIVACY_AGREE'],
+            'thirdparty_agree' => $row['THIRDPARTY_AGREE']
+          ];
+          $partSeqSet[$row['PART_SEQ']] = true;
+        }
+      }
+
+
+      // 중복되는 rank에서 part_seq가 가장 낮은 값만 participations_cal에 담기
+      // 유니크 참여자 배열 만들기
+      $participations = [];
+      foreach ($rows as $row) {
+          if (
+              !array_key_exists('PART_SEQ', $row) ||
+              !array_key_exists('RANK', $row) ||
+              $row['PART_SEQ'] === null ||
+              $row['RANK'] === null ||
+              $row['RANK'] == 0
+          ) {
+              continue;
+          }
+          $participations[$row['PART_SEQ']] = $row;
+      }
+
+      // 등수별 인원수 카운트
+      $rankCounts = [];
+      foreach ($participations as $part) {
+          $rank = $part['RANK'];
+          if (!isset($rankCounts[$rank])) {
+              $rankCounts[$rank] = 0;
+          }
+          $rankCounts[$rank]++;
+      }
+
+      // 중복 등수에서 part_seq가 가장 낮은 값만 participations_cal에 담기 (참여자 기준)
+      $rankMinPartSeq = [];
+      foreach ($participations as $part) {
+          $rank = $part['RANK'];
+          $partSeq = $part['PART_SEQ'];
+          if (!isset($rankMinPartSeq[$rank]) || $partSeq < $rankMinPartSeq[$rank]['part_seq']) {
+              $rankMinPartSeq[$rank] = [
+                  'part_seq' => $partSeq,
+                  'rank' => $part['RANK'],
+                  'item_name' => $part['PART_ITEM_NAME'],
+                  'id' => $part['ID'],
+                  'name' => $part['NAME'],
+                  'phone' => $part['PHONE'],
+                  'winner_date' => $part['WINNER_DATE'],
+                  'rank_index' => $part['RANK_INDEX'],
+                  'privacy_agree' => $part['PRIVACY_AGREE'],
+                  'thirdparty_agree' => $part['THIRDPARTY_AGREE'],
+                  'count' => $rankCounts[$rank]
+              ];
+          }
+      }
+      $event['participations_cal'] = array_values($rankMinPartSeq);
+
+      return $this->respond($event, 200);
+    }
+
+    //참여자 리스트
+    public function getParticipationByItem()
+    {
+      $db = \Config\Database::connect();
+      $request = $this->request->getJSON(true);
+
+      // 파라미터 추출
+      $seq = isset($request['seq']) ? $request['seq'] : null;
+      $itemName = isset($request['item_name']) ? $request['item_name'] : null;
+
+      if (empty($seq)) {
+        return $this->respond(['status' => 'fail', 'message' => 'seq는 필수입니다.'], 400);
+      }
+
+      // PARTICIPATION_LIST에서 EVT_SEQ와 ITEM_NAME으로 필터링
+      $builder = $db->table('PARTICIPATION_LIST');
+      $builder->where('EVT_SEQ', $seq);
+      if (!empty($itemName)) {
+        $builder->where('ITEM_NAME', $itemName);
+      }
+
+      $filterList = $builder->get()->getResultArray();
+
+      // 키를 모두 소문자로 변환
+      $filterListLower = [];
+      foreach ($filterList as $row) {
+        $filterListLower[] = array_change_key_case($row, CASE_LOWER);
+      }
+
+
+      return $this->respond([
+        'status' => 'success',
+        'list' => $filterListLower
+      ], 200);
+    }
+
+    // 이벤트 참가 여부 체크
+    public function matchedUser()
+    {
+      $data = $this->request->getJSON(true);
+
+      $seq = $data['seq'] ?? null;
+      $name = $data['name'] ?? null;
+      $phone = $data['phone'] ?? null;
+
+
+      if (!$seq || !$name || !$phone) {
+        return $this->fail('필수 값이 누락되었습니다.', 400);
+      }
+
+
+      $db = \Config\Database::connect();
+      $builder = $db->table('PARTICIPATION_LIST');
+
+
+      $existing = $builder
+        ->where('EVT_SEQ', $seq)
+        ->where('NAME', $name)
+        ->where('PHONE', $phone)
+        ->get()
+        ->getRowArray();
+
+      if ($existing) {
+        // 동일한 사람이 이미 존재할 경우
+        return $this->respond([
+          'result' => 'matched'
+        ]);
+      }
+
+      $phoneSame = $builder
+        ->where('EVT_SEQ', $seq)
+        ->where('NAME !=', $name)
+        ->where('PHONE', $phone)
+        ->get()
+        ->getRowArray();
+
+      if ($phoneSame) {
+        return $this->respond([
+          'result' => 'phonesame'
+        ]);
+      }
+
+      // 일치하는 정보가 없을 때 (필요에 따라 처리)
+      return $this->respond([
+        'result' => 'not_found'
+      ]);
+
+    }
+  }

+ 0 - 0
backend/app/Models/.gitkeep


+ 5 - 3
backend/app/Models/InfluencerPartnershipModel.php

@@ -122,7 +122,7 @@ class InfluencerPartnershipModel extends Model
     }
     
     /**
-     * 인플루언서 승인 요청 생성
+     * 승인 요청 생성
      */
     public function createApprovalRequest($data)
     {
@@ -142,7 +142,8 @@ class InfluencerPartnershipModel extends Model
             'IS_ACT' => 'Y'
         ]);
         
-        return $this->insert($insertData);
+        // mappingModel을 사용하여 insert (콜백 자동 실행)
+        return $this->mappingModel->insert($insertData);
     }
     
     /**
@@ -181,7 +182,8 @@ class InfluencerPartnershipModel extends Model
             'IS_ACT' => 'Y'
         ]);
         
-        return $this->insert($insertData);
+        // mappingModel을 사용하여 insert (콜백 자동 실행)
+        return $this->mappingModel->insert($insertData);
     }
     
     /**

+ 32 - 0
backend/app/Models/LoginModel.php

@@ -0,0 +1,32 @@
+<?php
+  // app/Models/LoginModel.php
+  namespace App\Models;
+  
+  use Config\Database;
+  
+  class LoginModel
+  {
+    /**
+     * 로그인 타입에 맞는 쿼리 빌더를 반환합니다.
+     * @param string $loginType ('vendor' or 'influence')
+     * @return \CodeIgniter\Database\BaseBuilder
+     */
+    public function getBuilderFor(string $loginType)
+    {
+      $db = Database::connect();
+      $tableName = '';
+      
+      switch ($loginType) {
+        case 'vendor':
+          $tableName = 'VENDOR_LIST';
+          break;
+        case 'influence':
+          $tableName = 'USER_LIST';
+          break;
+        default:
+          throw new \InvalidArgumentException("Invalid login type provided: " . $loginType);
+      }
+      
+      return $db->table($tableName);
+    }
+  }

+ 12 - 0
backend/app/Models/UserListModel.php

@@ -0,0 +1,12 @@
+<?php
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class UserListModel extends Model
+  {
+    protected $table      = 'USER_LIST';
+    protected $primaryKey = 'SEQ'; // PK 컬럼명 맞춰주세요
+    
+//    protected $allowedFields = ['user_name', 'email', /* 기타 컬럼들 */];
+  }

+ 223 - 0
backend/app/Models/UserModel.php

@@ -0,0 +1,223 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class UserModel extends Model
+  {
+    protected $table = 'USER_LIST';
+    protected $primaryKey = 'SEQ';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+      'ID',
+      'PASSWORD',
+      'NICK_NAME',
+      'EMAIL',
+      'PHONE',
+      'MEMBER_TYPE',
+      'STATUS',
+      'LAST_LOGIN_DATE',
+      'IS_ACT',
+      'REG_DATE',
+      'MOD_DATE',
+      // 인플루언서 관련 필드들
+      'INFLUENCER_TYPE',
+      'PRIMARY_CATEGORY',
+      'FOLLOWER_COUNT',
+      'AVG_VIEWS',
+      'PROFILE_IMAGE',
+      'BIO',
+      'INSTAGRAM_URL',
+      'YOUTUBE_URL',
+      'TIKTOK_URL',
+      'BLOG_URL',
+      'PREFERRED_REGION',
+      'MIN_COMMISSION_RATE',
+      'VERIFICATION_STATUS',
+      'VERIFIED_DATE'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'REG_DATE';
+    protected $updatedField = 'MOD_DATE';
+    
+    protected $validationRules = [
+      'ID' => 'required|max_length[50]|is_unique[USER_LIST.ID,SEQ,{SEQ}]',
+      'PASSWORD' => 'required|min_length[8]',
+      'NICK_NAME' => 'required|max_length[100]',
+      'EMAIL' => 'required|valid_email|is_unique[USER_LIST.EMAIL,SEQ,{SEQ}]',
+      'PHONE' => 'permit_empty|max_length[20]',
+      'MEMBER_TYPE' => 'required|in_list[ADMIN,INFLUENCER,VENDOR]',
+      'STATUS' => 'required|in_list[ACTIVE,INACTIVE,SUSPENDED,PENDING]',
+      'IS_ACT' => 'required|in_list[Y,N]',
+      'INFLUENCER_TYPE' => 'permit_empty|in_list[MACRO,MICRO,NANO,MEGA]',
+      'PRIMARY_CATEGORY' => 'permit_empty|in_list[FASHION_BEAUTY,FOOD_HEALTH,LIFESTYLE,TECH_ELECTRONICS,SPORTS_LEISURE,CULTURE_ENTERTAINMENT]',
+      'FOLLOWER_COUNT' => 'permit_empty|integer|greater_than_equal_to[0]',
+      'AVG_VIEWS' => 'permit_empty|integer|greater_than_equal_to[0]',
+      'PREFERRED_REGION' => 'permit_empty|in_list[SEOUL,GYEONGGI,INCHEON,BUSAN,DAEGU,DAEJEON,GWANGJU,ULSAN,OTHER]',
+      'MIN_COMMISSION_RATE' => 'permit_empty|decimal|greater_than_equal_to[0]|less_than_equal_to[100]',
+      'VERIFICATION_STATUS' => 'permit_empty|in_list[UNVERIFIED,PENDING,VERIFIED,REJECTED]'
+    ];
+    
+    protected $validationMessages = [
+      'ID' => [
+        'required' => '아이디는 필수입니다.',
+        'max_length' => '아이디는 50자를 초과할 수 없습니다.',
+        'is_unique' => '이미 사용 중인 아이디입니다.'
+      ],
+      'PASSWORD' => [
+        'required' => '비밀번호는 필수입니다.',
+        'min_length' => '비밀번호는 최소 8자 이상이어야 합니다.'
+      ],
+      'NICK_NAME' => [
+        'required' => '닉네임은 필수입니다.',
+        'max_length' => '닉네임은 100자를 초과할 수 없습니다.'
+      ],
+      'EMAIL' => [
+        'required' => '이메일은 필수입니다.',
+        'valid_email' => '유효한 이메일 형식이 아닙니다.',
+        'is_unique' => '이미 사용 중인 이메일입니다.'
+      ],
+      'MEMBER_TYPE' => [
+        'required' => '회원 유형은 필수입니다.',
+        'in_list' => '유효하지 않은 회원 유형입니다.'
+      ],
+      'STATUS' => [
+        'required' => '상태는 필수입니다.',
+        'in_list' => '유효하지 않은 상태입니다.'
+      ],
+      'IS_ACT' => [
+        'required' => '활성 상태는 필수입니다.',
+        'in_list' => '활성 상태는 Y 또는 N이어야 합니다.'
+      ]
+    ];
+    
+    protected $skipValidation = false;
+    protected $cleanValidationRules = true;
+    
+    /**
+     * 인플루언서 목록 조회
+     */
+    public function getInfluencers($filters = [], $page = 1, $perPage = 12)
+    {
+      $builder = $this->where('MEMBER_TYPE', 'INFLUENCER')
+        ->where('IS_ACT', 'Y')
+        ->where('STATUS', 'ACTIVE');
+      
+      // 키워드 검색
+      if (!empty($filters['keyword'])) {
+        $builder->groupStart()
+          ->like('NICK_NAME', $filters['keyword'])
+          ->orLike('ID', $filters['keyword'])
+          ->groupEnd();
+      }
+      
+      // 카테고리 필터
+      if (!empty($filters['category'])) {
+        $builder->where('PRIMARY_CATEGORY', $filters['category']);
+      }
+      
+      // 인플루언서 타입 필터
+      if (!empty($filters['influencer_type'])) {
+        $builder->where('INFLUENCER_TYPE', $filters['influencer_type']);
+      }
+      
+      // 팔로워 수 범위
+      if (!empty($filters['follower_min'])) {
+        $builder->where('FOLLOWER_COUNT >=', $filters['follower_min']);
+      }
+      if (!empty($filters['follower_max'])) {
+        $builder->where('FOLLOWER_COUNT <=', $filters['follower_max']);
+      }
+      
+      // 페이징
+      $offset = ($page - 1) * $perPage;
+      return $builder->limit($perPage, $offset)->findAll();
+    }
+    
+    /**
+     * 인플루언서 검색 결과 총 개수
+     */
+    public function countInfluencers($filters = [])
+    {
+      $builder = $this->where('MEMBER_TYPE', 'INFLUENCER')
+        ->where('IS_ACT', 'Y')
+        ->where('STATUS', 'ACTIVE');
+      
+      // 키워드 검색
+      if (!empty($filters['keyword'])) {
+        $builder->groupStart()
+          ->like('NICK_NAME', $filters['keyword'])
+          ->orLike('ID', $filters['keyword'])
+          ->groupEnd();
+      }
+      
+      // 카테고리 필터
+      if (!empty($filters['category'])) {
+        $builder->where('PRIMARY_CATEGORY', $filters['category']);
+      }
+      
+      // 인플루언서 타입 필터
+      if (!empty($filters['influencer_type'])) {
+        $builder->where('INFLUENCER_TYPE', $filters['influencer_type']);
+      }
+      
+      // 팔로워 수 범위
+      if (!empty($filters['follower_min'])) {
+        $builder->where('FOLLOWER_COUNT >=', $filters['follower_min']);
+      }
+      if (!empty($filters['follower_max'])) {
+        $builder->where('FOLLOWER_COUNT <=', $filters['follower_max']);
+      }
+      
+      return $builder->countAllResults();
+    }
+    
+    /**
+     * 인플루언서 타입별 통계
+     */
+    public function getInfluencerTypeStats()
+    {
+      return $this->select('INFLUENCER_TYPE, COUNT(*) as count')
+        ->where('MEMBER_TYPE', 'INFLUENCER')
+        ->where('IS_ACT', 'Y')
+        ->where('STATUS', 'ACTIVE')
+        ->groupBy('INFLUENCER_TYPE')
+        ->findAll();
+    }
+    
+    /**
+     * 카테고리별 인플루언서 통계
+     */
+    public function getInfluencerCategoryStats()
+    {
+      return $this->select('PRIMARY_CATEGORY, COUNT(*) as count')
+        ->where('MEMBER_TYPE', 'INFLUENCER')
+        ->where('IS_ACT', 'Y')
+        ->where('STATUS', 'ACTIVE')
+        ->groupBy('PRIMARY_CATEGORY')
+        ->findAll();
+    }
+    
+    /**
+     * 사용자 로그인
+     */
+    public function authenticate($id, $password)
+    {
+      $user = $this->where('ID', $id)
+        ->where('IS_ACT', 'Y')
+        ->first();
+      
+      if ($user && password_verify($password, $user['PASSWORD'])) {
+        // 로그인 날짜 업데이트
+        $this->update($user['SEQ'], ['LAST_LOGIN_DATE' => date('Y-m-d H:i:s')]);
+        return $user;
+      }
+      
+      return false;
+    }
+  }

+ 62 - 0
backend/app/Models/VendorAddressModel.php

@@ -0,0 +1,62 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class VendorAddressModel extends Model
+  {
+    protected $table = 'vendor_addresses';
+    protected $primaryKey = 'id';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+      'vendor_id', 'address_type', 'zip_code', 'address',
+      'detail_address', 'city', 'district', 'is_primary'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'created_at';
+    protected $updatedField = 'updated_at';
+    
+    protected $validationRules = [
+      'vendor_id' => 'required|integer|is_not_unique[vendors.id]',
+      'address_type' => 'required|in_list[HEAD_OFFICE,BRANCH,WAREHOUSE,BILLING]',
+      'zip_code' => 'permit_empty|max_length[10]',
+      'address' => 'required|max_length[500]',
+      'detail_address' => 'permit_empty|max_length[500]',
+      'city' => 'permit_empty|max_length[100]',
+      'district' => 'permit_empty|max_length[100]',
+      'is_primary' => 'permit_empty|in_list[0,1]'
+    ];
+    
+    protected $validationMessages = [
+      'vendor_id' => [
+        'required' => '벤더사 id는 필수입니다.',
+        'is_not_unique' => '존재하지 않는 벤더사입니다.'
+      ],
+      'address' => [
+        'required' => '주소는 필수입니다.'
+      ]
+    ];
+    
+    // 기본 주소로 설정
+    public function setPrimaryAddress($vendorId, $addressId)
+    {
+      $this->db->transStart();
+      
+      // 기존 기본 주소 해제
+      $this->where('vendor_id', $vendorId)
+        ->set('is_primary', 0)
+        ->update();
+      
+      // 새로운 기본 주소 설정
+      $this->update($addressId, ['is_primary' => 1]);
+      
+      $this->db->transComplete();
+      
+      return $this->db->transStatus();
+    }
+  }

+ 62 - 0
backend/app/Models/VendorBusinessAreaModel.php

@@ -0,0 +1,62 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class VendorBusinessAreaModel extends Model
+  {
+    protected $table = 'vendor_business_areas';
+    protected $primaryKey = 'id';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+      'vendor_seq', 'business_area'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'created_at';
+    protected $updatedField = null;
+    
+    protected $validationRules = [
+      'vendor_seq' => 'required|integer|is_not_unique[vendors.id]',
+      'business_area' => 'required|max_length[100]'
+    ];
+    
+    protected $validationMessages = [
+      'vendor_seq' => [
+        'required' => '벤더사 id는 필수입니다.',
+        'is_not_unique' => '존재하지 않는 벤더사입니다.'
+      ],
+      'business_area' => [
+        'required' => '사업 분야는 필수입니다.'
+      ]
+    ];
+    
+    // 벤더사의 사업 분야 일괄 업데이트
+    public function updateBusinessAreas($vendorId, $businessAreas)
+    {
+      $this->db->transStart();
+      
+      // 기존 사업 분야 삭제
+      $this->where('vendor_seq', $vendorId)->delete();
+      
+      // 새로운 사업 분야 추가
+      if (!empty($businessAreas)) {
+        $insertData = [];
+        foreach ($businessAreas as $area) {
+          $insertData[] = [
+            'vendor_seq' => $vendorId,
+            'business_area' => $area
+          ];
+        }
+        $this->insertBatch($insertData);
+      }
+      
+      $this->db->transComplete();
+      
+      return $this->db->transStatus();
+    }
+  }

+ 45 - 0
backend/app/Models/VendorCategoryModel.php

@@ -0,0 +1,45 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class VendorCategoryModel extends Model
+  {
+    protected $table = 'vendor_categories';
+    protected $primaryKey = 'id';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+      'code', 'name_ko', 'name_en', 'description', 'sort_order', 'is_active'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'created_at';
+    protected $updatedField = null;
+    
+    protected $validationRules = [
+      'code' => 'required|max_length[50]|is_unique[vendor_categories.code,id,{id}]',
+      'name_ko' => 'required|max_length[100]',
+      'name_en' => 'permit_empty|max_length[100]',
+      'description' => 'permit_empty|max_length[65535]',
+      'sort_order' => 'permit_empty|integer',
+      'is_active' => 'permit_empty|in_list[0,1]'
+    ];
+    
+    // 활성화된 카테고리만 조회
+    public function getActiveCategories()
+    {
+      return $this->where('is_active', 1)
+        ->orderBy('sort_order', 'ASC')
+        ->findAll();
+    }
+    
+    // 코드로 카테고리 조회
+    public function getCategoryByCode($code)
+    {
+      return $this->where('code', $code)->first();
+    }
+  }

+ 64 - 0
backend/app/Models/VendorContactModel.php

@@ -0,0 +1,64 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class VendorContactModel extends Model
+  {
+    protected $table = 'vendor_contacts';
+    protected $primaryKey = 'id';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = false;
+    
+    protected $allowedFields = [
+      'vendor_id', 'contact_type', 'name', 'position',
+      'phone', 'email', 'is_primary'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'created_at';
+    protected $updatedField = 'updated_at';
+    
+    protected $validationRules = [
+      'vendor_id' => 'required|integer|is_not_unique[vendors.id]',
+      'contact_type' => 'required|in_list[PRIMARY,SECONDARY,BILLING,TECHNICAL]',
+      'name' => 'required|max_length[100]',
+      'position' => 'permit_empty|max_length[100]',
+      'phone' => 'permit_empty|max_length[20]',
+      'email' => 'permit_empty|max_length[255]|valid_email',
+      'is_primary' => 'permit_empty|in_list[0,1]'
+    ];
+    
+    protected $validationMessages = [
+      'vendor_id' => [
+        'required' => '벤더사 id는 필수입니다.',
+        'is_not_unique' => '존재하지 않는 벤더사입니다.'
+      ],
+      'name' => [
+        'required' => '담당자명은 필수입니다.'
+      ],
+      'email' => [
+        'valid_email' => '유효하지 않은 이메일 형식입니다.'
+      ]
+    ];
+    
+    // 기본 담당자로 설정
+    public function setPrimaryContact($vendorId, $contactId)
+    {
+      $this->db->transStart();
+      
+      // 기존 기본 담당자 해제
+      $this->where('vendor_id', $vendorId)
+        ->set('is_primary', 0)
+        ->update();
+      
+      // 새로운 기본 담당자 설정
+      $this->update($contactId, ['is_primary' => 1]);
+      
+      $this->db->transComplete();
+      
+      return $this->db->transStatus();
+    }
+  }

+ 182 - 0
backend/app/Models/VendorInfluencerReapply.php

@@ -0,0 +1,182 @@
+<?php
+  
+  /**
+   * 벤더-인플루언서 재승인요청 API 예제
+   *
+   * 기능: 해지된 파트너십에 대한 재계약 요청 처리
+   * 경로: POST /api/vendor-influencer/reapply-request
+   */
+  
+  namespace App\Controllers;
+  
+  use App\Controllers\BaseController;
+  use App\Models\VendorModel;
+  use App\Models\UserModel;
+  use App\Models\VendorInfluencerMappingModel;
+  use CodeIgniter\HTTP\ResponseInterface;
+  
+  class VendorInfluencerController extends BaseController
+  {
+    protected $vendorModel;
+    protected $userModel;
+    protected $vendorInfluencerModel;
+    
+    public function __construct()
+    {
+      $this->vendorModel = new VendorModel();
+      $this->userModel = new UserModel();
+      $this->vendorInfluencerModel = new VendorInfluencerMappingModel();
+    }
+    
+    /**
+     * 재승인 요청 (해지된 파트너십에 대한 재계약 요청)
+     *
+     * @route POST /api/vendor-influencer/reapply-request
+     * @param int vendorSeq 벤더사 SEQ
+     * @param int influencerSeq 인플루언서 SEQ
+     * @param string requestMessage 요청 메시지
+     * @param int requestedBy 요청자 SEQ (인플루언서)
+     *
+     * @return JSON
+     */
+    public function reapplyRequest()
+    {
+      try {
+        $request = $this->request->getJSON();
+        
+        $vendorSeq = $request->vendorSeq ?? null;
+        $influencerSeq = $request->influencerSeq ?? null;
+        $requestMessage = $request->requestMessage ?? '';
+        $requestedBy = $request->requestedBy ?? null;
+        
+        // 필수 파라미터 검증
+        if (!$vendorSeq || !$influencerSeq || !$requestedBy) {
+          return $this->response->setStatusCode(400)->setJSON([
+            'success' => false,
+            'message' => '필수 파라미터가 누락되었습니다.'
+          ]);
+        }
+        
+        // 기존 해지된 파트너십 확인
+        $terminatedPartnership = $this->vendorInfluencerModel
+          ->where('VENDOR_SEQ', $vendorSeq)
+          ->where('INFLUENCER_SEQ', $influencerSeq)
+          ->where('STATUS', 'TERMINATED')
+          ->where('IS_ACT', 'Y')
+          ->orderBy('REG_DATE', 'DESC')
+          ->first();
+        
+        if (!$terminatedPartnership) {
+          return $this->response->setStatusCode(404)->setJSON([
+            'success' => false,
+            'message' => '해지된 파트너십 기록을 찾을 수 없습니다.'
+          ]);
+        }
+        
+        // 현재 처리 중인 요청이 있는지 확인
+        $existingPendingRequest = $this->vendorInfluencerModel
+          ->where('VENDOR_SEQ', $vendorSeq)
+          ->where('INFLUENCER_SEQ', $influencerSeq)
+          ->where('STATUS', 'PENDING')
+          ->where('IS_ACT', 'Y')
+          ->first();
+        
+        if ($existingPendingRequest) {
+          return $this->response->setStatusCode(409)->setJSON([
+            'success' => false,
+            'message' => '이미 처리 중인 승인 요청이 있습니다.'
+          ]);
+        }
+        
+        // 재승인 요청 생성
+        $reapplyData = [
+          'VENDOR_SEQ' => $vendorSeq,
+          'INFLUENCER_SEQ' => $influencerSeq,
+          'REQUEST_TYPE' => 'INFLUENCER_REQUEST',
+          'STATUS' => 'PENDING',
+          'REQUEST_MESSAGE' => '[재계약 요청] ' . $requestMessage,
+          'REQUESTED_BY' => $requestedBy,
+          'COMMISSION_RATE' => $terminatedPartnership['COMMISSION_RATE'], // 이전 수수료율 유지
+          'SPECIAL_CONDITIONS' => $terminatedPartnership['SPECIAL_CONDITIONS'], // 이전 특별조건 유지
+          'EXPIRED_DATE' => date('Y-m-d H:i:s', strtotime('+7 days')),
+          'ADD_INFO1' => 'REAPPLY', // 재신청 구분자
+          'ADD_INFO2' => $terminatedPartnership['SEQ'], // 이전 파트너십 SEQ 참조
+          'ADD_INFO3' => date('Y-m-d H:i:s') // 재신청 일시
+        ];
+        
+        $insertId = $this->vendorInfluencerModel->insert($reapplyData);
+        
+        // 생성된 재승인 요청 정보 조회
+        $createdReapply = $this->vendorInfluencerModel
+          ->select('vim.*, v.COMPANY_NAME as vendorName, u.NICK_NAME as influencerName, req_user.NICK_NAME as requestedByName')
+          ->from('VENDOR_INFLUENCER_MAPPING vim')
+          ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left')
+          ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left')
+          ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left')
+          ->where('vim.SEQ', $insertId)
+          ->get()
+          ->getRowArray();
+        
+        return $this->response->setJSON([
+          'success' => true,
+          'message' => '재승인 요청이 성공적으로 생성되었습니다.',
+          'data' => [
+            'reapplyRequest' => $createdReapply,
+            'previousPartnership' => $terminatedPartnership
+          ]
+        ]);
+        
+      } catch (\Exception $e) {
+        return $this->response->setStatusCode(500)->setJSON([
+          'success' => false,
+          'message' => '재승인 요청 생성 중 오류가 발생했습니다.',
+          'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null
+        ]);
+      }
+    }
+  }
+  
+  /*
+  사용 예시:
+  
+  POST /api/vendor-influencer/reapply-request
+  Content-Type: application/json
+  
+  {
+      "vendorSeq": 8,
+      "influencerSeq": 15,
+      "requestMessage": "이전 계약이 만료되어 재계약을 요청드립니다. 새로운 프로모션 진행을 위해 파트너십을 재개하고 싶습니다.",
+      "requestedBy": 15
+  }
+  
+  응답 예시:
+  {
+      "success": true,
+      "message": "재승인 요청이 성공적으로 생성되었습니다.",
+      "data": {
+          "reapplyRequest": {
+              "SEQ": 25,
+              "VENDOR_SEQ": 8,
+              "INFLUENCER_SEQ": 15,
+              "REQUEST_TYPE": "INFLUENCER_REQUEST",
+              "STATUS": "PENDING",
+              "REQUEST_MESSAGE": "[재계약 요청] 이전 계약이 만료되어 재계약을 요청드립니다...",
+              "REQUESTED_BY": 15,
+              "COMMISSION_RATE": 10.5,
+              "SPECIAL_CONDITIONS": "월 2회 포스팅",
+              "EXPIRED_DATE": "2024-01-20 10:30:00",
+              "ADD_INFO1": "REAPPLY",
+              "ADD_INFO2": "23",
+              "ADD_INFO3": "2024-01-13 10:30:00",
+              "vendorName": "뷰티코리아",
+              "influencerName": "뷰티블로거",
+              "requestedByName": "뷰티블로거"
+          },
+          "previousPartnership": {
+              "SEQ": 23,
+              "STATUS": "TERMINATED",
+              "PARTNERSHIP_END_DATE": "2024-01-10 15:20:00"
+          }
+      }
+  }
+  */ 

+ 183 - 15
backend/app/Models/VendorInfluencerStatusHistoryModel.php

@@ -34,7 +34,7 @@ class VendorInfluencerStatusHistoryModel extends Model
     protected $validationRules = [
         'MAPPING_SEQ' => 'required|integer',
         'STATUS' => 'required|in_list[PENDING,APPROVED,REJECTED,CANCELLED,EXPIRED,TERMINATED]',
-        'CHANGED_BY' => 'required|integer',
+        'CHANGED_BY' => 'permit_empty|integer',  // required 제거, permit_empty로 변경
         'IS_CURRENT' => 'required|in_list[Y,N]'
     ];
 
@@ -48,8 +48,11 @@ class VendorInfluencerStatusHistoryModel extends Model
             'in_list' => '유효하지 않은 상태입니다.'
         ],
         'CHANGED_BY' => [
-            'required' => '변경자는 필수입니다.',
             'integer' => '변경자 SEQ는 정수여야 합니다.'
+        ],
+        'IS_CURRENT' => [
+            'required' => 'IS_CURRENT는 필수입니다.',
+            'in_list' => 'IS_CURRENT는 Y 또는 N이어야 합니다.'
         ]
     ];
 
@@ -107,7 +110,7 @@ class VendorInfluencerStatusHistoryModel extends Model
     }
 
     /**
-     * 상태 변경 (트랜잭션 포함)
+     * 상태 변경 (트랜잭션 포함) - UNIQUE 제약조건 완전 안전 처리
      */
     public function changeStatus($mappingSeq, $newStatus, $statusMessage = '', $changedBy = null)
     {
@@ -115,30 +118,183 @@ class VendorInfluencerStatusHistoryModel extends Model
         $db->transStart();
 
         try {
-            // 1. 현재 상태 조회
+            log_message('debug', "상태 변경 시작: mappingSeq={$mappingSeq}, newStatus={$newStatus}");
+
+            // CHANGED_BY가 null이면 기본값 설정 (NOT NULL 제약조건 대응)
+            if ($changedBy === null) {
+                $changedBy = 1; // 기본 시스템 사용자
+                log_message('warning', 'CHANGED_BY가 null이므로 기본값 1로 설정');
+            }
+
+            // 1. 현재 상태 조회 (더 상세한 로깅)
             $currentStatus = $this->getCurrentStatus($mappingSeq);
             $previousStatus = $currentStatus ? $currentStatus['STATUS'] : null;
 
-            // 2. 기존 현재 상태를 이전 상태로 변경
-            if ($currentStatus) {
-                $this->update($currentStatus['SEQ'], ['IS_CURRENT' => 'N']);
+            log_message('debug', "현재 상태 조회 결과: " . json_encode($currentStatus));
+            log_message('debug', "이전 상태: " . ($previousStatus ?: 'NULL') . " → 새 상태: {$newStatus}");
+
+            // 2. UNIQUE 제약조건 완전 방지 - 강력한 중복 제거
+            log_message('debug', "UNIQUE 제약조건 방지: mappingSeq={$mappingSeq}에 대한 모든 IS_CURRENT='Y' 처리");
+            
+            // 2-1. 현재 상태 개수 확인
+            $currentCount = $this->where('MAPPING_SEQ', $mappingSeq)
+                                ->where('IS_CURRENT', 'Y')
+                                ->countAllResults();
+            
+            log_message('debug', "기존 IS_CURRENT='Y' 개수: {$currentCount}");
+
+            // 2-2. 강력한 UPDATE로 모든 현재 상태를 비활성화 (히스토리 보존)
+            if ($currentCount > 0) {
+                // 여러 번 시도하여 확실하게 업데이트
+                for ($attempt = 1; $attempt <= 3; $attempt++) {
+                    log_message('debug', "현재 상태 비활성화 시도 #{$attempt}");
+                    
+                    $updateBuilder = $this->builder();
+                    $updateCount = $updateBuilder->where('MAPPING_SEQ', $mappingSeq)
+                                                 ->where('IS_CURRENT', 'Y')
+                                                 ->update(['IS_CURRENT' => 'N']);
+                    
+                    log_message('debug', "업데이트 시도 #{$attempt}: 업데이트된 행 수={$updateCount}");
+                    
+                    // 업데이트 후 확인
+                    $remainingCount = $this->where('MAPPING_SEQ', $mappingSeq)
+                                          ->where('IS_CURRENT', 'Y')
+                                          ->countAllResults();
+                    
+                    log_message('debug', "업데이트 후 남은 IS_CURRENT='Y' 개수: {$remainingCount}");
+                    
+                    if ($remainingCount === 0) {
+                        log_message('debug', "모든 현재 상태 비활성화 완료 (시도 #{$attempt})");
+                        break;
+                    }
+                    
+                    if ($attempt === 3) {
+                        log_message('error', "3번 시도 후에도 현재 상태가 남아있음: {$remainingCount}개");
+                        throw new \Exception("기존 현재 상태 비활성화 실패: {$remainingCount}개 남음");
+                    }
+                    
+                    // 잠시 대기 후 재시도 (동시성 문제 대응)
+                    usleep(10000); // 10ms 대기
+                }
+            }
+
+            // 2-3. 최종 안전 확인: UNIQUE 제약조건 위반 방지
+            $finalCheck = $this->where('MAPPING_SEQ', $mappingSeq)
+                              ->where('IS_CURRENT', 'Y')
+                              ->countAllResults();
+                              
+            if ($finalCheck > 0) {
+                log_message('error', "최종 체크에서 중복 발견: {$finalCheck}개");
+                
+                // 마지막 시도: 직접 SQL 실행
+                $sql = "UPDATE VENDOR_INFLUENCER_STATUS_HISTORY SET IS_CURRENT = 'N' WHERE MAPPING_SEQ = ? AND IS_CURRENT = 'Y'";
+                $this->db->query($sql, [$mappingSeq]);
+                
+                // 다시 확인
+                $finalFinalCheck = $this->where('MAPPING_SEQ', $mappingSeq)
+                                       ->where('IS_CURRENT', 'Y')
+                                       ->countAllResults();
+                
+                if ($finalFinalCheck > 0) {
+                    throw new \Exception("UNIQUE 제약조건 위반 방지 실패: {$finalFinalCheck}개의 현재 상태 존재");
+                }
+                
+                log_message('debug', "직접 SQL로 현재 상태 정리 완료");
             }
 
-            // 3. 새로운 상태 히스토리 추가
+            log_message('debug', "UNIQUE 제약조건 완전 클리어 완료");
+
+            // 3. 새로운 상태 히스토리 추가 (이제 안전함)
             $historyData = [
-                'MAPPING_SEQ' => $mappingSeq,
+                'MAPPING_SEQ' => (int)$mappingSeq,
                 'STATUS' => $newStatus,
                 'PREVIOUS_STATUS' => $previousStatus,
-                'STATUS_MESSAGE' => $statusMessage,
-                'CHANGED_BY' => $changedBy,
-                'IS_CURRENT' => 'Y'
+                'STATUS_MESSAGE' => $statusMessage ?: '',
+                'CHANGED_BY' => (int)$changedBy,
+                'IS_CURRENT' => 'Y',
+                'CHANGED_DATE' => date('Y-m-d H:i:s'),
+                'REG_DATE' => date('Y-m-d H:i:s')
             ];
 
-            $result = $this->insert($historyData);
+            log_message('debug', '히스토리 데이터: ' . json_encode($historyData));
+
+            // validation 체크
+            $this->skipValidation = false;
+            
+            if (!$this->validate($historyData)) {
+                $validationErrors = $this->errors();
+                log_message('error', 'Validation 실패: ' . json_encode($validationErrors));
+                throw new \Exception('Validation 오류: ' . implode(', ', $validationErrors));
+            }
+
+            // validation을 통과했으므로 이제 insert
+            $this->skipValidation = true;
+            
+            // UNIQUE 제약조건 오류에 대한 추가 방어: 재시도 로직
+            $insertSuccess = false;
+            for ($insertAttempt = 1; $insertAttempt <= 2; $insertAttempt++) {
+                try {
+                    log_message('debug', "Insert 시도 #{$insertAttempt}");
+                    
+                    $result = $this->insert($historyData, false);
+                    
+                    if ($result) {
+                        $insertSuccess = true;
+                        log_message('debug', "Insert 성공 (시도 #{$insertAttempt}): ID={$result}");
+                        break;
+                    } else {
+                        log_message('warning', "Insert 시도 #{$insertAttempt} 실패: result=false");
+                    }
+                    
+                } catch (\Exception $insertError) {
+                    log_message('error', "Insert 시도 #{$insertAttempt} 예외: " . $insertError->getMessage());
+                    
+                    // UNIQUE 제약조건 오류인 경우 재정리 후 재시도
+                    if (strpos($insertError->getMessage(), 'Duplicate entry') !== false && 
+                        strpos($insertError->getMessage(), 'unique_current_mapping') !== false) {
+                        
+                        log_message('warning', "UNIQUE 제약조건 오류 감지 - 재정리 후 재시도");
+                        
+                        // 응급 정리: 다시 한번 현재 상태 비활성화
+                        $emergencyCleanup = $this->builder();
+                        $emergencyCleanup->where('MAPPING_SEQ', $mappingSeq)
+                                        ->where('IS_CURRENT', 'Y')
+                                        ->update(['IS_CURRENT' => 'N']);
+                        
+                        // 잠시 대기
+                        usleep(50000); // 50ms 대기
+                        
+                        if ($insertAttempt === 2) {
+                            throw $insertError; // 마지막 시도에서도 실패하면 예외 던지기
+                        }
+                    } else {
+                        throw $insertError; // 다른 오류는 즉시 던지기
+                    }
+                }
+            }
+            
+            $this->skipValidation = false;
+            
+            if (!$insertSuccess || !$result) {
+                $dbError = $this->db->error();
+                log_message('error', 'DB Insert 최종 실패: ' . json_encode($dbError));
+                log_message('error', 'Insert 데이터: ' . json_encode($historyData));
+                throw new \Exception('DB Insert 오류: ' . ($dbError['message'] ?? 'Unknown DB error'));
+            }
+
+            log_message('debug', "새 히스토리 추가 완료: ID={$result}");
 
             // 4. 메인 테이블의 MOD_DATE 업데이트
-            $mappingModel = new VendorInfluencerMappingModel();
-            $mappingModel->update($mappingSeq, ['MOD_DATE' => date('Y-m-d H:i:s')]);
+            try {
+                $mappingModel = new VendorInfluencerMappingModel();
+                $mainUpdateResult = $mappingModel->update($mappingSeq, ['MOD_DATE' => date('Y-m-d H:i:s')]);
+                
+                if (!$mainUpdateResult) {
+                    log_message('warning', 'VENDOR_INFLUENCER_MAPPING 테이블 MOD_DATE 업데이트 실패 (비중요)');
+                }
+            } catch (\Exception $mainUpdateError) {
+                log_message('warning', '메인 테이블 업데이트 실패 (계속 진행): ' . $mainUpdateError->getMessage());
+            }
 
             $db->transComplete();
 
@@ -146,11 +302,23 @@ class VendorInfluencerStatusHistoryModel extends Model
                 throw new \Exception('상태 변경 트랜잭션 실패');
             }
 
+            log_message('debug', "상태 변경 트랜잭션 완료: mappingSeq={$mappingSeq}");
+
             return $result;
 
         } catch (\Exception $e) {
             $db->transRollback();
             log_message('error', '상태 변경 실패: ' . $e->getMessage());
+            log_message('error', '상태 변경 스택 트레이스: ' . $e->getTraceAsString());
+            
+            // 상세한 디버그 정보 추가
+            log_message('error', '실패한 파라미터: ' . json_encode([
+                'mappingSeq' => $mappingSeq,
+                'newStatus' => $newStatus,
+                'statusMessage' => $statusMessage,
+                'changedBy' => $changedBy
+            ]));
+
             throw $e;
         }
     }

+ 1 - 1
backend/app/Models/VendorModel.php

@@ -121,7 +121,7 @@ class VendorModel extends Model
         
         // 페이징
         $offset = ($page - 1) * $perPage;
-        return $builder->limit($perPage, $offset)->findAll();
+        return $builder->limit($perPage, $offset)->get()->getResultArray();
     }
     
     /**

+ 34 - 0
backend/app/Models/VendorPartnershipModel.php

@@ -58,6 +58,40 @@ class VendorPartnershipModel extends Model
         $this->mappingModel = new VendorInfluencerMappingModel();
     }
     
+    /**
+     * 벤더사의 인플루언서 요청 목록 조회 (페이지네이션 포함)
+     */
+    public function getVendorRequestsWithPagination($vendorSeq, $page = 1, $size = 20, $status = null)
+    {
+        $filters = [];
+        if ($status) {
+            $filters['status'] = $status;
+        }
+
+        $builder = $this->getVendorRequests($vendorSeq, $filters);
+        
+        // 전체 개수 계산
+        $totalBuilder = clone $builder;
+        $total = $totalBuilder->countAllResults();
+        
+        // 페이지네이션 적용
+        $offset = ($page - 1) * $size;
+        $builder->limit($size, $offset);
+        
+        $data = $builder->get()->getResultArray();
+        
+        return [
+            'data' => $data,
+            'pagination' => [
+                'total' => $total,
+                'currentPage' => $page,
+                'totalPages' => ceil($total / $size),
+                'limit' => $size,
+                'offset' => $offset
+            ]
+        ];
+    }
+
     /**
      * 벤더사의 인플루언서 요청 목록 조회
      */

+ 62 - 0
backend/app/Models/VendorProductModel.php

@@ -0,0 +1,62 @@
+<?php
+  
+  namespace App\Models;
+  
+  use CodeIgniter\Model;
+  
+  class VendorProductModel extends Model
+  {
+    protected $table = 'vendor_products';
+    protected $primaryKey = 'id';
+    protected $useAutoIncrement = true;
+    protected $returnType = 'array';
+    protected $useSoftDeletes = true;
+    
+    protected $allowedFields = [
+      'vendor_id', 'name', 'description', 'category',
+      'price', 'image_url', 'is_featured', 'status'
+    ];
+    
+    protected $useTimestamps = true;
+    protected $createdField = 'created_at';
+    protected $updatedField = 'updated_at';
+    protected $deletedField = 'deleted_at';
+    
+    protected $validationRules = [
+      'vendor_id' => 'required|integer|is_not_unique[vendors.id]',
+      'name' => 'required|max_length[255]',
+      'description' => 'permit_empty|max_length[65535]',
+      'category' => 'permit_empty|max_length[100]',
+      'price' => 'permit_empty|decimal|greater_than_equal_to[0]',
+      'image_url' => 'permit_empty|max_length[500]|valid_url',
+      'is_featured' => 'permit_empty|in_list[0,1]',
+      'status' => 'required|in_list[ACTIVE,INACTIVE,DISCONTINUED]'
+    ];
+    
+    protected $validationMessages = [
+      'vendor_id' => [
+        'required' => '벤더사 id는 필수입니다.',
+        'is_not_unique' => '존재하지 않는 벤더사입니다.'
+      ],
+      'name' => [
+        'required' => '제품명은 필수입니다.'
+      ],
+      'price' => [
+        'greater_than_equal_to' => '가격은 0 이상이어야 합니다.'
+      ],
+      'image_url' => [
+        'valid_url' => '유효하지 않은 이미지 URL입니다.'
+      ]
+    ];
+    
+    // 벤더사별 주요 제품 조회
+    public function getFeaturedProducts($vendorId, $limit = 5)
+    {
+      return $this->where('vendor_id', $vendorId)
+        ->where('status', 'ACTIVE')
+        ->where('is_featured', 1)
+        ->limit($limit)
+        ->orderBy('created_at', 'DESC')
+        ->findAll();
+    }
+  }

+ 42 - 0
ddl/012_add_rating_column.sql

@@ -0,0 +1,42 @@
+-- 012_add_rating_column.sql
+-- USER_LIST 테이블에 RATING 컬럼 추가 (MariaDB 호환)
+
+-- RATING 컬럼이 이미 존재하는지 확인 후 추가
+SET @sql = (
+    SELECT IF(
+        (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+         WHERE TABLE_SCHEMA = DATABASE() 
+         AND TABLE_NAME = 'USER_LIST' 
+         AND COLUMN_NAME = 'RATING') = 0,
+        'ALTER TABLE USER_LIST ADD COLUMN RATING DECIMAL(3,1) DEFAULT 0.0 COMMENT "사용자 평점 (0.0~5.0)"',
+        'SELECT "RATING 컬럼이 이미 존재합니다" as message'
+    )
+);
+
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- RATING 컬럼 인덱스 생성 (존재하지 않을 경우에만)
+SET @sql = (
+    SELECT IF(
+        (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+         WHERE TABLE_SCHEMA = DATABASE() 
+         AND TABLE_NAME = 'USER_LIST' 
+         AND INDEX_NAME = 'idx_user_rating') = 0,
+        'CREATE INDEX idx_user_rating ON USER_LIST (RATING DESC)',
+        'SELECT "idx_user_rating 인덱스가 이미 존재합니다" as message'
+    )
+);
+
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- RATING 기본값 업데이트 (NULL인 경우에만)
+UPDATE USER_LIST 
+SET RATING = 0.0 
+WHERE RATING IS NULL;
+
+-- 성공 메시지
+SELECT 'USER_LIST 테이블 RATING 컬럼 추가 완료' as result; 

+ 54 - 0
ddl/012_add_rating_column_fixed.sql

@@ -0,0 +1,54 @@
+-- 012_add_rating_column_fixed.sql
+-- USER_LIST 테이블에 RATING 컬럼 추가 (MariaDB 호환, Prepared Statement 충돌 방지)
+
+-- 1. RATING 컬럼이 이미 존재하는지 확인 후 추가
+SET @sql_column = (
+    SELECT IF(
+        (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS 
+         WHERE TABLE_SCHEMA = DATABASE() 
+         AND TABLE_NAME = 'USER_LIST' 
+         AND COLUMN_NAME = 'RATING') = 0,
+        'ALTER TABLE USER_LIST ADD COLUMN RATING DECIMAL(3,1) DEFAULT 0.0 COMMENT "사용자 평점 (0.0~5.0)"',
+        'SELECT "RATING 컬럼이 이미 존재합니다" as message'
+    )
+);
+
+PREPARE stmt_column FROM @sql_column;
+EXECUTE stmt_column;
+DEALLOCATE PREPARE stmt_column;
+
+-- 2. RATING 컬럼 인덱스 생성 (존재하지 않을 경우에만)
+SET @sql_index = (
+    SELECT IF(
+        (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS 
+         WHERE TABLE_SCHEMA = DATABASE() 
+         AND TABLE_NAME = 'USER_LIST' 
+         AND INDEX_NAME = 'idx_user_rating') = 0,
+        'CREATE INDEX idx_user_rating ON USER_LIST (RATING DESC)',
+        'SELECT "idx_user_rating 인덱스가 이미 존재합니다" as message'
+    )
+);
+
+PREPARE stmt_index FROM @sql_index;
+EXECUTE stmt_index;
+DEALLOCATE PREPARE stmt_index;
+
+-- 3. RATING 기본값 업데이트 (NULL인 경우에만)
+UPDATE USER_LIST 
+SET RATING = 0.0 
+WHERE RATING IS NULL;
+
+-- 4. 성공 메시지
+SELECT 'USER_LIST 테이블 RATING 컬럼 추가 완료' as result;
+
+-- 5. 검증: 컬럼 생성 확인
+SELECT 
+    COLUMN_NAME, 
+    DATA_TYPE, 
+    IS_NULLABLE, 
+    COLUMN_DEFAULT,
+    COLUMN_COMMENT
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_SCHEMA = DATABASE() 
+AND TABLE_NAME = 'USER_LIST' 
+AND COLUMN_NAME = 'RATING'; 

+ 51 - 0
ddl/012_add_rating_column_simple.sql

@@ -0,0 +1,51 @@
+-- 012_add_rating_column_simple.sql
+-- USER_LIST 테이블에 RATING 컬럼 추가 (MariaDB 호환, 간단한 방식)
+
+-- 1. 기존 RATING 컬럼 확인
+SELECT 
+    CASE 
+        WHEN COUNT(*) > 0 THEN 'RATING 컬럼이 이미 존재합니다'
+        ELSE 'RATING 컬럼을 추가합니다'
+    END as status
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_SCHEMA = DATABASE() 
+AND TABLE_NAME = 'USER_LIST' 
+AND COLUMN_NAME = 'RATING';
+
+-- 2. RATING 컬럼 추가 (존재하지 않으면 추가됨, 존재하면 오류 발생하지만 무시 가능)
+-- 이 명령은 컬럼이 이미 존재하면 오류가 발생합니다. 이는 정상적인 동작입니다.
+ALTER TABLE USER_LIST 
+ADD COLUMN RATING DECIMAL(3,1) DEFAULT 0.0 COMMENT '사용자 평점 (0.0~5.0)';
+
+-- 3. 인덱스 추가 (존재하지 않으면 추가됨)
+-- 이 명령도 인덱스가 이미 존재하면 오류가 발생합니다. 이는 정상적인 동작입니다.
+CREATE INDEX idx_user_rating ON USER_LIST (RATING DESC);
+
+-- 4. RATING 기본값 업데이트 (NULL인 경우에만)
+UPDATE USER_LIST 
+SET RATING = 0.0 
+WHERE RATING IS NULL;
+
+-- 5. 최종 검증
+SELECT 
+    COLUMN_NAME, 
+    DATA_TYPE, 
+    IS_NULLABLE, 
+    COLUMN_DEFAULT,
+    COLUMN_COMMENT
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_SCHEMA = DATABASE() 
+AND TABLE_NAME = 'USER_LIST' 
+AND COLUMN_NAME = 'RATING';
+
+-- 6. 인덱스 확인
+SELECT 
+    INDEX_NAME,
+    COLUMN_NAME,
+    SEQ_IN_INDEX
+FROM INFORMATION_SCHEMA.STATISTICS 
+WHERE TABLE_SCHEMA = DATABASE() 
+AND TABLE_NAME = 'USER_LIST' 
+AND INDEX_NAME = 'idx_user_rating';
+
+SELECT '🎉 USER_LIST 테이블 RATING 컬럼 추가 완료!' as result; 

+ 109 - 105
ddl/README.md

@@ -1,114 +1,118 @@
-# DDL 실행 순서 가이드
-
-벤더사-인플루언서 시스템의 데이터베이스 스키마를 구축하기 위한 DDL 파일들의 실행 순서입니다.
-
-## 실행 순서
-
-### 1. 001_create_vendor_influencer_mapping_table.sql
-- **목적**: 벤더사-인플루언서 승인 매핑 테이블 생성
-- **의존성**: VENDOR_LIST, USER_LIST 테이블이 이미 존재해야 함
-- **설명**: 벤더사와 인플루언서 간의 승인 요청 및 파트너십 관리를 위한 핵심 테이블
-
-### 2. 002_add_vendor_influencer_mapping_indexes.sql
-- **목적**: 성능 최적화를 위한 인덱스 추가
-- **의존성**: 001번 DDL 실행 완료
-- **포함 인덱스**:
-  - 기본 검색용 인덱스 (VENDOR_SEQ, INFLUENCER_SEQ, STATUS 등)
-  - 복합 인덱스 (성능 최적화)
-  - 유니크 인덱스 (중복 방지)
-
-### 3. 003_add_vendor_influencer_mapping_foreign_keys.sql
-- **목적**: 데이터 무결성을 위한 외래키 제약 조건 추가
-- **의존성**: 001, 002번 DDL 실행 완료
-- **외래키 관계**:
-  - VENDOR_LIST와의 관계
-  - USER_LIST와의 관계 (인플루언서, 요청자, 승인자)
-
-### 4. 004_add_vendor_list_additional_columns.sql
-- **목적**: VENDOR_LIST 테이블에 검색 및 분류를 위한 컬럼 추가
-- **의존성**: 기존 VENDOR_LIST 테이블 존재
-- **추가 컬럼**:
-  - CATEGORY (사업 카테고리)
-  - REGION (지역)
-  - DESCRIPTION (설명)
-  - LOGO (로고 URL)
-  - TAGS (검색 태그)
-  - APPROVAL_STATUS (승인 상태)
-  - 기타 관리용 컬럼들
-
-### 5. 005_add_user_list_additional_columns.sql
-- **목적**: USER_LIST 테이블에 인플루언서 정보를 위한 컬럼 추가
-- **의존성**: 기존 USER_LIST 테이블 존재
-- **추가 컬럼**:
-  - INFLUENCER_TYPE (인플루언서 타입)
-  - PRIMARY_CATEGORY (주요 활동 카테고리)
-  - FOLLOWER_COUNT (팔로워 수)
-  - 소셜미디어 링크들
-  - 프로필 정보
-  - 인증 관련 컬럼들
-
-### 6. 006_create_partnership_history_table.sql
-- **목적**: 파트너십 활동 이력 추적을 위한 테이블 생성
-- **의존성**: 001~005번 DDL 실행 완료
-- **기능**: 모든 파트너십 관련 액션을 로깅하여 감사 추적 가능
-
-### 7. 007_create_notification_table.sql
-- **목적**: 알림 시스템을 위한 테이블 생성
-- **의존성**: 001~006번 DDL 실행 완료
-- **기능**: 파트너십 관련 알림 및 시스템 공지사항 관리
-
-### 8. 008_create_sample_data_inserts.sql
-- **목적**: 테스트 및 개발을 위한 샘플 데이터 삽입
-- **의존성**: 001~007번 DDL 실행 완료
-- **포함 데이터**:
-  - 벤더사 샘플 데이터
-  - 인플루언서 샘플 데이터
-  - 매핑 관계 샘플 데이터
-  - 이력 및 알림 샘플 데이터
-
-### 9. 009_add_vendor_list_indexes.sql
-- **목적**: VENDOR_LIST 테이블 검색 최적화 인덱스 추가
-- **의존성**: 004번 DDL 실행 완료 (컬럼 추가 후)
-- **포함 인덱스**: 카테고리, 지역, 승인상태 등
-
-### 10. 010_add_user_list_indexes.sql
-- **목적**: USER_LIST 테이블 검색 최적화 인덱스 추가
-- **의존성**: 005번 DDL 실행 완료 (컬럼 추가 후)
-- **포함 인덱스**: 인플루언서타입, 카테고리, 인증상태 등
-
-## 실행 방법
+# DDL 스크립트 실행 가이드
 
+## 📋 스크립트 실행 순서
+
+**중요:** 반드시 순서대로 실행해주세요.
+
+### 기본 테이블 생성
+```bash
+# 1. 기본 매핑 테이블 생성
+mysql -h [HOST] -u [USER] -p [DATABASE] < 001_create_vendor_influencer_mapping_table.sql
+
+# 2. 인덱스 생성
+mysql -h [HOST] -u [USER] -p [DATABASE] < 002_add_vendor_influencer_mapping_indexes.sql
+
+# 3. 외래키 설정
+mysql -h [HOST] -u [USER] -p [DATABASE] < 003_add_vendor_influencer_mapping_foreign_keys.sql
+```
+
+### 스키마 수정
+```bash
+# 4. 외래키 제거
+mysql -h [HOST] -u [USER] -p [DATABASE] < 004_remove_approved_by_foreign_key.sql
+
+# 5. 유니크 제약조건 수정
+mysql -h [HOST] -u [USER] -p [DATABASE] < 006_fix_unique_constraint_fundamental.sql
+
+# 6. 히스토리 테이블 생성
+mysql -h [HOST] -u [USER] -p [DATABASE] < 007_create_status_history_table.sql
+```
+
+### 데이터 정리
 ```bash
-# MySQL 명령어로 순차 실행
-mysql -u [username] -p [database_name] < ddl/001_create_vendor_influencer_mapping_table.sql
-mysql -u [username] -p [database_name] < ddl/002_add_vendor_influencer_mapping_indexes.sql
-mysql -u [username] -p [database_name] < ddl/003_add_vendor_influencer_mapping_foreign_keys.sql
-mysql -u [username] -p [database_name] < ddl/004_add_vendor_list_additional_columns.sql
-mysql -u [username] -p [database_name] < ddl/005_add_user_list_additional_columns.sql
-mysql -u [username] -p [database_name] < ddl/006_create_partnership_history_table.sql
-mysql -u [username] -p [database_name] < ddl/007_create_notification_table.sql
-mysql -u [username] -p [database_name] < ddl/008_create_sample_data_inserts.sql
-mysql -u [username] -p [database_name] < ddl/009_add_vendor_list_indexes.sql
-mysql -u [username] -p [database_name] < ddl/010_add_user_list_indexes.sql
+# 7. 데이터 정리 및 스키마 최적화
+mysql -h [HOST] -u [USER] -p [DATABASE] < 008_clear_data_and_drop_status.sql
+
+# 8. 안전한 데이터 정리
+mysql -h [HOST] -u [USER] -p [DATABASE] < 009_safe_truncate_with_fk.sql
 ```
 
-## 주의사항
+### MariaDB 호환성
+```bash
+# 9. MariaDB 호환 스크립트
+mysql -h [HOST] -u [USER] -p [DATABASE] < 010_mariadb_compatible.sql
+
+# 10. MariaDB 동적 SQL
+mysql -h [HOST] -u [USER] -p [DATABASE] < 011_mariadb_safe_dynamic.sql
 
-1. **순서 중요**: 반드시 번호 순서대로 실행해야 합니다.
-2. **의존성 확인**: 각 DDL 파일의 전제조건을 확인하세요.
-   - 009번은 004번 실행 후
-   - 010번은 005번 실행 후
-3. **백업**: 프로덕션 환경에서는 실행 전 반드시 데이터베이스 백업을 수행하세요.
-4. **권한**: DDL 실행을 위한 적절한 데이터베이스 권한이 필요합니다.
-5. **테스트**: 개발 환경에서 먼저 테스트 후 프로덕션에 적용하세요.
-6. **샘플 데이터**: 008번 파일의 샘플 데이터는 개발/테스트 환경에서만 사용하세요.
-7. **인덱스 중복**: 기존에 동일한 이름의 인덱스가 있으면 에러가 발생할 수 있습니다.
+# 11. RATING 컬럼 추가 ✨ **신규 추가**
+mysql -h [HOST] -u [USER] -p [DATABASE] < 012_add_rating_column.sql
+```
 
-## 롤백 방법
+## 🚨 주의사항
 
-각 DDL 파일에 대응하는 롤백 스크립트가 필요한 경우, 역순으로 DROP TABLE, DROP INDEX, DROP CONSTRAINT 등을 실행해야 합니다.
+### 실행 전 확인사항
+1. 데이터베이스 백업 완료
+2. 충분한 권한 확보 (ALTER, CREATE, DROP 권한 필요)
+3. 운영 시간대 피해서 실행
+
+### 실행 후 확인사항
+1. 테이블 구조 확인: `DESC VENDOR_INFLUENCER_MAPPING;`
+2. 인덱스 확인: `SHOW INDEX FROM VENDOR_INFLUENCER_MAPPING;`
+3. 히스토리 테이블 확인: `DESC VENDOR_INFLUENCER_STATUS_HISTORY;`
+4. USER_LIST 테이블 RATING 컬럼 확인: `DESC USER_LIST;`
+
+## 📁 스크립트 설명
+
+| 파일명 | 목적 | 상태 |
+|--------|------|------|
+| `001_create_vendor_influencer_mapping_table.sql` | 기본 매핑 테이블 생성 | ✅ |
+| `002_add_vendor_influencer_mapping_indexes.sql` | 성능 최적화 인덱스 | ✅ |
+| `003_add_vendor_influencer_mapping_foreign_keys.sql` | 데이터 무결성 외래키 | ✅ |
+| `004_remove_approved_by_foreign_key.sql` | 외래키 제거 | ✅ |
+| `006_fix_unique_constraint_fundamental.sql` | 유니크 제약조건 수정 | ✅ |
+| `007_create_status_history_table.sql` | 상태 이력 테이블 생성 | ✅ |
+| `008_clear_data_and_drop_status.sql` | 데이터 정리 | ✅ |
+| `009_safe_truncate_with_fk.sql` | 안전한 데이터 정리 | ✅ |
+| `010_mariadb_compatible.sql` | MariaDB 호환성 | ✅ |
+| `011_mariadb_safe_dynamic.sql` | MariaDB 동적 SQL | ✅ |
+| `012_add_rating_column.sql` | USER_LIST RATING 컬럼 추가 | ✨ **신규** |
+
+## 🔄 롤백 방법
+
+문제 발생 시 역순으로 롤백:
+
+```bash
+# RATING 컬럼 제거
+ALTER TABLE USER_LIST DROP COLUMN RATING;
+DROP INDEX idx_user_rating ON USER_LIST;
+
+# 기타 테이블 롤백은 기존 문서 참조
+```
+
+## ✅ 검증 명령어
+
+```sql
+-- 1. 테이블 존재 확인
+SHOW TABLES LIKE '%VENDOR_INFLUENCER%';
+
+-- 2. 컬럼 구조 확인
+DESC VENDOR_INFLUENCER_MAPPING;
+DESC VENDOR_INFLUENCER_STATUS_HISTORY;
+DESC USER_LIST;
+
+-- 3. RATING 컬럼 확인
+SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT 
+FROM INFORMATION_SCHEMA.COLUMNS 
+WHERE TABLE_NAME = 'USER_LIST' AND COLUMN_NAME = 'RATING';
+
+-- 4. 인덱스 확인
+SHOW INDEX FROM VENDOR_INFLUENCER_MAPPING;
+SHOW INDEX FROM USER_LIST;
+```
 
-## 버전 관리
+---
 
-- 각 DDL 파일의 상단에 생성일과 목적이 명시되어 있습니다.
-- 변경사항이 있을 경우 새로운 번호의 DDL 파일을 생성하여 관리합니다.
+**최종 업데이트:** 2024-12-22  
+**총 스크립트 수:** 11개  
+**실행 예상 시간:** 5-10분

+ 177 - 0
md/2024-12-22-백엔드-프론트엔드-완전통합-가이드.md

@@ -0,0 +1,177 @@
+# 백엔드-프론트엔드 완전 통합 가이드
+
+## 📋 개요
+
+프론트엔드 페이지 분석을 통해 백엔드 API 구조를 완전히 프론트엔드 요구사항에 맞게 수정하는 작업입니다.
+
+## 🔍 발견된 문제점
+
+### 1. 데이터베이스 스키마 문제
+- `USER_LIST` 테이블에 `RATING` 컬럼이 없어 SQL 오류 발생
+- 여러 모델에서 `u.RATING` 필드를 참조하고 있음
+
+### 2. API 엔드포인트 불일치
+- 프론트엔드: `/api/vendor-influencer/approve` 호출
+- 백엔드: 해당 엔드포인트 없음 (기존에는 `process-request` 사용)
+
+### 3. 응답 데이터 구조 불일치
+- 프론트엔드에서 기대하는 응답 구조와 백엔드 실제 응답 구조 차이
+
+## 🛠️ 작업 순서
+
+### 1단계: 데이터베이스 스키마 수정 ⚠️ **먼저 실행 필요**
+
+```sql
+-- ddl/012_add_rating_column.sql 실행
+-- USER_LIST 테이블에 RATING 컬럼 추가
+```
+
+**실행 명령:**
+```bash
+mysql -h [DB_HOST] -u [DB_USER] -p [DB_NAME] < ddl/012_add_rating_column.sql
+```
+
+**확인 방법:**
+```sql
+DESC USER_LIST;
+-- RATING DECIMAL(3,1) DEFAULT 0.0 컬럼이 있는지 확인
+```
+
+### 2단계: 백엔드 API 구조 수정 ✅ **완료됨**
+
+#### 2-1. 라우팅 추가
+- [x] `backend/app/Config/Routes.php`에 `/api/vendor-influencer/approve` 엔드포인트 추가
+- [x] 기존 라우팅 정리 및 그룹화
+
+#### 2-2. 컨트롤러 메서드 추가
+- [x] `VendorController::approveInfluencerRequest()` 메서드 추가
+- [x] 프론트엔드 파라미터 형식에 맞춤 (`action`: 'APPROVE'/'REJECT')
+
+### 3단계: 테스트 및 검증
+
+#### 3-1. 기본 기능 테스트
+```bash
+# 1. 벤더사 로그인 후 인플루언서 요청 목록 확인
+curl -X POST http://localhost:8080/api/vendor-influencer/requests \
+  -H "Content-Type: application/json" \
+  -d '{"vendorSeq": 1, "page": 1, "size": 20}'
+
+# 2. 승인 처리 테스트
+curl -X POST http://localhost:8080/api/vendor-influencer/approve \
+  -H "Content-Type: application/json" \
+  -d '{
+    "mappingSeq": 1,
+    "action": "APPROVE",
+    "processedBy": 1,
+    "responseMessage": "승인합니다"
+  }'
+```
+
+#### 3-2. 프론트엔드 페이지 테스트
+1. **벤더사 대시보드 테스트**
+   - URL: `http://localhost:3000/view/vendor/dashboard/influencer-requests`
+   - 확인사항: 
+     - 인플루언서 요청 목록 로딩
+     - 통계 카드 표시
+     - 승인/거부 버튼 동작
+
+2. **인플루언서 검색 페이지 테스트**
+   - URL: `http://localhost:3000/view/influencer/search`
+   - 확인사항:
+     - 벤더사 목록 로딩
+     - 승인요청 기능
+     - 재승인요청 기능
+
+## 📁 수정된 파일 목록
+
+### DDL 스크립트
+- `ddl/012_add_rating_column.sql` ✨ **신규생성**
+
+### 백엔드 파일
+- `backend/app/Config/Routes.php` ✏️ **수정완료**
+- `backend/app/Controllers/VendorController.php` ✏️ **수정완료**
+
+### 프론트엔드 파일 (이미 존재)
+- `pages/view/vendor/dashboard/influencer-requests.vue`
+- `pages/view/vendor/dashboard/index.vue`
+- `pages/view/influencer/search.vue`
+- `pages/view/influencer/[id].vue`
+
+## 🚨 주의사항
+
+### 1. 데이터베이스 작업 순서
+**반드시 DDL 스크립트를 먼저 실행한 후 백엔드 서버를 재시작하세요.**
+
+```bash
+# 1. DDL 실행
+mysql -h [HOST] -u [USER] -p [DATABASE] < ddl/012_add_rating_column.sql
+
+# 2. 백엔드 서버 재시작
+cd backend
+php spark serve --host=0.0.0.0 --port=8080
+```
+
+### 2. 기존 기능 안전성 보장
+- 기존 API 엔드포인트는 그대로 유지
+- 새로운 엔드포인트 추가 방식으로 구현
+- 호환성 라우팅을 통해 점진적 이전 가능
+
+### 3. 에러 처리 강화
+- 모든 API에서 상세한 에러 로깅
+- 프론트엔드 친화적인 에러 메시지
+- 상태 코드 표준화
+
+## 🔄 롤백 방법
+
+만약 문제가 발생할 경우:
+
+### 데이터베이스 롤백
+```sql
+-- RATING 컬럼 제거 (필요시)
+ALTER TABLE USER_LIST DROP COLUMN RATING;
+DROP INDEX idx_user_rating ON USER_LIST;
+```
+
+### 백엔드 롤백
+```bash
+git checkout HEAD~1 backend/app/Config/Routes.php
+git checkout HEAD~1 backend/app/Controllers/VendorController.php
+```
+
+## ✅ 검증 체크리스트
+
+### 필수 확인사항
+- [ ] DDL 스크립트 실행 완료
+- [ ] 백엔드 서버 정상 시작
+- [ ] 프론트엔드 컴파일 오류 없음
+- [ ] 벤더사 대시보드 정상 로딩
+- [ ] 인플루언서 검색 페이지 정상 로딩
+- [ ] 승인/거부 기능 정상 동작
+
+### 성능 확인사항
+- [ ] API 응답 시간 300ms 이하
+- [ ] 대량 데이터 처리 성능 이상 없음
+- [ ] 메모리 사용량 급증 없음
+
+## 📞 문제 발생 시 대응
+
+1. **DDL 실행 오류**
+   - 테이블 권한 확인
+   - 데이터베이스 연결 상태 확인
+   - 기존 RATING 컬럼 존재 여부 확인
+
+2. **API 호출 오류**
+   - 라우팅 설정 재확인
+   - 컨트롤러 메서드 존재 확인
+   - 로그 파일에서 상세 에러 확인
+
+3. **프론트엔드 연동 오류**
+   - 브라우저 개발자 도구 네트워크 탭 확인
+   - API 응답 데이터 구조 확인
+   - CORS 설정 확인
+
+---
+
+**작업 완료 예상 시간:** 30분 (DDL 실행 5분 + 테스트 25분)  
+**리스크 레벨:** 낮음 (기존 기능 유지하며 추가만 진행)  
+**우선순위:** 높음 (프론트엔드 오류 해결 필수) 

+ 12 - 23
pages/view/vendor/dashboard/influencer-requests.vue

@@ -133,7 +133,7 @@
             v-for="request in requests"
             :key="request.SEQ"
             class="request--card"
-            :class="getRequestStatusClass(request.STATUS)"
+            :class="getRequestStatusClass(request.CURRENT_STATUS)"
           >
             <!-- 카드 헤더 -->
             <div class="request--card--header">
@@ -211,8 +211,8 @@
               </div>
               <div class="request--status">
                 <div class="status--badges">
-                  <v-chip :color="getStatusColor(request.STATUS)" size="small">
-                    {{ getStatusText(request.STATUS) }}
+                  <v-chip :color="getStatusColor(request.CURRENT_STATUS)" size="small">
+                    {{ getStatusText(request.CURRENT_STATUS) }}
                   </v-chip>
                   <v-chip 
                     v-if="request.ADD_INFO1 === 'REAPPLY'" 
@@ -250,28 +250,17 @@
                 <p>"{{ request.REQUEST_MESSAGE }}"</p>
               </div>
 
-              <div
-                v-if="request.COMMISSION_RATE || request.SPECIAL_CONDITIONS"
-                class="request--conditions"
-              >
-                <h5>희망 조건</h5>
-                <div v-if="request.COMMISSION_RATE" class="condition--item">
-                  <span class="condition--label">희망 수수료율:</span>
-                  <span class="condition--value">{{ request.COMMISSION_RATE }}%</span>
-                </div>
-                <div v-if="request.SPECIAL_CONDITIONS" class="condition--item">
-                  <span class="condition--label">특별 조건:</span>
-                  <span class="condition--value">{{ request.SPECIAL_CONDITIONS }}</span>
-                </div>
+              <div class="request--commission">
+                <h5>수수료 조건</h5>
+                <p>{{ request.COMMISSION_RATE || 0 }}%</p>
               </div>
 
-              <div v-if="request.STATUS === 'PENDING'" class="expire--info">
-                <v-icon size="16" color="warning">mdi-clock-alert</v-icon>
-                <span>만료일: {{ formatDate(request.EXPIRED_DATE) }}</span>
+              <div v-if="request.SPECIAL_CONDITIONS" class="request--conditions">
+                <h5>특별 조건</h5>
+                <p>"{{ request.SPECIAL_CONDITIONS }}"</p>
               </div>
             </div>
 
-            <!-- 카드 푸터 (액션 버튼) -->
             <div class="request--card--footer">
               <div class="card--actions">
                 <v-btn
@@ -281,7 +270,7 @@
                   프로필 보기
                 </v-btn>
 
-                <div v-if="request.STATUS === 'PENDING'" class="approval--actions">
+                <div v-if="request.CURRENT_STATUS === 'PENDING'" class="approval--actions">
                   <v-btn
                     class="custom-btn mini btn-red"
                     @click="handleReject(request)"
@@ -298,7 +287,7 @@
                   </v-btn>
                 </div>
 
-                <div v-else-if="request.STATUS === 'APPROVED'" class="approved--actions">
+                <div v-else-if="request.CURRENT_STATUS === 'APPROVED'" class="approved--actions">
                   <v-btn
                     class="custom-btn mini btn-outline"
                     @click="viewRequestHistory(request.SEQ)"
@@ -316,7 +305,7 @@
                 </div>
 
                 <div
-                  v-else-if="request.STATUS === 'TERMINATED'"
+                  v-else-if="request.CURRENT_STATUS === 'TERMINATED'"
                   class="terminated--actions"
                 >
                   <v-btn