'required|integer', 'STATUS' => 'required|in_list[PENDING,APPROVED,REJECTED,CANCELLED,EXPIRED,TERMINATED]', 'CHANGED_BY' => 'permit_empty|integer', // required 제거, permit_empty로 변경 'IS_CURRENT' => 'required|in_list[Y,N]' ]; protected $validationMessages = [ 'MAPPING_SEQ' => [ 'required' => '매핑 SEQ는 필수입니다.', 'integer' => '매핑 SEQ는 정수여야 합니다.' ], 'STATUS' => [ 'required' => '상태는 필수입니다.', 'in_list' => '유효하지 않은 상태입니다.' ], 'CHANGED_BY' => [ 'integer' => '변경자 SEQ는 정수여야 합니다.' ], 'IS_CURRENT' => [ 'required' => 'IS_CURRENT는 필수입니다.', 'in_list' => 'IS_CURRENT는 Y 또는 N이어야 합니다.' ] ]; protected $skipValidation = false; protected $cleanValidationRules = true; // Callbacks protected $allowCallbacks = true; protected $beforeInsert = ['beforeInsert']; protected $afterInsert = []; protected $beforeUpdate = []; protected $afterUpdate = []; protected $beforeFind = []; protected $afterFind = []; protected $beforeDelete = []; protected $afterDelete = []; /** * 상태 변경 전 처리 */ protected function beforeInsert(array $data) { // REG_DATE 자동 설정 if (!isset($data['data']['REG_DATE'])) { $data['data']['REG_DATE'] = date('Y-m-d H:i:s'); } // CHANGED_DATE 자동 설정 if (!isset($data['data']['CHANGED_DATE'])) { $data['data']['CHANGED_DATE'] = date('Y-m-d H:i:s'); } return $data; } /** * 특정 매핑의 현재 상태 조회 */ public function getCurrentStatus($mappingSeq) { return $this->where('MAPPING_SEQ', $mappingSeq) ->where('IS_CURRENT', 'Y') ->first(); } /** * 특정 매핑의 상태 히스토리 조회 */ public function getStatusHistory($mappingSeq, $limit = 10) { return $this->where('MAPPING_SEQ', $mappingSeq) ->orderBy('CHANGED_DATE', 'DESC') ->limit($limit) ->findAll(); } /** * 상태 변경 (트랜잭션 포함) - Fallback 방식 우선 적용 */ public function changeStatus($mappingSeq, $newStatus, $statusMessage = '', $changedBy = null) { $db = \Config\Database::connect(); $db->transStart(); try { log_message('info', "상태 변경 시작: mappingSeq={$mappingSeq}, newStatus={$newStatus}"); // CHANGED_BY 기본값 처리 if ($changedBy === null) { $changedBy = 1; log_message('warning', 'CHANGED_BY가 null이므로 기본값 1로 설정'); } // 1. 현재 상태 조회 $currentStatus = $this->getCurrentStatus($mappingSeq); $previousStatus = $currentStatus ? $currentStatus['STATUS'] : null; log_message('info', "이전 상태: " . ($previousStatus ?: 'NULL') . " → 새 상태: {$newStatus}"); // 2. 히스토리 테이블 방식 시도 $historySuccess = false; try { // 기존 상태 비활성화 if ($currentStatus) { $updateSql = "UPDATE VENDOR_INFLUENCER_STATUS_HISTORY SET IS_CURRENT = 'N' WHERE MAPPING_SEQ = ? AND IS_CURRENT = 'Y'"; $db->query($updateSql, [$mappingSeq]); log_message('info', "기존 상태 비활성화 완료"); } // 새 히스토리 레코드 추가 $historyData = [ 'MAPPING_SEQ' => (int)$mappingSeq, 'STATUS' => $newStatus, 'PREVIOUS_STATUS' => $previousStatus, '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') ]; if ($this->validate($historyData)) { $insertResult = $this->insert($historyData, false); if ($insertResult) { $historySuccess = true; log_message('info', "히스토리 테이블 업데이트 성공: ID={$insertResult}"); } } } catch (\Exception $historyError) { log_message('warning', '히스토리 테이블 방식 실패: ' . $historyError->getMessage()); } // 3. 히스토리 테이블 실패 시 메인 테이블 직접 업데이트 (Fallback) if (!$historySuccess) { log_message('info', '히스토리 테이블 실패 - 메인 테이블 직접 업데이트로 fallback'); $mappingModel = new VendorInfluencerMappingModel(); $mainUpdateData = [ 'STATUS' => $newStatus, 'RESPONSE_MESSAGE' => $statusMessage, 'RESPONSE_DATE' => date('Y-m-d H:i:s'), 'APPROVED_BY' => $changedBy, 'MOD_DATE' => date('Y-m-d H:i:s') ]; // TERMINATED 상태인 경우 종료일 추가 if ($newStatus === 'TERMINATED') { $mainUpdateData['PARTNERSHIP_END_DATE'] = date('Y-m-d H:i:s'); } $mainUpdateResult = $mappingModel->update($mappingSeq, $mainUpdateData); if (!$mainUpdateResult) { throw new \Exception('메인 테이블 업데이트도 실패'); } log_message('info', '메인 테이블 직접 업데이트 성공 (Fallback)'); // 트랜잭션 완료 $db->transComplete(); if ($db->transStatus() === false) { throw new \Exception('트랜잭션 실패'); } return 'main_table_update'; // 성공 표시 } // 4. 히스토리 테이블 성공 시 메인 테이블 MOD_DATE도 업데이트 try { $mappingModel = new VendorInfluencerMappingModel(); $mappingModel->update($mappingSeq, ['MOD_DATE' => date('Y-m-d H:i:s')]); } catch (\Exception $mainUpdateError) { log_message('warning', '메인 테이블 MOD_DATE 업데이트 실패 (계속 진행): ' . $mainUpdateError->getMessage()); } // 트랜잭션 완료 $db->transComplete(); if ($db->transStatus() === false) { throw new \Exception('상태 변경 트랜잭션 실패'); } log_message('info', "상태 변경 완료: mappingSeq={$mappingSeq}"); return $insertResult ?? 'fallback_success'; } catch (\Exception $e) { $db->transRollback(); log_message('error', '상태 변경 실패: ' . $e->getMessage()); log_message('error', '실패한 파라미터: ' . json_encode([ 'mappingSeq' => $mappingSeq, 'newStatus' => $newStatus, 'statusMessage' => $statusMessage, 'changedBy' => $changedBy ])); throw $e; } } /** * 특정 상태의 매핑 목록 조회 */ public function getMappingsByStatus($status, $isActive = true) { $builder = $this->builder(); $builder->select('VENDOR_INFLUENCER_STATUS_HISTORY.*, VENDOR_INFLUENCER_MAPPING.*') ->join('VENDOR_INFLUENCER_MAPPING', 'VENDOR_INFLUENCER_MAPPING.SEQ = VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ') ->where('VENDOR_INFLUENCER_STATUS_HISTORY.STATUS', $status) ->where('VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT', 'Y'); if ($isActive) { $builder->where('VENDOR_INFLUENCER_MAPPING.IS_ACT', 'Y'); } return $builder->get()->getResultArray(); } /** * 벤더사별 상태 통계 */ public function getStatusStatsByVendor($vendorSeq) { $builder = $this->builder(); return $builder->select('VENDOR_INFLUENCER_STATUS_HISTORY.STATUS, COUNT(*) as count') ->join('VENDOR_INFLUENCER_MAPPING', 'VENDOR_INFLUENCER_MAPPING.SEQ = VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ') ->where('VENDOR_INFLUENCER_MAPPING.VENDOR_SEQ', $vendorSeq) ->where('VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT', 'Y') ->where('VENDOR_INFLUENCER_MAPPING.IS_ACT', 'Y') ->groupBy('VENDOR_INFLUENCER_STATUS_HISTORY.STATUS') ->get() ->getResultArray(); } /** * 인플루언서별 상태 통계 */ public function getStatusStatsByInfluencer($influencerSeq) { $builder = $this->builder(); return $builder->select('VENDOR_INFLUENCER_STATUS_HISTORY.STATUS, COUNT(*) as count') ->join('VENDOR_INFLUENCER_MAPPING', 'VENDOR_INFLUENCER_MAPPING.SEQ = VENDOR_INFLUENCER_STATUS_HISTORY.MAPPING_SEQ') ->where('VENDOR_INFLUENCER_MAPPING.INFLUENCER_SEQ', $influencerSeq) ->where('VENDOR_INFLUENCER_STATUS_HISTORY.IS_CURRENT', 'Y') ->where('VENDOR_INFLUENCER_MAPPING.IS_ACT', 'Y') ->groupBy('VENDOR_INFLUENCER_STATUS_HISTORY.STATUS') ->get() ->getResultArray(); } }