vendorInfluencerModel = new VendorInfluencerMappingModel(); $this->vendorPartnershipModel = new VendorPartnershipModel(); $this->statusHistoryModel = new VendorInfluencerStatusHistoryModel(); $this->vendorModel = new VendorModel(); $this->influencerModel = new InfluencerModel(); } /** * 벤더사의 인플루언서 요청 목록 조회 (히스토리 테이블 기반) */ 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->vendorPartnershipModel->getVendorRequestsWithPagination($vendorSeq, $page, $size, $status); // 통계 계산 (히스토리 테이블이 없을 경우를 대비한 안전장치) $statsFormatted = [ 'pending' => 0, 'approved' => 0, 'rejected' => 0, 'total' => 0 ]; try { $stats = $this->statusHistoryModel->getStatusStatsByVendor($vendorSeq); 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; } } } catch (\Exception $statsError) { log_message('warning', '통계 조회 실패 (히스토리 테이블 없음?): ' . $statsError->getMessage()); // 히스토리 테이블이 없으면 메인 테이블에서 대략적인 통계 계산 try { $mainStats = $this->vendorInfluencerModel ->where('VENDOR_SEQ', $vendorSeq) ->where('IS_ACT', 'Y') ->countAllResults(); $statsFormatted['total'] = $mainStats; $statsFormatted['pending'] = $mainStats; // 히스토리가 없으면 모두 PENDING으로 가정 } catch (\Exception $mainStatsError) { log_message('error', '메인 테이블 통계도 실패: ' . $mainStatsError->getMessage()); } } 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->influencerModel ->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; $terminateReason = $request->terminateReason ?? ''; log_message('info', '파트너십 해지 요청: ' . json_encode([ 'mappingSeq' => $mappingSeq, 'terminatedBy' => $terminatedBy, 'terminateReason' => $terminateReason ])); if (!$mappingSeq || !$terminatedBy) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '필수 파라미터가 누락되었습니다.' ]); } // 매핑 정보 확인 (메인 테이블만 사용) $mapping = $this->vendorInfluencerModel->where('SEQ', $mappingSeq) ->where('IS_ACT', 'Y') ->first(); if (!$mapping) { return $this->response->setStatusCode(404)->setJSON([ 'success' => false, 'message' => '파트너십을 찾을 수 없습니다.' ]); } // 현재 상태 확인 (히스토리 테이블 기준) $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq); $actualStatus = $currentStatus ? $currentStatus['STATUS'] : $mapping['STATUS']; log_message('info', '현재 매핑 정보: ' . json_encode($mapping)); log_message('info', '히스토리 테이블 현재 상태: ' . json_encode($currentStatus)); log_message('info', '실제 확인할 상태: ' . $actualStatus); // 현재 상태가 APPROVED인지 확인 if ($actualStatus !== 'APPROVED') { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '승인된 파트너십만 해지할 수 있습니다. 현재 상태: ' . $actualStatus ]); } // 처리자 확인 $processingUser = $this->validateProcessor($terminatedBy); if (!$processingUser['success']) { return $this->response->setStatusCode(400)->setJSON($processingUser); } log_message('info', '처리자 검증 완료: ' . json_encode($processingUser['data'])); // 메인 테이블 직접 업데이트 (단순하고 확실한 방법) $statusMessage = '파트너십 해지: ' . $terminateReason; $actualChangedBy = $processingUser['data']['seq'] ?? $terminatedBy ?: 1; $updateData = [ 'STATUS' => 'TERMINATED', 'RESPONSE_MESSAGE' => $statusMessage, 'RESPONSE_DATE' => date('Y-m-d H:i:s'), 'PARTNERSHIP_END_DATE' => date('Y-m-d H:i:s'), 'APPROVED_BY' => $actualChangedBy, 'MOD_DATE' => date('Y-m-d H:i:s') ]; log_message('info', "메인 테이블 업데이트 데이터: " . json_encode($updateData)); // 업데이트 전 데이터 저장 $beforeUpdate = $this->vendorInfluencerModel->find($mappingSeq); log_message('info', "업데이트 전 데이터: " . json_encode($beforeUpdate)); // UNIQUE 제약조건 우회를 위해 직접 SQL 사용 $db = \Config\Database::connect(); try { // 1. 먼저 기존 TERMINATED 레코드가 있는지 확인 $existingTerminated = $db->query( "SELECT SEQ FROM VENDOR_INFLUENCER_MAPPING WHERE VENDOR_SEQ = ? AND INFLUENCER_SEQ = ? AND STATUS = 'TERMINATED' AND SEQ != ?", [$beforeUpdate['VENDOR_SEQ'], $beforeUpdate['INFLUENCER_SEQ'], $mappingSeq] )->getRowArray(); if ($existingTerminated) { log_message('warning', '기존 TERMINATED 레코드 존재 - 비활성화: ' . json_encode($existingTerminated)); // 기존 TERMINATED 레코드를 비활성화 $db->query( "UPDATE VENDOR_INFLUENCER_MAPPING SET IS_ACT = 'N' WHERE SEQ = ?", [$existingTerminated['SEQ']] ); } // 2. 직접 SQL로 현재 레코드 업데이트 $updateSql = "UPDATE VENDOR_INFLUENCER_MAPPING SET STATUS = 'TERMINATED', RESPONSE_MESSAGE = ?, RESPONSE_DATE = ?, PARTNERSHIP_END_DATE = ?, APPROVED_BY = ?, MOD_DATE = ? WHERE SEQ = ?"; $updateParams = [ $statusMessage, date('Y-m-d H:i:s'), date('Y-m-d H:i:s'), $actualChangedBy, date('Y-m-d H:i:s'), $mappingSeq ]; log_message('info', "직접 SQL 실행: " . $updateSql); log_message('info', "SQL 파라미터: " . json_encode($updateParams)); $updateResult = $db->query($updateSql, $updateParams); $affectedRows = $db->affectedRows(); log_message('info', "직접 SQL 업데이트 결과: 영향받은 행 수={$affectedRows}"); if ($affectedRows === 0) { throw new \Exception('직접 SQL 업데이트 실패 - 영향받은 행이 0개'); } } catch (\Exception $sqlError) { log_message('error', '직접 SQL 업데이트 실패: ' . $sqlError->getMessage()); return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '파트너십 해지 처리 중 SQL 오류가 발생했습니다.', 'error' => '직접 SQL 업데이트 실패', 'debug' => $sqlError->getMessage() ]); } // 업데이트 후 데이터 확인 $afterUpdate = $this->vendorInfluencerModel->find($mappingSeq); log_message('info', "업데이트 후 데이터: " . json_encode($afterUpdate)); // 실제 상태 변경 확인 if ($afterUpdate['STATUS'] !== 'TERMINATED') { log_message('error', '상태 변경 검증 실패: ' . $afterUpdate['STATUS']); return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '파트너십 해지 처리 중 오류가 발생했습니다.', 'error' => '상태 변경 검증 실패', 'debug' => [ 'expected' => 'TERMINATED', 'actual' => $afterUpdate['STATUS'] ] ]); } log_message('info', '파트너십 해지 완료: mappingSeq=' . $mappingSeq); return $this->response->setJSON([ 'success' => true, 'message' => '파트너십이 해지되었습니다.', 'data' => [ 'mappingSeq' => $mappingSeq, 'status' => 'TERMINATED', 'terminatedBy' => $processingUser['data']['name'], 'terminateReason' => $terminateReason, 'terminateDate' => date('Y-m-d H:i:s'), 'verifiedStatus' => $afterUpdate['STATUS'] // 검증된 상태 ] ]); } catch (\Exception $e) { log_message('error', '파트너십 해지 오류: ' . $e->getMessage()); return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '파트너십 해지 중 오류가 발생했습니다.', 'error' => '시스템 오류', 'debug' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 벤더사 상태 통계 조회 */ 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() ]); } } /** * 인플루언서 요청 승인/거절 (프론트엔드 호환용) * 프론트엔드에서 /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 { if (!$mappingSeq) { $mappingSeq = $this->request->getGet('seq') ?? 1; } // 메인 테이블 상태 $mainData = $this->vendorInfluencerModel->where('SEQ', $mappingSeq)->first(); // 히스토리 테이블 전체 $historyData = $this->statusHistoryModel->where('MAPPING_SEQ', $mappingSeq) ->orderBy('CHANGED_DATE', 'DESC') ->findAll(); // 현재 상태 (IS_CURRENT='Y') $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq); return $this->response->setJSON([ 'success' => true, 'mappingSeq' => $mappingSeq, 'mainTable' => $mainData, 'historyTable' => $historyData, 'currentStatus' => $currentStatus, 'timestamp' => date('Y-m-d H:i:s') ]); } catch (\Exception $e) { return $this->response->setJSON([ 'success' => false, '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() ]); } } /** * 메인 테이블과 히스토리 테이블 상태 동기화 */ public function syncMappingStatus() { try { $request = $this->request->getJSON(); $mappingSeq = $request->mappingSeq ?? $this->request->getGet('seq'); if (!$mappingSeq) { return $this->response->setJSON([ 'success' => false, 'message' => 'mappingSeq가 필요합니다.' ]); } // 현재 히스토리 테이블 상태 조회 $currentStatus = $this->statusHistoryModel->getCurrentStatus($mappingSeq); if (!$currentStatus) { return $this->response->setJSON([ 'success' => false, 'message' => '히스토리 테이블에서 현재 상태를 찾을 수 없습니다.' ]); } // 메인 테이블 업데이트 $updateData = [ 'STATUS' => $currentStatus['STATUS'], 'MOD_DATE' => date('Y-m-d H:i:s') ]; // TERMINATED 상태인 경우 추가 필드 업데이트 if ($currentStatus['STATUS'] === 'TERMINATED') { $updateData['RESPONSE_MESSAGE'] = $currentStatus['STATUS_MESSAGE'] ?? '파트너십 해지'; $updateData['RESPONSE_DATE'] = $currentStatus['CHANGED_DATE']; $updateData['PARTNERSHIP_END_DATE'] = $currentStatus['CHANGED_DATE']; $updateData['APPROVED_BY'] = $currentStatus['CHANGED_BY']; } $result = $this->vendorInfluencerModel->update($mappingSeq, $updateData); if ($result) { // 동기화 후 상태 확인 $updatedMain = $this->vendorInfluencerModel->find($mappingSeq); return $this->response->setJSON([ 'success' => true, 'message' => '상태 동기화 완료', 'data' => [ 'mappingSeq' => $mappingSeq, 'syncedStatus' => $currentStatus['STATUS'], 'updatedMainTable' => $updatedMain ] ]); } else { return $this->response->setJSON([ 'success' => false, 'message' => '메인 테이블 업데이트 실패' ]); } } catch (\Exception $e) { return $this->response->setJSON([ 'success' => false, 'message' => '동기화 중 오류 발생', 'error' => $e->getMessage() ]); } } }