| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- <?php
- namespace App\Models;
- use CodeIgniter\Model;
- class VendorInfluencerStatusHistoryModel extends Model
- {
- protected $table = 'VENDOR_INFLUENCER_STATUS_HISTORY';
- protected $primaryKey = 'SEQ';
- protected $useAutoIncrement = true;
- protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $protectFields = true;
- protected $allowedFields = [
- 'MAPPING_SEQ',
- 'STATUS',
- 'PREVIOUS_STATUS',
- 'STATUS_MESSAGE',
- 'CHANGED_BY',
- 'CHANGED_DATE',
- 'IS_CURRENT',
- 'REG_DATE'
- ];
- // Dates
- protected $useTimestamps = false;
- protected $dateFormat = 'datetime';
- protected $createdField = 'REG_DATE';
- protected $updatedField = '';
- protected $deletedField = '';
- // Validation
- protected $validationRules = [
- 'MAPPING_SEQ' => '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();
- }
- /**
- * 상태 변경 (트랜잭션 포함) - UNIQUE 제약조건 완전 안전 처리
- */
- public function changeStatus($mappingSeq, $newStatus, $statusMessage = '', $changedBy = null)
- {
- $db = \Config\Database::connect();
- $db->transStart();
- try {
- 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;
- 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로 현재 상태 정리 완료");
- }
- log_message('debug', "UNIQUE 제약조건 완전 클리어 완료");
- // 3. 새로운 상태 히스토리 추가 (이제 안전함)
- $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')
- ];
- 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 업데이트
- 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();
- if ($db->transStatus() === false) {
- 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;
- }
- }
- /**
- * 특정 상태의 매핑 목록 조회
- */
- 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();
- }
- }
|