vendorModel = new VendorModel(); $this->userModel = new UserModel(); $this->vendorInfluencerModel = new VendorInfluencerMappingModel(); } /** * 벤더사 검색 */ public function searchVendors() { try { $request = $this->request->getJSON(); $keyword = $request->keyword ?? ''; $category = $request->category ?? ''; $region = $request->region ?? ''; $sortBy = $request->sortBy ?? 'latest'; $page = intval($request->page ?? 1); $size = intval($request->size ?? 12); $influencerSeq = $request->influencerSeq ?? null; $builder = $this->vendorModel->builder(); $builder->where('IS_ACT', 'Y'); // 키워드 검색 if (!empty($keyword)) { $builder->groupStart() ->like('COMPANY_NAME', $keyword) ->orLike('DESCRIPTION', $keyword) ->orLike('TAGS', $keyword) ->groupEnd(); } // 카테고리 필터 if (!empty($category)) { $builder->where('CATEGORY', $category); } // 지역 필터 if (!empty($region)) { $builder->where('REGION', $region); } // 정렬 switch ($sortBy) { case 'partnership': $builder->orderBy('PARTNERSHIP_COUNT', 'DESC') ->orderBy('REG_DATE', 'DESC'); break; case 'name': $builder->orderBy('COMPANY_NAME', 'ASC'); break; case 'latest': default: $builder->orderBy('REG_DATE', 'DESC'); break; } // 전체 개수 $totalCount = $builder->countAllResults(false); // 페이징 $offset = ($page - 1) * $size; $vendors = $builder->limit($size, $offset)->get()->getResultArray(); // 파트너십 개수 추가 foreach ($vendors as &$vendor) { $partnershipCount = $this->vendorInfluencerModel ->where('VENDOR_SEQ', $vendor['SEQ']) ->where('STATUS', 'APPROVED') ->where('IS_ACT', 'Y') ->countAllResults(); $vendor['PARTNERSHIP_COUNT'] = $partnershipCount; // 인플루언서의 파트너십 상태 확인 if ($influencerSeq) { $partnershipStatus = $this->vendorInfluencerModel ->where('VENDOR_SEQ', $vendor['SEQ']) ->where('INFLUENCER_SEQ', $influencerSeq) ->where('IS_ACT', 'Y') ->first(); $vendor['PARTNERSHIP_STATUS'] = $partnershipStatus['STATUS'] ?? null; $vendor['REQUEST_TYPE'] = $partnershipStatus['REQUEST_TYPE'] ?? null; } } $totalPages = ceil($totalCount / $size); return $this->response->setJSON([ 'success' => true, 'data' => [ 'items' => $vendors, 'pagination' => [ 'currentPage' => $page, 'totalPages' => $totalPages, 'totalCount' => $totalCount, 'pageSize' => $size, 'hasNext' => $page < $totalPages, 'hasPrev' => $page > 1 ] ] ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '벤더사 검색 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 승인 요청 생성 */ public function createRequest() { try { $request = $this->request->getJSON(); $vendorSeq = $request->vendorSeq ?? null; $influencerSeq = $request->influencerSeq ?? null; $requestType = $request->requestType ?? 'INFLUENCER_REQUEST'; $requestMessage = $request->requestMessage ?? ''; $requestedBy = $request->requestedBy ?? null; $commissionRate = $request->commissionRate ?? null; $specialConditions = $request->specialConditions ?? ''; // 필수 파라미터 검증 if (!$vendorSeq || !$influencerSeq || !$requestedBy) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '필수 파라미터가 누락되었습니다.' ]); } // 중복 요청 확인 $existingRequest = $this->vendorInfluencerModel ->where('VENDOR_SEQ', $vendorSeq) ->where('INFLUENCER_SEQ', $influencerSeq) ->where('STATUS', 'PENDING') ->where('IS_ACT', 'Y') ->first(); if ($existingRequest) { return $this->response->setStatusCode(409)->setJSON([ 'success' => false, 'message' => '이미 처리 중인 요청이 있습니다.' ]); } // 요청 생성 $data = [ 'VENDOR_SEQ' => $vendorSeq, 'INFLUENCER_SEQ' => $influencerSeq, 'REQUEST_TYPE' => $requestType, 'STATUS' => 'PENDING', 'REQUEST_MESSAGE' => $requestMessage, 'REQUESTED_BY' => $requestedBy, 'COMMISSION_RATE' => $commissionRate, 'SPECIAL_CONDITIONS' => $specialConditions, 'EXPIRED_DATE' => date('Y-m-d H:i:s', strtotime('+7 days')), 'REG_DATE' => date('Y-m-d H:i:s') ]; $insertId = $this->vendorInfluencerModel->insert($data); // 생성된 데이터 조회 $createdRequest = $this->vendorInfluencerModel ->select('vim.*, v.COMPANY_NAME as vendorName, u.NICK_NAME as influencerName, req_user.NICK_NAME as requestedByName') ->from('VENDOR_INFLUENCER_MAPPING vim') ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left') ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left') ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left') ->where('vim.SEQ', $insertId) ->get() ->getRowArray(); return $this->response->setJSON([ 'success' => true, 'message' => '승인 요청이 성공적으로 생성되었습니다.', 'data' => $createdRequest ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '승인 요청 생성 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 매핑 리스트 조회 (벤더사용 승인요청 목록 포함) */ public function getList() { try { $request = $this->request->getJSON(); $vendorSeq = $request->vendorSeq ?? null; $influencerSeq = $request->influencerSeq ?? null; $status = $request->status ?? null; $requestType = $request->requestType ?? null; $keyword = $request->keyword ?? null; $category = $request->category ?? null; $sortBy = $request->sortBy ?? 'latest'; $page = intval($request->page ?? 1); $size = intval($request->size ?? 12); $builder = $this->vendorInfluencerModel->builder(); $builder->select('vim.*, v.COMPANY_NAME as vendorName, v.EMAIL as vendorEmail, v.LOGO as vendorLogo, u.NICK_NAME as influencerNickname, u.NAME as influencerName, u.EMAIL as influencerEmail, u.PHONE as influencerPhone, u.PROFILE_IMAGE as influencerAvatar, u.REGION as influencerRegion, u.PRIMARY_CATEGORY as influencerCategory, u.FOLLOWER_COUNT as followerCount, u.AVG_VIEWS as avgViews, u.ENGAGEMENT_RATE as engagementRate, u.DESCRIPTION as influencerDescription, u.SNS_CHANNELS as influencerSnsChannels, req_user.NICK_NAME as requestedByName, app_user.NICK_NAME as approvedByName, CASE WHEN vim.EXPIRED_DATE < NOW() AND vim.STATUS = "PENDING" THEN "EXPIRED" ELSE vim.STATUS END as displayStatus') ->from('VENDOR_INFLUENCER_MAPPING vim') ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left') ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left') ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left') ->join('USER_LIST app_user', 'vim.APPROVED_BY = app_user.SEQ', 'left') ->where('vim.IS_ACT', 'Y'); // 필터 적용 if ($vendorSeq) { $builder->where('vim.VENDOR_SEQ', $vendorSeq); } if ($influencerSeq) { $builder->where('vim.INFLUENCER_SEQ', $influencerSeq); } if ($status) { $builder->where('vim.STATUS', $status); } if ($requestType) { $builder->where('vim.REQUEST_TYPE', $requestType); } // 키워드 검색 (인플루언서명) if (!empty($keyword)) { $builder->groupStart() ->like('u.NICK_NAME', $keyword) ->orLike('u.NAME', $keyword) ->groupEnd(); } // 카테고리 필터 (인플루언서 카테고리) if (!empty($category)) { $builder->where('u.PRIMARY_CATEGORY', $category); } // 전체 개수 $totalCount = $builder->countAllResults(false); // 정렬 switch ($sortBy) { case 'oldest': $builder->orderBy('vim.REG_DATE', 'ASC'); break; case 'expiring': $builder->orderBy('vim.EXPIRED_DATE', 'ASC'); break; case 'latest': default: $builder->orderBy('vim.REG_DATE', 'DESC'); break; } // 페이징 $offset = ($page - 1) * $size; $items = $builder->limit($size, $offset)->get()->getResultArray(); $totalPages = ceil($totalCount / $size); // 통계 데이터 계산 (벤더사용) $stats = []; if ($vendorSeq) { $statsBuilder = $this->vendorInfluencerModel->builder(); $stats = [ 'pending' => $statsBuilder->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'PENDING')->where('IS_ACT', 'Y')->countAllResults(), 'approved' => $statsBuilder->resetQuery()->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'APPROVED')->where('IS_ACT', 'Y')->countAllResults(), 'rejected' => $statsBuilder->resetQuery()->where('VENDOR_SEQ', $vendorSeq)->where('STATUS', 'REJECTED')->where('IS_ACT', 'Y')->countAllResults(), 'total' => $totalCount ]; } return $this->response->setJSON([ 'success' => true, 'data' => [ 'items' => $items, 'pagination' => [ 'currentPage' => $page, 'totalPages' => $totalPages, 'totalCount' => $totalCount, 'pageSize' => $size, 'hasNext' => $page < $totalPages, 'hasPrev' => $page > 1 ], 'stats' => $stats ] ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '리스트 조회 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 요청 취소 */ public function cancelRequest() { try { $request = $this->request->getJSON(); $mappingSeq = $request->mappingSeq ?? null; $cancelledBy = $request->cancelledBy ?? null; $cancelReason = $request->cancelReason ?? '요청자에 의해 취소됨'; // 필수 파라미터 검증 if (!$mappingSeq || !$cancelledBy) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '필수 파라미터가 누락되었습니다.' ]); } // 기존 요청 확인 $existingMapping = $this->vendorInfluencerModel ->where('SEQ', $mappingSeq) ->where('STATUS', 'PENDING') ->where('IS_ACT', 'Y') ->first(); if (!$existingMapping) { return $this->response->setStatusCode(404)->setJSON([ 'success' => false, 'message' => '취소할 수 있는 요청을 찾을 수 없습니다.' ]); } // 권한 확인 if ($existingMapping['REQUESTED_BY'] != $cancelledBy) { return $this->response->setStatusCode(403)->setJSON([ 'success' => false, 'message' => '요청을 취소할 권한이 없습니다.' ]); } // 취소 처리 $updateData = [ 'STATUS' => 'CANCELLED', 'RESPONSE_MESSAGE' => $cancelReason, 'RESPONSE_DATE' => date('Y-m-d H:i:s'), 'APPROVED_BY' => $cancelledBy ]; $this->vendorInfluencerModel->update($mappingSeq, $updateData); // 업데이트된 데이터 조회 $updatedMapping = $this->vendorInfluencerModel ->select('vim.*, v.COMPANY_NAME as vendorName, u.NICK_NAME as influencerName, req_user.NICK_NAME as requestedByName') ->from('VENDOR_INFLUENCER_MAPPING vim') ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left') ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left') ->join('USER_LIST req_user', 'vim.REQUESTED_BY = req_user.SEQ', 'left') ->where('vim.SEQ', $mappingSeq) ->get() ->getRowArray(); return $this->response->setJSON([ 'success' => true, 'message' => '요청이 취소되었습니다.', 'data' => $updatedMapping ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '요청 취소 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 요청 승인/거부 처리 */ public function approveRequest() { try { $request = $this->request->getJSON(); $mappingSeq = $request->mappingSeq ?? null; $action = $request->action ?? 'APPROVE'; // 'APPROVE' or 'REJECT' $processedBy = $request->processedBy ?? $request->approvedBy ?? null; // 기존 코드 호환성 $responseMessage = $request->responseMessage ?? ''; $commissionRate = $request->commissionRate ?? null; if (!$mappingSeq || !$processedBy) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '필수 파라미터가 누락되었습니다.' ]); } // 처리자 벤더사 존재 확인 $processingVendor = $this->vendorModel ->where('SEQ', $processedBy) ->first(); // IS_ACT 조건 제거해서 벤더사 존재 여부만 확인 if (!$processingVendor) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => "처리자 SEQ {$processedBy}가 VENDOR_LIST 테이블에 존재하지 않습니다." ]); } // 벤더사 활성 상태 확인 if ($processingVendor['IS_ACT'] !== 'Y') { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => "처리자 SEQ {$processedBy}는 비활성 상태입니다. (IS_ACT: {$processingVendor['IS_ACT']})" ]); } // 기존 요청 확인 $existingMapping = $this->vendorInfluencerModel ->where('SEQ', $mappingSeq) ->where('STATUS', 'PENDING') ->where('IS_ACT', 'Y') ->first(); if (!$existingMapping) { return $this->response->setStatusCode(404)->setJSON([ 'success' => false, 'message' => '처리할 수 있는 요청을 찾을 수 없습니다.' ]); } // 벤더사 권한 확인 (선택적) // 벤더사만 자신의 요청을 처리할 수 있도록 하려면 주석 해제 // if ($existingMapping['VENDOR_SEQ'] !== $processingUser['SEQ']) { // return $this->response->setStatusCode(403)->setJSON([ // 'success' => false, // 'message' => '해당 요청을 처리할 권한이 없습니다.' // ]); // } $status = $action === 'APPROVE' ? 'APPROVED' : 'REJECTED'; $updateData = [ 'STATUS' => $status, 'APPROVED_BY' => $processedBy, 'RESPONSE_MESSAGE' => $responseMessage, 'RESPONSE_DATE' => date('Y-m-d H:i:s') ]; if ($commissionRate !== null && $action === 'APPROVE') { $updateData['COMMISSION_RATE'] = $commissionRate; } // 단계별 업데이트로 외래키 제약조건 문제 우회 try { // 1단계: APPROVED_BY 없이 먼저 업데이트 $firstUpdateData = [ 'STATUS' => $status, 'RESPONSE_MESSAGE' => $responseMessage, 'RESPONSE_DATE' => date('Y-m-d H:i:s') ]; if ($commissionRate !== null && $action === 'APPROVE') { $firstUpdateData['COMMISSION_RATE'] = $commissionRate; } log_message('debug', "1단계 업데이트 시도: " . json_encode($firstUpdateData)); $result1 = $this->vendorInfluencerModel->update($mappingSeq, $firstUpdateData); log_message('debug', "1단계 업데이트 결과: " . ($result1 ? 'SUCCESS' : 'FAILED')); // 2단계: APPROVED_BY만 별도로 업데이트 $secondUpdateData = [ 'APPROVED_BY' => $processedBy ]; log_message('debug', "2단계 업데이트 시도: " . json_encode($secondUpdateData)); $result2 = $this->vendorInfluencerModel->update($mappingSeq, $secondUpdateData); log_message('debug', "2단계 업데이트 결과: " . ($result2 ? 'SUCCESS' : 'FAILED')); if (!$result1 || !$result2) { throw new \Exception("단계별 업데이트 실패: 1단계={$result1}, 2단계={$result2}"); } } catch (\Exception $updateException) { log_message('error', "단계별 업데이트 실패: " . $updateException->getMessage()); // 외래키 제약조건 오류인 경우 더 상세한 정보 제공 if (strpos($updateException->getMessage(), 'foreign key constraint') !== false) { // 추가 디버깅: 직접 SQL로 사용자 존재 확인 $db = \Config\Database::connect(); $userCheck = $db->query("SELECT SEQ, NICK_NAME, EMAIL, IS_ACT FROM USER_LIST WHERE SEQ = ?", [$processedBy])->getRowArray(); // 추가 디버깅: 현재 매핑 레코드 상태 확인 $mappingCheck = $db->query("SELECT SEQ, STATUS, APPROVED_BY FROM VENDOR_INFLUENCER_MAPPING WHERE SEQ = ?", [$mappingSeq])->getRowArray(); return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '외래키 제약조건 오류가 발생했습니다.', 'error' => $updateException->getMessage(), 'debug_info' => [ 'processedBy' => $processedBy, 'processingVendor' => $processingVendor, 'existingMapping' => $existingMapping, 'userCheck' => $userCheck, 'mappingCheck' => $mappingCheck, 'updateData' => $updateData ] ]); } throw $updateException; // 다른 예외는 기존 처리 방식 유지 } $actionText = $action === 'APPROVE' ? '승인' : '거부'; return $this->response->setJSON([ 'success' => true, 'message' => "요청이 {$actionText}되었습니다.", 'data' => $updateData ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '요청 처리 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } /** * 상세 조회 */ public function getDetail() { try { $request = $this->request->getJSON(); $mappingSeq = $request->mappingSeq ?? null; if (!$mappingSeq) { return $this->response->setStatusCode(400)->setJSON([ 'success' => false, 'message' => '매핑 SEQ가 필요합니다.' ]); } $detail = $this->vendorInfluencerModel ->select('vim.*, v.*, u.NICK_NAME as influencerName, u.EMAIL as influencerEmail') ->from('VENDOR_INFLUENCER_MAPPING vim') ->join('VENDOR_LIST v', 'vim.VENDOR_SEQ = v.SEQ', 'left') ->join('USER_LIST u', 'vim.INFLUENCER_SEQ = u.SEQ', 'left') ->where('vim.SEQ', $mappingSeq) ->where('vim.IS_ACT', 'Y') ->get() ->getRowArray(); if (!$detail) { return $this->response->setStatusCode(404)->setJSON([ 'success' => false, 'message' => '요청을 찾을 수 없습니다.' ]); } return $this->response->setJSON([ 'success' => true, 'data' => $detail ]); } catch (\Exception $e) { return $this->response->setStatusCode(500)->setJSON([ 'success' => false, 'message' => '상세 조회 중 오류가 발생했습니다.', 'error' => ENVIRONMENT === 'development' ? $e->getMessage() : null ]); } } }