VendorPartnershipModel.php 18 KB

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