VendorPartnershipModel.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <?php
  2. namespace App\Models;
  3. use CodeIgniter\Model;
  4. class VendorPartnershipModel extends Model
  5. {
  6. protected $table = 'VENDOR_INFLUENCER_MAPPING';
  7. protected $primaryKey = 'SEQ';
  8. protected $useAutoIncrement = true;
  9. protected $returnType = 'array';
  10. protected $useSoftDeletes = false;
  11. protected $allowedFields = [
  12. 'VENDOR_SEQ',
  13. 'INFLUENCER_SEQ',
  14. 'REQUEST_TYPE',
  15. 'REQUEST_MESSAGE',
  16. 'RESPONSE_MESSAGE',
  17. 'REQUESTED_BY',
  18. 'APPROVED_BY',
  19. 'COMMISSION_RATE',
  20. 'SPECIAL_CONDITIONS',
  21. 'EXPIRED_DATE',
  22. 'REQUEST_DATE',
  23. 'RESPONSE_DATE',
  24. 'PARTNERSHIP_START_DATE',
  25. 'PARTNERSHIP_END_DATE',
  26. 'ADD_INFO1',
  27. 'ADD_INFO2',
  28. 'ADD_INFO3',
  29. 'IS_ACT'
  30. ];
  31. protected $useTimestamps = true;
  32. protected $createdField = 'REG_DATE';
  33. protected $updatedField = 'MOD_DATE';
  34. protected $dateFormat = 'datetime';
  35. protected $validationRules = [
  36. 'VENDOR_SEQ' => 'required|integer',
  37. 'INFLUENCER_SEQ' => 'required|integer',
  38. 'REQUEST_TYPE' => 'required|in_list[INFLUENCER_REQUEST,VENDOR_PROPOSAL,INFLUENCER_REAPPLY]',
  39. 'REQUESTED_BY' => 'required|integer',
  40. 'COMMISSION_RATE' => 'permit_empty|decimal|greater_than_equal_to[0]|less_than_equal_to[100]',
  41. 'IS_ACT' => 'required|in_list[Y,N]'
  42. ];
  43. // 히스토리 모델
  44. protected $statusHistoryModel;
  45. protected $mappingModel;
  46. public function __construct()
  47. {
  48. parent::__construct();
  49. $this->statusHistoryModel = new VendorInfluencerStatusHistoryModel();
  50. $this->mappingModel = new VendorInfluencerMappingModel();
  51. }
  52. /**
  53. * 벤더사의 인플루언서 요청 목록 조회
  54. */
  55. public function getVendorRequests($vendorSeq, $filters = [])
  56. {
  57. $builder = $this->db->table('VENDOR_INFLUENCER_MAPPING vim');
  58. $builder->select('
  59. vim.*,
  60. vsh.STATUS as CURRENT_STATUS,
  61. vsh.STATUS_MESSAGE as CURRENT_STATUS_MESSAGE,
  62. vsh.CHANGED_DATE as STATUS_CHANGED_DATE,
  63. u.NICK_NAME as INFLUENCER_NAME,
  64. u.NAME as INFLUENCER_REAL_NAME,
  65. u.EMAIL as INFLUENCER_EMAIL,
  66. u.PHONE as INFLUENCER_PHONE,
  67. u.PROFILE_IMAGE,
  68. u.FOLLOWER_COUNT,
  69. u.ENGAGEMENT_RATE,
  70. u.PRIMARY_CATEGORY,
  71. u.INFLUENCER_TYPE,
  72. u.REGION as INFLUENCER_REGION,
  73. u.DESCRIPTION as INFLUENCER_DESCRIPTION,
  74. u.RATING as INFLUENCER_RATING,
  75. u.VERIFICATION_STATUS
  76. ');
  77. $builder->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  78. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"', 'left');
  79. $builder->join('USER_LIST u', 'u.SEQ = vim.INFLUENCER_SEQ', 'left');
  80. $builder->where('vim.VENDOR_SEQ', $vendorSeq);
  81. $builder->where('vim.IS_ACT', 'Y');
  82. // 상태 필터
  83. if (isset($filters['status'])) {
  84. if (is_array($filters['status'])) {
  85. $builder->whereIn('vsh.STATUS', $filters['status']);
  86. } else {
  87. $builder->where('vsh.STATUS', $filters['status']);
  88. }
  89. }
  90. // 요청 타입 필터
  91. if (isset($filters['request_type'])) {
  92. $builder->where('vim.REQUEST_TYPE', $filters['request_type']);
  93. }
  94. // 인플루언서 타입 필터
  95. if (isset($filters['influencer_type'])) {
  96. $builder->where('u.INFLUENCER_TYPE', $filters['influencer_type']);
  97. }
  98. // 카테고리 필터
  99. if (isset($filters['category'])) {
  100. $builder->where('u.PRIMARY_CATEGORY', $filters['category']);
  101. }
  102. // 팔로워 수 필터
  103. if (isset($filters['min_followers'])) {
  104. $builder->where('u.FOLLOWER_COUNT >=', $filters['min_followers']);
  105. }
  106. if (isset($filters['max_followers'])) {
  107. $builder->where('u.FOLLOWER_COUNT <=', $filters['max_followers']);
  108. }
  109. // 기간 필터
  110. if (isset($filters['start_date'])) {
  111. $builder->where('vim.REG_DATE >=', $filters['start_date']);
  112. }
  113. if (isset($filters['end_date'])) {
  114. $builder->where('vim.REG_DATE <=', $filters['end_date']);
  115. }
  116. // 검증 상태 필터
  117. if (isset($filters['verification_status'])) {
  118. $builder->where('u.VERIFICATION_STATUS', $filters['verification_status']);
  119. }
  120. // 재승인 요청 필터
  121. if (isset($filters['is_reapply'])) {
  122. $builder->where('vim.ADD_INFO1', 'REAPPLY');
  123. }
  124. $builder->orderBy('vim.REG_DATE', 'DESC');
  125. return $builder;
  126. }
  127. /**
  128. * 요청 승인/거부 처리
  129. */
  130. public function processRequest($mappingSeq, $action, $processedBy, $responseMessage = '')
  131. {
  132. $partnership = $this->mappingModel->getBasicMapping($mappingSeq);
  133. if (!$partnership) {
  134. throw new \Exception('요청을 찾을 수 없습니다.');
  135. }
  136. // 현재 상태 확인
  137. $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq);
  138. if (!$currentStatus || $currentStatus['STATUS'] !== 'PENDING') {
  139. throw new \Exception('이미 처리된 요청입니다.');
  140. }
  141. $newStatus = ($action === 'approve') ? 'APPROVED' : 'REJECTED';
  142. // 상태 변경
  143. $statusResult = $this->statusHistoryModel->changeStatus(
  144. $mappingSeq,
  145. $newStatus,
  146. $responseMessage,
  147. $processedBy
  148. );
  149. $updateData = [
  150. 'RESPONSE_MESSAGE' => $responseMessage,
  151. 'APPROVED_BY' => $processedBy,
  152. 'RESPONSE_DATE' => date('Y-m-d H:i:s')
  153. ];
  154. // 승인인 경우 파트너십 시작일 설정
  155. if ($action === 'approve') {
  156. $updateData['PARTNERSHIP_START_DATE'] = date('Y-m-d H:i:s');
  157. }
  158. $this->update($mappingSeq, $updateData);
  159. return $statusResult;
  160. }
  161. /**
  162. * 파트너십 해지 (벤더사가 해지)
  163. */
  164. public function terminateByVendor($mappingSeq, $vendorSeq, $reason = '')
  165. {
  166. $partnership = $this->mappingModel->getBasicMapping($mappingSeq);
  167. if (!$partnership) {
  168. throw new \Exception('파트너십을 찾을 수 없습니다.');
  169. }
  170. if ($partnership['VENDOR_SEQ'] != $vendorSeq) {
  171. throw new \Exception('본인의 파트너십만 해지할 수 있습니다.');
  172. }
  173. // 현재 상태 확인
  174. $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq);
  175. if (!$currentStatus || $currentStatus['STATUS'] !== 'APPROVED') {
  176. throw new \Exception('승인된 파트너십만 해지할 수 있습니다.');
  177. }
  178. // 상태를 TERMINATED로 변경
  179. $statusResult = $this->statusHistoryModel->changeStatus(
  180. $mappingSeq,
  181. 'TERMINATED',
  182. $reason,
  183. $vendorSeq
  184. );
  185. // 파트너십 종료일 설정
  186. $this->update($mappingSeq, [
  187. 'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s'),
  188. 'ADD_INFO1' => $reason, // 해지 사유
  189. 'ADD_INFO2' => $vendorSeq // 해지 처리자
  190. ]);
  191. return $statusResult;
  192. }
  193. /**
  194. * 벤더사 통계 조회
  195. */
  196. public function getVendorStats($vendorSeq)
  197. {
  198. $stats = [];
  199. // 전체 파트너십 수
  200. $stats['total_partnerships'] = $this->where('VENDOR_SEQ', $vendorSeq)
  201. ->where('IS_ACT', 'Y')
  202. ->countAllResults();
  203. // 상태별 통계는 히스토리 모델에서 조회
  204. $statusStats = $this->statusHistoryModel->getStatusStatsByVendor($vendorSeq);
  205. $statusCounts = [];
  206. foreach ($statusStats as $stat) {
  207. $statusCounts[$stat['STATUS']] = $stat['count'];
  208. }
  209. $stats['approved_partnerships'] = $statusCounts['APPROVED'] ?? 0;
  210. $stats['active_partnerships'] = $statusCounts['APPROVED'] ?? 0;
  211. $stats['terminated_partnerships'] = $statusCounts['TERMINATED'] ?? 0;
  212. $stats['pending_requests'] = $statusCounts['PENDING'] ?? 0;
  213. $stats['rejected_requests'] = $statusCounts['REJECTED'] ?? 0;
  214. // 재승인 요청 수
  215. $stats['reapply_requests'] = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  216. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  217. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  218. ->where('vim.VENDOR_SEQ', $vendorSeq)
  219. ->where('vsh.STATUS', 'PENDING')
  220. ->where('vim.ADD_INFO1', 'REAPPLY')
  221. ->where('vim.IS_ACT', 'Y')
  222. ->countAllResults();
  223. // 평균 커미션율
  224. $avgCommission = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  225. ->select('AVG(vim.COMMISSION_RATE) as avg_rate')
  226. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  227. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  228. ->where('vim.VENDOR_SEQ', $vendorSeq)
  229. ->where('vsh.STATUS', 'APPROVED')
  230. ->where('vim.IS_ACT', 'Y')
  231. ->get()
  232. ->getRowArray();
  233. $stats['avg_commission_rate'] = round($avgCommission['avg_rate'] ?? 0, 2);
  234. // 인플루언서 타입별 분포
  235. $stats['influencer_type_distribution'] = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  236. ->select('u.INFLUENCER_TYPE, COUNT(*) as count')
  237. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  238. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  239. ->join('USER_LIST u', 'u.SEQ = vim.INFLUENCER_SEQ', 'left')
  240. ->where('vim.VENDOR_SEQ', $vendorSeq)
  241. ->where('vsh.STATUS', 'APPROVED')
  242. ->where('vim.IS_ACT', 'Y')
  243. ->groupBy('u.INFLUENCER_TYPE')
  244. ->get()
  245. ->getResultArray();
  246. // 카테고리별 인플루언서 분포
  247. $stats['category_distribution'] = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  248. ->select('u.PRIMARY_CATEGORY, COUNT(*) as count')
  249. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  250. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  251. ->join('USER_LIST u', 'u.SEQ = vim.INFLUENCER_SEQ', 'left')
  252. ->where('vim.VENDOR_SEQ', $vendorSeq)
  253. ->where('vsh.STATUS', 'APPROVED')
  254. ->where('vim.IS_ACT', 'Y')
  255. ->groupBy('u.PRIMARY_CATEGORY')
  256. ->get()
  257. ->getResultArray();
  258. // 월별 파트너십 생성 추이 (최근 12개월)
  259. $stats['monthly_partnerships'] = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  260. ->select('DATE_FORMAT(vim.PARTNERSHIP_START_DATE, "%Y-%m") as month, COUNT(*) as count')
  261. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  262. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  263. ->where('vim.VENDOR_SEQ', $vendorSeq)
  264. ->where('vsh.STATUS', 'APPROVED')
  265. ->where('vim.PARTNERSHIP_START_DATE >=', date('Y-m-d', strtotime('-12 months')))
  266. ->where('vim.IS_ACT', 'Y')
  267. ->groupBy('month')
  268. ->orderBy('month', 'ASC')
  269. ->get()
  270. ->getResultArray();
  271. // 인플루언서별 성과 상위 10명
  272. $stats['top_influencers'] = $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  273. ->select('
  274. u.SEQ, u.NICK_NAME, u.PROFILE_IMAGE, u.FOLLOWER_COUNT,
  275. u.ENGAGEMENT_RATE, vim.COMMISSION_RATE, vim.PARTNERSHIP_START_DATE
  276. ')
  277. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  278. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"')
  279. ->join('USER_LIST u', 'u.SEQ = vim.INFLUENCER_SEQ', 'left')
  280. ->where('vim.VENDOR_SEQ', $vendorSeq)
  281. ->where('vsh.STATUS', 'APPROVED')
  282. ->where('vim.IS_ACT', 'Y')
  283. ->orderBy('u.FOLLOWER_COUNT', 'DESC')
  284. ->orderBy('u.ENGAGEMENT_RATE', 'DESC')
  285. ->limit(10)
  286. ->get()
  287. ->getResultArray();
  288. return $stats;
  289. }
  290. /**
  291. * 벤더사의 현재 활성 파트너십 조회
  292. */
  293. public function getActivePartnerships($vendorSeq)
  294. {
  295. return $this->getVendorRequests($vendorSeq, [
  296. 'status' => 'APPROVED'
  297. ])->get()->getResultArray();
  298. }
  299. /**
  300. * 새로운 요청 알림 조회
  301. */
  302. public function getNewRequests($vendorSeq, $days = 7)
  303. {
  304. $fromDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
  305. return $this->getVendorRequests($vendorSeq, [
  306. 'status' => 'PENDING',
  307. 'start_date' => $fromDate
  308. ])->get()->getResultArray();
  309. }
  310. /**
  311. * 재승인 요청 목록 조회
  312. */
  313. public function getReapplyRequests($vendorSeq)
  314. {
  315. return $this->getVendorRequests($vendorSeq, [
  316. 'status' => 'PENDING',
  317. 'is_reapply' => true
  318. ])->get()->getResultArray();
  319. }
  320. /**
  321. * 요청 상세 정보 조회
  322. */
  323. public function getRequestDetail($mappingSeq, $vendorSeq)
  324. {
  325. return $this->db->table('VENDOR_INFLUENCER_MAPPING vim')
  326. ->select('
  327. vim.*,
  328. vsh.STATUS as CURRENT_STATUS,
  329. vsh.STATUS_MESSAGE as CURRENT_STATUS_MESSAGE,
  330. vsh.CHANGED_DATE as STATUS_CHANGED_DATE,
  331. u.NICK_NAME, u.NAME, u.EMAIL, u.PHONE, u.PROFILE_IMAGE,
  332. u.FOLLOWER_COUNT, u.ENGAGEMENT_RATE, u.PRIMARY_CATEGORY,
  333. u.INFLUENCER_TYPE, u.REGION, u.DESCRIPTION,
  334. u.RATING as INFLUENCER_RATING, u.VERIFICATION_STATUS,
  335. u.SNS_CHANNELS, u.PORTFOLIO_URL,
  336. requester.NICK_NAME as REQUESTED_BY_NAME
  337. ')
  338. ->join('VENDOR_INFLUENCER_STATUS_HISTORY vsh',
  339. 'vsh.MAPPING_SEQ = vim.SEQ AND vsh.IS_CURRENT = "Y"', 'left')
  340. ->join('USER_LIST u', 'u.SEQ = vim.INFLUENCER_SEQ', 'left')
  341. ->join('USER_LIST requester', 'requester.SEQ = vim.REQUESTED_BY', 'left')
  342. ->where('vim.SEQ', $mappingSeq)
  343. ->where('vim.VENDOR_SEQ', $vendorSeq)
  344. ->where('vim.IS_ACT', 'Y')
  345. ->first();
  346. }
  347. /**
  348. * 인플루언서 제안 생성 (벤더사가 먼저 제안)
  349. */
  350. public function createVendorProposal($data)
  351. {
  352. // 중복 제안 확인
  353. $existing = $this->mappingModel->getExistingMapping(
  354. $data['VENDOR_SEQ'],
  355. $data['INFLUENCER_SEQ'],
  356. ['TERMINATED', 'REJECTED', 'CANCELLED']
  357. );
  358. if (!empty($existing)) {
  359. throw new \Exception('이미 진행 중인 파트너십이나 제안이 있습니다.');
  360. }
  361. $insertData = array_merge($data, [
  362. 'REQUEST_TYPE' => 'VENDOR_PROPOSAL',
  363. 'REQUEST_DATE' => date('Y-m-d H:i:s'),
  364. 'IS_ACT' => 'Y'
  365. ]);
  366. return $this->insert($insertData);
  367. }
  368. /**
  369. * 만료 예정 파트너십 조회
  370. */
  371. public function getExpiringPartnerships($vendorSeq, $days = 30)
  372. {
  373. $expireDate = date('Y-m-d H:i:s', strtotime("+{$days} days"));
  374. return $this->getVendorRequests($vendorSeq, [
  375. 'status' => 'APPROVED'
  376. ])
  377. ->where('vim.EXPIRED_DATE <=', $expireDate)
  378. ->where('vim.EXPIRED_DATE IS NOT NULL')
  379. ->get()
  380. ->getResultArray();
  381. }
  382. /**
  383. * 벤더사별 인플루언서 추천 점수 계산
  384. */
  385. public function getInfluencerRecommendationScore($vendorSeq, $influencerSeq)
  386. {
  387. // 벤더사와 인플루언서 정보 조회는 각각의 모델에서 처리
  388. $vendorModel = new \App\Models\VendorModel();
  389. $influencerModel = new \App\Models\InfluencerModel();
  390. $vendor = $vendorModel->find($vendorSeq);
  391. $influencer = $influencerModel->getProfile($influencerSeq);
  392. if (!$vendor || !$influencer) {
  393. return 0;
  394. }
  395. $score = 0;
  396. // 카테고리 일치도 (40점)
  397. if ($vendor['CATEGORY'] === $influencer['PRIMARY_CATEGORY']) {
  398. $score += 40;
  399. } elseif ($vendor['CATEGORY'] === $influencer['SECONDARY_CATEGORY']) {
  400. $score += 20;
  401. }
  402. // 지역 일치도 (20점)
  403. if ($vendor['REGION'] === $influencer['REGION']) {
  404. $score += 20;
  405. }
  406. // 인플루언서 등급 (20점)
  407. switch ($influencer['INFLUENCER_TYPE']) {
  408. case 'MEGA':
  409. $score += 20;
  410. break;
  411. case 'MACRO':
  412. $score += 15;
  413. break;
  414. case 'MICRO':
  415. $score += 10;
  416. break;
  417. case 'NANO':
  418. $score += 5;
  419. break;
  420. }
  421. // 인플루언서 평점 (10점)
  422. $score += ($influencer['RATING'] ?? 0) * 2;
  423. // 검증 상태 (10점)
  424. if ($influencer['VERIFICATION_STATUS'] === 'VERIFIED') {
  425. $score += 10;
  426. }
  427. return min(100, $score); // 최대 100점
  428. }
  429. }