diff --git a/app/Commands/NaverWorker.php b/app/Commands/NaverWorker.php index bcb265e..bf3a72e 100644 --- a/app/Commands/NaverWorker.php +++ b/app/Commands/NaverWorker.php @@ -42,11 +42,13 @@ class NaverWorker extends BaseCommand // 1. DB 연결 상태 체크 (더 견고하게) try { - if ($this->db->connID === false || !@$this->db->connID->ping()) { + // connID가 없거나, 가벼운 쿼리 실행 실패 시 재연결 시도 + if ($this->db->connID === false || !$this->db->simpleQuery('SELECT 1')) { $this->db->reconnect(); CLI::write(CLI::color('🔄 Database reconnected.', 'yellow')); } } catch (\Throwable $e) { + // 어떤 이유로든 에러 발생 시 재연결 시도 $this->db->reconnect(); } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e46662a..433a7b8 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -76,8 +76,10 @@ $routes->group('', ['namespace' => 'App\Controllers\Article'], static function ( $routes->post('chgStatus', 'Receipt::chgStatus'); // 상태변경 $routes->post('sendSms', 'Receipt::sendSms'); // 문자발송 $routes->post('saveRecInfo', 'Receipt::saveRecInfo'); // 거주인정보저장 + $routes->get('getImages', 'Receipt::getImages'); // 이미지 목록 조회 $routes->post('uploadFile', 'Receipt::uploadFile'); // 파일업로드 $routes->post('removeUploadFile', 'Receipt::removeUploadFile'); // 파일삭제 + $routes->post('updateImageOrder', 'Receipt::updateImageOrder'); // 이미지 순서 업데이트 $routes->get('downloadAllImages', 'Receipt::downloadAllImages'); // 이미지 일괄 다운로드 $routes->post('saveImgLocation', 'Receipt::saveImgLocation'); // 촬영위치 저장 }); diff --git a/app/Controllers/Article/Receipt.php b/app/Controllers/Article/Receipt.php index b6b6927..143b420 100644 --- a/app/Controllers/Article/Receipt.php +++ b/app/Controllers/Article/Receipt.php @@ -23,6 +23,8 @@ class Receipt extends BaseController public function lists(): string { + error_log('========== Receipt::lists() CALLED =========='); + $usr_id = $this->request->getGet('usr_id') ?: ''; $sBonbu = $this->request->getGet('bonbu') ?: ''; $sTeanm = $this->request->getGet('dept_sq') ?: ''; @@ -54,6 +56,8 @@ class Receipt extends BaseController public function getResultList() { + error_log('========== Receipt::getResultList() CALLED =========='); + $start = (int) $this->request->getGet('start') ?: 0; $end = (int) $this->request->getGet('length') ?: 10; @@ -92,9 +96,13 @@ class Receipt extends BaseController 'srchTxt' => $this->request->getGet('srchTxt'), // 검색어 ]; + error_log('[Receipt::getResultList] START - start=' . $start . ' length=' . $end); + $totalCount = $this->model->getTotalCount($data); + error_log('[Receipt::getResultList] TotalCount = ' . $totalCount); $datas = $this->model->getResultList($start, $end, $data); + error_log('[Receipt::getResultList] END - returned ' . count($datas) . ' rows'); return $this->response->setJSON(body: [ 'recordsTotal' => $totalCount, @@ -717,6 +725,26 @@ class Receipt extends BaseController $rsrv_sq = $this->request->getPost('rsrv_sq'); $rcpt_sq = $this->request->getPost('rcpt_sq'); $img_type = $this->request->getPost('img_type'); + $img_sub_type = $this->request->getPost('img_sub_type'); + $file_order = $this->request->getPost('file_order'); + + log_message('info', '[Receipt::uploadFile] Request received - rcpt_key:{rcpt_key}, rsrv_sq:{rsrv_sq}, rcpt_sq:{rcpt_sq}, img_type:{img_type}, img_sub_type:{img_sub_type}, file_order:{file_order}', [ + 'rcpt_key' => $rcpt_key, + 'rsrv_sq' => $rsrv_sq, + 'rcpt_sq' => $rcpt_sq, + 'img_type' => $img_type, + 'img_sub_type' => $img_sub_type, + 'file_order' => $file_order, + ]); + + // 필수 파라미터 검증 + if (empty($rsrv_sq)) { + log_message('error', '[Receipt::uploadFile] rsrv_sq is empty'); + return $this->response->setJSON([ + 'code' => '9', + 'msg' => 'rsrv_sq(예약일련번호)가 필요합니다.' + ]); + } $files = $this->request->getFiles(); $uploadPath = "/upload/result/" . $rsrv_sq . "/"; @@ -738,7 +766,7 @@ class Receipt extends BaseController // 워터마크 이미지 조회 $watermark_info = []; - if (!empty($receipt) && ($receipt['comp_sq'] ?? '') == '2') { + if (!empty($receipt) && !empty($receipt['rcpt_cpid'])) { $watermark_info = $this->model->getWatermarkList($receipt['rcpt_cpid']); } @@ -758,15 +786,20 @@ class Receipt extends BaseController if ($uploadData !== false) { $arrUploadfile[] = $uploadData; - log_message('info', '[Receipt::uploadFile] upload success name={name} stored={stored}', [ + log_message('info', '[Receipt::uploadFile] upload success name={name} stored={stored} cloud_url={url}', [ 'name' => $file->getName(), 'stored' => $uploadData['file_name'] ?? '(unknown)', + 'url' => $uploadData['object_storage_url'] ?? '(none)', ]); } else { - log_message('error', '[Receipt::uploadFile] do_upload2 failed name={name} error={error}:{errorString}', [ + log_message('error', '[Receipt::uploadFile] Cloud upload failed name={name}', [ 'name' => $file->getName(), - 'error' => $file->getError(), - 'errorString' => $file->getErrorString(), + ]); + + // 클라우드 업로드 실패 시 즉시 에러 반환 + return $this->response->setJSON([ + 'code' => '9', + 'msg' => '클라우드 스토리지 업로드 실패: ' . $file->getName() . '\n네트워크 연결 또는 권한을 확인해주세요.' ]); } @@ -786,25 +819,79 @@ class Receipt extends BaseController $dir = rtrim(dirname($uploadFile['object_key']), '/'); // upload/result/* $thumbKey = $dir . '/' . $base . '_thumb.jpg'; - $imageDataBlob = file_get_contents($object_storage_url); - $im = new \Imagick(); - $im->readImageBlob($imageDataBlob); - $imgWidth = $im->getImageWidth(); - $imgHeight = $im->getImageHeight(); - $im->thumbnailImage(105, 80, false); - $thumb_im = $im->getImageBlob(); - // 썸네일 s3 전송 - $lib->upload_object_storage_imagick2($thumbKey, $thumb_im); - $im->destroy(); + try { + // 원본 이미지 다운로드 (클라우드에서) + $imageDataBlob = file_get_contents($object_storage_url); + + if (!$imageDataBlob) { + throw new \Exception('Failed to download original image from cloud storage'); + } + + if (class_exists('\Imagick')) { + // Imagick 사용 + $im = new \Imagick(); + $im->readImageBlob($imageDataBlob); + $imgWidth = $im->getImageWidth(); + $imgHeight = $im->getImageHeight(); + $im->thumbnailImage(105, 80, false); + $thumb_im = $im->getImageBlob(); + $im->destroy(); + + // 썸네일 클라우드 업로드 + $thumbUploaded = $lib->upload_object_storage_imagick2($thumbKey, $thumb_im); + if (!$thumbUploaded) { + log_message('warning', '[Receipt::uploadFile] Thumbnail cloud upload failed: {key}', ['key' => $thumbKey]); + } + + } elseif (extension_loaded('gd')) { + // GD 라이브러리 사용 + $source = imagecreatefromstring($imageDataBlob); + if ($source !== false) { + $imgWidth = imagesx($source); + $imgHeight = imagesy($source); + + // 썸네일 크기 계산 (105x80 비율 유지) + $thumb_width = 105; + $thumb_height = 80; + + // 썸네일 이미지 생성 + $thumb = imagecreatetruecolor($thumb_width, $thumb_height); + imagecopyresampled($thumb, $source, 0, 0, 0, 0, $thumb_width, $thumb_height, $imgWidth, $imgHeight); + + // JPEG로 변환 + ob_start(); + imagejpeg($thumb, null, 85); + $thumb_im = ob_get_clean(); + + // 메모리 해제 + imagedestroy($source); + imagedestroy($thumb); + + // 썸네일 클라우드 업로드 + $thumbUploaded = $lib->upload_object_storage_imagick2($thumbKey, $thumb_im); + if (!$thumbUploaded) { + log_message('warning', '[Receipt::uploadFile] Thumbnail cloud upload failed: {key}', ['key' => $thumbKey]); + } + + log_message('info', '[Receipt::uploadFile] Thumbnail created with GD: {key}', ['key' => $thumbKey]); + } else { + log_message('warning', '[Receipt::uploadFile] GD failed to create image from string'); + } + } else { + log_message('warning', '[Receipt::uploadFile] No image library available (Imagick or GD), skipping thumbnail generation'); + } + } catch (\Exception $e) { + log_message('error', '[Receipt::uploadFile] Thumbnail generation failed: {message}', [ + 'message' => $e->getMessage(), + ]); + } } // 워터마크 삽입 $watermark_imgs = ['I3', 'I4']; - if (in_array($img_type, $watermark_imgs)) { - if (!empty($watermark_info)) { - $common->watermarking($object_storage_url, $watermark_info, '', $receipt['rcpt_cpid'], $uploadFile['object_key']); - } + if (in_array($img_type, $watermark_imgs) && !empty($watermark_info)) { + $common->watermarking($object_storage_url, $watermark_info, '', $receipt['rcpt_cpid'], $uploadFile['object_key']); } // 촬영정보 가져오기 @@ -880,19 +967,34 @@ class Receipt extends BaseController 'meta_data' => $metaData, // 이미지메타데이터 'receipt' => $receipt, 'img_type' => $img_type, + 'cloud_upload_yn' => 'Y', // 클라우드 업로드 성공 (여기까지 왔으면 성공) ]; - $this->model->saveImg($uploadParam); + $img_sq = $this->model->saveImg($uploadParam); } } + // 성공 응답 - JavaScript에서 사용할 이미지 정보 포함 + $lastUploadedFile = end($arrUploadfile); return $this->response->setJSON([ 'code' => '0', - 'msg' => 'success' + 'msg' => 'success', + 'img_sq' => $img_sq ?? null, + 'img_path' => $uploadPath, + 'img_filenm' => $lastUploadedFile['file_name'] ?? '', + 'cloud_upload_yn' => 'Y', + 'img_location' => '' ]); } catch (\Exception $e) { + log_message('error', '[Receipt::uploadFile] Exception occurred: {message} at {file}:{line}', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString(), + ]); + return $this->response->setJSON([ 'code' => '9', 'msg' => $e->getMessage(), @@ -988,6 +1090,69 @@ class Receipt extends BaseController } } + // 업로드파일 목록 조회 (AJAX용) + public function getImages() + { + try { + $rsrv_sq = $this->request->getGet('rsrv_sq') ?? $this->request->getPost('rsrv_sq'); + $img_type = $this->request->getGet('img_type') ?? $this->request->getPost('img_type'); + + if (empty($rsrv_sq)) { + return $this->response->setJSON([ + 'success' => false, + 'msg' => 'rsrv_sq가 필요합니다.' + ]); + } + + // 전체 이미지 리스트 조회 + $images = $this->model->getImageList2($rsrv_sq); + + // img_type이 지정된 경우 필터링 + if (!empty($img_type)) { + $images = array_filter($images, function($img) use ($img_type) { + return $img['img_type'] === $img_type; + }); + $images = array_values($images); // 인덱스 재정렬 + } + + // 이미지 URL 생성 + foreach ($images as &$img) { + $temp = explode(".", $img['img_filenm']); + $thumbName = $temp[0] . "_thumb.jpg"; + + $localOriginalPath = $img['img_path'] . $img['img_filenm']; + $localThumbPath = $img['img_path'] . $thumbName; + + // 모든 파일은 클라우드에 저장됨 + if (($img['cloud_upload_yn'] ?? 'Y') === 'Y') { + $img['original_url'] = NCLOUD_OBJECT_STORAGE_URL . $localOriginalPath; + $img['thumbnail_url'] = NCLOUD_OBJECT_STORAGE_URL . $localThumbPath; + } else { + // 혹시 모를 레거시 로컬 파일 (로컬 파일 시스템 체크) + $localThumbFullPath = FCPATH . $localThumbPath; + if (file_exists($localThumbFullPath)) { + $img['thumbnail_url'] = $localThumbPath; + } else { + $img['thumbnail_url'] = $localOriginalPath; + } + $img['original_url'] = $localOriginalPath; + } + } + + return $this->response->setJSON([ + 'success' => true, + 'images' => $images + ]); + + } catch (\Exception $e) { + log_message('error', '[Receipt::getImages] Error: ' . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'msg' => $e->getMessage() + ]); + } + } + // 업로드파일 삭제 public function removeUploadFile() { @@ -1016,17 +1181,76 @@ class Receipt extends BaseController $lib->deleteFile($thumbPath); } - $this->model->removeUploadFile($rcpt_sq, $img_sq); + $result = $this->model->removeUploadFile($rcpt_sq, $img_sq); + if (!$result['success']) { + return $this->response->setJSON([ + 'code' => '1', + 'msg' => $result['msg'] ?? '삭제 실패' + ]); + } return $this->response->setJSON([ 'code' => '0', 'msg' => 'success' ]); } catch (\Exception $e) { + log_message('error', 'removeUploadFile error: ' . $e->getMessage()); + return $this->response->setJSON([ + 'code' => '1', + 'msg' => '삭제 중 에러가 발생했습니다: ' . $e->getMessage() + ]); + } + } + + // 이미지 순서 업데이트 + public function updateImageOrder() + { + try { + $rcpt_sq = $this->request->getPost('rcpt_sq'); + $img_type = $this->request->getPost('img_type'); + $orders = $this->request->getPost('orders'); + + log_message('info', '[updateImageOrder] 요청 데이터 - rcpt_sq: ' . $rcpt_sq . ', img_type: ' . $img_type . ', orders: ' . $orders); + + if (!$rcpt_sq || !$img_type || !$orders) { + log_message('error', '[updateImageOrder] 필수 파라미터 누락'); + return $this->response->setJSON([ + 'code' => '1', + 'msg' => '필수 파라미터가 누락되었습니다.' + ]); + } + + // JSON 파싱 + $orderData = json_decode($orders, true); + if (!is_array($orderData)) { + return $this->response->setJSON([ + 'code' => '1', + 'msg' => '순서 데이터 형식이 올바르지 않습니다.' + ]); + } + + // 순서 업데이트 + $result = $this->model->updateImageOrder($rcpt_sq, $img_type, $orderData); + + if (!$result['success']) { + return $this->response->setJSON([ + 'code' => '1', + 'msg' => $result['msg'] ?? '순서 업데이트 실패' + ]); + } + return $this->response->setJSON([ 'code' => '0', - 'msg' => 'success' + 'success' => true, + 'msg' => '순서가 저장되었습니다.' + ]); + + } catch (\Exception $e) { + log_message('error', 'updateImageOrder error: ' . $e->getMessage()); + return $this->response->setJSON([ + 'code' => '1', + 'msg' => '순서 업데이트 중 에러가 발생했습니다: ' . $e->getMessage() ]); } } diff --git a/app/Libraries/Common.php b/app/Libraries/Common.php index 5e90353..214a6f6 100644 --- a/app/Libraries/Common.php +++ b/app/Libraries/Common.php @@ -49,7 +49,6 @@ class Common $wmTextAlpha = 0.5; // 워터마크 텍스트 투명도 try { - $img = new \Imagick(); if (is_string($imagePath) && is_file($imagePath)) { $img->readImage($imagePath); @@ -96,19 +95,23 @@ class Common } } - if (empty($wmImagePath)) + if (empty($wmImagePath)) { return; + } + // 워터마크 이미지 경로 처리 if (substr($wmImagePath, 0, 1) == '/') { $wmImagePath = substr($wmImagePath, 1); } + + // ROOTPATH와 결합하여 절대 경로 생성 (img 폴더는 프로젝트 루트에 있음) + $fullWmPath = ROOTPATH . $wmImagePath; + + if (!is_file($fullWmPath)) { + return; + } - - // echo FCPATH.$wmImagePath; - - - - $wm = new \Imagick($wmImagePath); + $wm = new \Imagick($fullWmPath); $hWm = $wm->getImageHeight(); $wWm = $wm->getImageWidth(); @@ -119,29 +122,34 @@ class Common $img->compositeImage($wm, \Imagick::COMPOSITE_OVER, $wmImgLeft, $wmImgTop); $wm->destroy(); - $draw = new \ImagickDraw(); + // 워터마크 텍스트가 있는 경우에만 텍스트 그리기 + if (!empty($wmText)) { + $draw = new \ImagickDraw(); - $basePath = defined('BASEPATH') ? BASEPATH : (defined('ROOTPATH') ? ROOTPATH . 'system/' : ''); - $wmFont = $basePath . 'fonts/' . $wmFont; - if (is_file($wmFont)) { - $draw->setFont($wmFont); + // 폰트 경로: img/watermark/fonts/ + if (!empty($wmFont)) { + $fontPath = ROOTPATH . 'img/watermark/fonts/' . $wmFont; + if (is_file($fontPath)) { + $draw->setFont($fontPath); + } + } + + $draw->setFontSize($wmFontSize); + $draw->setFillColor(new \ImagickPixel($wmTextColor)); + // $draw->setFillAlpha( $wmTextAlpha ); + $draw->setFillOpacity($wmTextAlpha); // 워터마크 텍스트 투명도 설정 + $draw->setTextAlignment(2); // center + $draw->setStrokeAntialias(1); + $draw->setStrokeWidth(1); + $draw->setStrokeColor(new \ImagickPixel('#000000')); + // $draw->setStrokeAlpha(0.1); + $draw->setStrokeOpacity(0.1); + $draw->annotation($wImg / 2, $wmTxtTop, $wmText); + + $img->drawImage($draw); + $draw->destroy(); } - $draw->setFontSize($wmFontSize); - $draw->setFillColor(new \ImagickPixel($wmTextColor)); - // $draw->setFillAlpha( $wmTextAlpha ); - $draw->setFillOpacity($wmTextAlpha); // 워터마크 텍스트 투명도 설정 - $draw->setTextAlignment(2); // center - $draw->setStrokeAntialias(1); - $draw->setStrokeWidth(1); - $draw->setStrokeColor(new \ImagickPixel('#000000')); - // $draw->setStrokeAlpha(0.1); - $draw->setStrokeOpacity(0.1); - $draw->annotation($wImg / 2, $wmTxtTop, $wmText); - - $img->drawImage($draw); - $draw->destroy(); - // $img->writeImage(); $watermark_img = $img->getImageBlob(); @@ -149,14 +157,9 @@ class Common if (empty($key)) { throw new \RuntimeException('Empty upload key'); } + $ok = $uploader->upload_object_storage_imagick2($key, $watermark_img); - if ($ok) { - log_message('info', '[watermarking] upload success key={key} size={size} cpid={cpid}', [ - 'key' => $key, - 'size' => strlen($watermark_img), - 'cpid' => $cpid, - ]); - } else { + if (!$ok) { log_message('error', '[watermarking] upload failed key={key} cpid={cpid}', [ 'key' => $key, 'cpid' => $cpid, @@ -166,7 +169,11 @@ class Common // $object_upload = $this->upload->upload_object_storage($imagePath , $imagePath , 'data'); } catch (\Throwable $e) { - log_message('error', '[watermarking] ' . $e->getMessage()); + log_message('error', '[watermarking] Exception: {message} at {file}:{line}', [ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); } } diff --git a/app/Libraries/MyUpload.php b/app/Libraries/MyUpload.php index 00e566d..ecf5440 100644 --- a/app/Libraries/MyUpload.php +++ b/app/Libraries/MyUpload.php @@ -54,7 +54,7 @@ class MyUpload } /** - * 파일 업로드 요청 + * 파일 업로드 요청 (Object Storage 전용) * 추가일 2025.12.24 * 작성자 - yangsh */ @@ -73,39 +73,40 @@ class MyUpload $newName = $file->getRandomName(); - // ✅ PHP 임시 업로드 파일 경로 (writable로 move() 필요 없음) + // ✅ PHP 임시 업로드 파일 경로 $tmpFile = $file->getTempName(); if (!is_file($tmpFile)) { $this->set_error('upload_temp_file_missing'); - log_message('error', 'do_upload2 temp file missing: ' . $tmpFile); + log_message('error', '[MyUpload] Temp file missing: ' . $tmpFile); return false; } - // ✅ 클라우드에 올라갈 "Key"를 직접 만든다 (로컬 경로 절대 넣지 말기) - // 예시: upload/tmp/랜덤파일명 또는 upload/apt_file/{rcpt_no}/... + // ✅ 클라우드에 올라갈 "Key" $objectKey = $filePath . $newName; + // 클라우드 업로드 (필수) $up = $this->upload_object_storage($objectKey, $tmpFile, 'file'); + if ($up === false) { - $this->set_error('upload_destination_cloud_error'); + $this->set_error('cloud_upload_failed'); + log_message('error', '[MyUpload] Cloud upload failed: ' . $objectKey); + @unlink($tmpFile); return false; } - // (선택) tmp 파일 삭제 + // tmp 파일 삭제 @unlink($tmpFile); $this->s3_data = [ 'object_key' => $objectKey, 'object_storage_url' => $up['object_storage_url'] ?? null, 'origin_name' => $file->getClientName(), - 'file_name' => basename($objectKey), // xxxx.jpg - 'base_name' => pathinfo($objectKey, PATHINFO_FILENAME), // xxxx - 'ext' => pathinfo($objectKey, PATHINFO_EXTENSION), // jpg + 'file_name' => basename($objectKey), + 'base_name' => pathinfo($objectKey, PATHINFO_FILENAME), + 'ext' => pathinfo($objectKey, PATHINFO_EXTENSION), ]; - - log_message('debug', 's3_data=' . json_encode($this->s3_data ?? null, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); - + log_message('info', '[MyUpload] Cloud upload success: ' . $objectKey); return $this->s3_data; } @@ -386,6 +387,7 @@ class MyUpload 'Key' => ltrim($object_storage_upload_path, '/'), 'Body' => $blobData, 'ACL' => 'public-read', + 'ContentType' => 'image/jpeg', ]); diff --git a/app/Models/Entities/WatermarkModel.php b/app/Models/Entities/WatermarkModel.php new file mode 100644 index 0000000..2be4fc8 --- /dev/null +++ b/app/Models/Entities/WatermarkModel.php @@ -0,0 +1,154 @@ + 'required|max_length[20]', + 'wm_type' => 'required|integer', + 'wm_img_path' => 'required|max_length[300]', + 'wm_img_height' => 'required|integer', + 'wm_img_width' => 'required|integer', + 'wm_position' => 'required|max_length[2]', + 'wm_img_opacity' => 'required|integer', + 'wm_space' => 'required|integer', + 'img_width_min' => 'required|integer', + 'img_width_max' => 'required|integer' + ]; + + protected $validationMessages = []; + protected $skipValidation = false; + + /** + * 워터마크 정보 조회 (복합키) + * + * @param string $cpid + * @param int $wm_type + * @return array|null + */ + public function getWatermark($cpid, $wm_type) + { + return $this->where('cpid', $cpid) + ->where('wm_type', $wm_type) + ->first(); + } + + /** + * cpid로 워터마크 목록 조회 + * + * @param string $cpid + * @return array + */ + public function getWatermarksByCpid($cpid) + { + return $this->where('cpid', $cpid)->findAll(); + } + + /** + * 워터마크 타입별 조회 + * + * @param int $wm_type + * @return array + */ + public function getWatermarksByType($wm_type) + { + return $this->where('wm_type', $wm_type)->findAll(); + } + + /** + * 워터마크 정보 저장/업데이트 + * + * @param array $data + * @return bool + */ + public function saveWatermark($data) + { + if (!isset($data['cpid']) || !isset($data['wm_type'])) { + return false; + } + + // 복합키로 기존 데이터 확인 + $existing = $this->getWatermark($data['cpid'], $data['wm_type']); + + if ($existing) { + // 업데이트 + return $this->where('cpid', $data['cpid']) + ->where('wm_type', $data['wm_type']) + ->set($data) + ->update(); + } else { + // 삽입 + return $this->insert($data); + } + } + + /** + * 워터마크 삭제 (복합키) + * + * @param string $cpid + * @param int $wm_type + * @return bool + */ + public function deleteWatermark($cpid, $wm_type) + { + return $this->where('cpid', $cpid) + ->where('wm_type', $wm_type) + ->delete(); + } + + /** + * cpid로 모든 워터마크 삭제 + * + * @param string $cpid + * @return bool + */ + public function deleteAllByCpid($cpid) + { + return $this->where('cpid', $cpid)->delete(); + } + + /** + * 이미지 크기 범위로 워터마크 조회 + * + * @param string $cpid + * @param int $img_width + * @return array|null + */ + public function getWatermarkByImageSize($cpid, $img_width) + { + return $this->where('cpid', $cpid) + ->where('img_width_min <=', $img_width) + ->where('img_width_max >=', $img_width) + ->findAll(); + } +} diff --git a/app/Models/article/ReceiptModel.php b/app/Models/article/ReceiptModel.php index 3ca1bb5..80b6cfc 100644 --- a/app/Models/article/ReceiptModel.php +++ b/app/Models/article/ReceiptModel.php @@ -409,10 +409,9 @@ class ReceiptModel extends Model } - // log_message('debug', '[getTotalCount] SQL = ' . $builder->getCompiledSelect()); - - $row = $builder->get()->getRowArray(); + error_log('[getTotalCount] SQL = ' . $this->db->getLastQuery()); + return (int) ($row['cnt'] ?? 0); } @@ -538,10 +537,10 @@ class ReceiptModel extends Model $builder->join('result b', 'b.rcpt_sq = a.rcpt_sq', 'inner'); $builder->join('region_codes c', 'a.rcpt_dong = c.region_cd', 'inner'); - $builder->join('departments d', 'b.dept_sq = d.dept_sq', 'left'); - $builder->join('users u', 'b.usr_sq = u.usr_sq', 'left'); - $builder->join('result_imgs e', "e.rsrv_sq = b.rsrv_sq AND e.img_type = 'I1' AND e.use_yn = 'Y'", 'left'); - $builder->join('receipt_transimage_log l', 'a.rcpt_key = l.rcpt_key', 'left'); + $builder->join('departments d', 'b.dept_sq = d.dept_sq', 'left outer'); + $builder->join('users u', 'b.usr_sq = u.usr_sq', 'left outer'); + $builder->join('result_imgs e', "e.rsrv_sq = b.rsrv_sq AND e.img_type = 'I1' AND e.use_yn = 'Y'", 'left outer'); + $builder->join('receipt_transimage_log l', 'a.rcpt_key = l.rcpt_key', 'left outer'); $login_dept_info = $this->getDeptDetail($dept_sq); // 로그인 사용자 소속부서정보 @@ -573,8 +572,8 @@ class ReceiptModel extends Model $builder->where('a.rcpt_tm >=', $data['sdate'] . ' 00:00:00'); $builder->where('a.rcpt_tm <=', $data['edate'] . ' 23:59:59'); } else { - $builder->where('b.rsrv_date >=', $data['sdate'] . ' 00:00:00'); - $builder->where('b.rsrv_date <=', $data['edate'] . ' 23:59:59'); + $builder->where('b.rsrv_date >=', $data['sdate'] ); + $builder->where('b.rsrv_date <=', $data['edate'] ); } // 지역 @@ -753,9 +752,12 @@ class ReceiptModel extends Model $builder->limit($end, $start); - // log_message('debug', '[getResultList] SQL = ' . $builder->getCompiledSelect()); + $result = $builder->get()->getResultArray(); + + error_log('[getResultList] SQL = ' . $this->db->getLastQuery()); + error_log('[getResultList] Result count = ' . count($result)); - return $builder->get()->getResultArray(); + return $result; } @@ -1837,7 +1839,8 @@ class ReceiptModel extends Model $receipt = $param['receipt']; - $cloud_upload_yn = 'Y'; + // 실제 클라우드 업로드 성공 여부를 파라미터에서 받아옴 (기본값 'Y') + $cloud_upload_yn = $param['cloud_upload_yn'] ?? 'Y'; if ($param['img_type'] == 'I6' || $param['img_type'] == 'I7') { $yn_sql = "update receipt " . @@ -2011,103 +2014,161 @@ class ReceiptModel extends Model // 이미지정보 조회 $row = $this->getUploadFileInfo($img_sq); - if (!empty($row)) { + if (empty($row)) { + $this->db->transComplete(); + // 파일이 이미 삭제된 경우 성공으로 처리 (중복 삭제 요청 방지) + log_message('info', "[removeUploadFile] 파일 정보 없음 (이미 삭제됨): img_sq={$img_sq}"); + return [ + 'success' => true, + 'msg' => '이미 삭제된 파일입니다', + ]; + } - if ($row['img_type'] == 'I6' || $row['img_type'] == 'I7') { - $yn_sql = "update receipt " . - " set exp_spc_yn = 'N' " . - " where rcpt_sq = ? "; - $yn_data = [$rcpt_sq]; - $this->db->query($yn_sql, $yn_data); - } else if ($row['img_type'] == 'I8') { - $yn_sql = "UPDATE receipt" . - " SET parcel_out_yn = CASE (SELECT COUNT('x')" . - " FROM result_imgs " . - " WHERE rsrv_sq = (SELECT rsrv_sq FROM result_imgs WHERE img_sq = ? AND img_type = 'I8')" . - " AND use_yn = 'Y'" . - " AND img_type = 'I8') WHEN 0 THEN 'N' ELSE 'Y' END" . - " WHERE rcpt_sq = ? "; - $yn_data = [$rcpt_sq]; - $this->db->query($yn_sql, $yn_data); - } else if ($row['img_type'] == 'I9') { - $yn_sql = "UPDATE receipt" . - " SET image_360_yn = (" . - " CASE (SELECT COUNT(1)" . - " FROM result_imgs" . - " WHERE rsrv_sq = (SELECT rsrv_sq FROM result_imgs WHERE img_sq = ? AND img_type = 'I9')" . - " AND img_type = 'I9' AND use_yn = 'Y')" . - " WHEN 0 THEN 'N'" . - " ELSE 'Y'" . - " END" . - " )" . - " WHERE rcpt_sq = ?"; - $yn_data = [$rcpt_sq]; - $this->db->query($yn_sql, $yn_data); - } else if ($row['img_type'] == 'I11') { - $yn_sql = "update receipt " . - " set check_list_img_yn = 'N' " . - " where rcpt_sq = ? "; - $yn_data = [$rcpt_sq]; - $this->db->query($yn_sql, $yn_data); + if ($row['img_type'] == 'I6' || $row['img_type'] == 'I7') { + $yn_sql = "update receipt " . + " set exp_spc_yn = 'N' " . + " where rcpt_sq = ? "; + $yn_data = [$rcpt_sq]; + $this->db->query($yn_sql, $yn_data); + } else if ($row['img_type'] == 'I8') { + $yn_sql = "UPDATE receipt" . + " SET parcel_out_yn = CASE (SELECT COUNT('x')" . + " FROM result_imgs " . + " WHERE rsrv_sq = (SELECT rsrv_sq FROM result_imgs WHERE img_sq = ? AND img_type = 'I8')" . + " AND use_yn = 'Y'" . + " AND img_type = 'I8') WHEN 0 THEN 'N' ELSE 'Y' END" . + " WHERE rcpt_sq = ? "; + $yn_data = [$img_sq, $rcpt_sq]; + $this->db->query($yn_sql, $yn_data); + } else if ($row['img_type'] == 'I9') { + $yn_sql = "UPDATE receipt" . + " SET image_360_yn = (" . + " CASE (SELECT COUNT(1)" . + " FROM result_imgs" . + " WHERE rsrv_sq = (SELECT rsrv_sq FROM result_imgs WHERE img_sq = ? AND img_type = 'I9')" . + " AND img_type = 'I9' AND use_yn = 'Y')" . + " WHEN 0 THEN 'N'" . + " ELSE 'Y'" . + " END" . + " )" . + " WHERE rcpt_sq = ?"; + $yn_data = [$img_sq, $rcpt_sq]; + $this->db->query($yn_sql, $yn_data); + } else if ($row['img_type'] == 'I11') { + $yn_sql = "update receipt " . + " set check_list_img_yn = 'N' " . + " where rcpt_sq = ? "; + $yn_data = [$rcpt_sq]; + $this->db->query($yn_sql, $yn_data); + } + //삭제이미지보다 순번이 높은거는 순번 업데이트 + $sql = "UPDATE result_imgs SET view_odr = view_odr - 1 WHERE rsrv_sq = ? AND img_type = ? AND view_odr > ? AND use_yn = 'Y'"; + $data = [$row['rsrv_sq'], $row['img_type'], $row['view_odr']]; + + $this->db->query($sql, $data); + + //이미지 삭제 + $sql = "DELETE FROM result_imgs WHERE img_sq = ?"; + $data = [$img_sq]; + + $deleteResult = $this->db->query($sql, $data); + + if (in_array($row['img_type'], ['I1', 'I2', 'I8', 'I10', 'I11'])) { + $remark = ""; + switch ($row['img_type']) { + case 'I1': + $remark = "홍보확인서 사진 삭제"; + break; + case 'I2': + $remark = "현장확인 내역서 사진 삭제"; + break; + case 'I8': + $remark = "분양권 파일 삭제"; + break; + case 'I10': + $remark = "촬영동의서 사진 삭제"; + break; + case 'I11': + $remark = "체크리스트 사진 삭제"; + break; } + // 상태값을 가져오기위한 쿼리 해오기 + $sql = "SELECT rcpt_stat FROM receipt WHERE rcpt_sq = ?"; + $data = [$rcpt_sq]; + $query = $this->db->query($sql, $data); + $rowStat = $query->getRowArray(); - //삭제이미지보다 순번이 높은거는 순번 업데이트 - $sql = "UPDATE result_imgs SET view_odr = view_odr - 1 WHERE rsrv_sq = ? AND img_type = ? AND view_odr > ? AND use_yn = 'Y'"; - $data = [$row['rsrv_sq'], $row['img_type'], $row['view_odr']]; + $this->saveChangedHistory($rcpt_sq, $rowStat['rcpt_stat'], 'C31', $usr_id, $remark); + } - $this->db->query($sql, $data); + $this->db->transComplete(); + if ($deleteResult === false || $this->db->transStatus() === false) { + return [ + 'success' => false, + 'msg' => '삭제실패', + ]; + } - //이미지 삭제 - $sql = "DELETE FROM result_imgs WHERE img_sq = ?"; - $data = [$img_sq]; - - if ($this->db->query($sql, $data) === false) { + return [ + 'success' => true, + ]; + } + + // 이미지 순서 업데이트 + public function updateImageOrder($rcpt_sq, $img_type, $orderData) + { + log_message('info', '[ReceiptModel::updateImageOrder] 시작 - rcpt_sq: ' . $rcpt_sq . ', img_type: ' . $img_type . ', 개수: ' . count($orderData)); + + $this->db->transStart(); + + try { + $updateCount = 0; + foreach ($orderData as $item) { + $img_sq = $item['img_sq'] ?? null; + $view_odr = $item['view_odr'] ?? null; + + if (!$img_sq || !$view_odr) { + log_message('warning', '[ReceiptModel::updateImageOrder] 스킵 - img_sq 또는 view_odr 없음'); + continue; + } + + $sql = "UPDATE result_imgs SET view_odr = ? WHERE img_sq = ? AND img_type = ?"; + $data = [$view_odr, $img_sq, $img_type]; + + log_message('debug', '[ReceiptModel::updateImageOrder] 업데이트 - img_sq: ' . $img_sq . ', view_odr: ' . $view_odr); + + $result = $this->db->query($sql, $data); + if ($result) { + $updateCount++; + } + } + + $this->db->transComplete(); + + if ($this->db->transStatus() === false) { + log_message('error', '[ReceiptModel::updateImageOrder] 트랜잭션 실패'); return [ 'success' => false, - 'msg' => '삭제실패', + 'msg' => '순서 업데이트 실패', ]; } - - - if (in_array($row['img_type'], ['I1', 'I2', 'I8', 'I10', 'I11'])) { - $remark = ""; - switch ($row['img_type']) { - case 'I1': - $remark = "홍보확인서 사진 삭제"; - break; - case 'I2': - $remark = "현장확인 내역서 사진 삭제"; - break; - case 'I8': - $remark = "분양권 파일 삭제"; - break; - case 'I10': - $remark = "촬영동의서 사진 삭제"; - break; - case 'I11': - $remark = "체크리스트 사진 삭제"; - break; - } - - // 상태값을 가져오기위한 쿼리 해오기 - $sql = "SELECT rcpt_stat FROM receipt WHERE rcpt_sq = ?"; - $data = [$rcpt_sq]; - $query = $this->db->query($sql, $data); - $row = $query->getRowArray(); - - $this->saveChangedHistory($rcpt_sq, $row['rcpt_stat'], 'C31', $usr_id, $remark); - } - - $this->db->transComplete(); - + + log_message('info', '[ReceiptModel::updateImageOrder] 완료 - 업데이트 수: ' . $updateCount); + return [ 'success' => true, ]; - - + + } catch (\Exception $e) { + log_message('error', '[ReceiptModel::updateImageOrder] Exception: ' . $e->getMessage()); + + return [ + 'success' => false, + 'msg' => '순서 업데이트 중 오류 발생: ' . $e->getMessage(), + ]; } } diff --git a/app/Services/ParameterMapper/TypeSParameterMapper.php b/app/Services/ParameterMapper/TypeSParameterMapper.php index d5f0ed8..08b8476 100644 --- a/app/Services/ParameterMapper/TypeSParameterMapper.php +++ b/app/Services/ParameterMapper/TypeSParameterMapper.php @@ -40,8 +40,8 @@ class TypeSParameterMapper extends BaseParameterMapper 'rcpt_product_info5' => $price['premiumAmount'] ?? '0', 'rcpt_living_yn' => ($rawData['site']['isRegistration'] ?? false) ? 'Y' : 'N', 'rcpt_agent' => $realtor['realtorName'] ?? null, - 'rcpt_sido' => mb_substr($address['legalDivision']['cityNumber'] ?? '', 0, 5), - 'rcpt_gugun' => mb_substr($address['legalDivision']['divisionNumber'] ?? '', 0, 10), + 'rcpt_sido' => $address['legalDivision']['cityNumber'] ?? null, + 'rcpt_gugun' => $address['legalDivision']['divisionNumber'] ?? null, 'rcpt_dong' => $address['legalDivision']['sectorNumber'] ?? null, 'rcpt_hscp_nm' => $address['complexName'] ?? null, 'rcpt_hscp_no' => $address['complexNumber'] ?? null, diff --git a/app/Views/pages/article/receipt/detail.php b/app/Views/pages/article/receipt/detail.php index dd32bf3..3706726 100644 --- a/app/Views/pages/article/receipt/detail.php +++ b/app/Views/pages/article/receipt/detail.php @@ -29,41 +29,22 @@ $usr_level = session('usr_level'); } -
-
-
-
현장확인매물 상세 내용
-
-
-
- -
-
- -
-
- - - - - - - - - - - -
- 매물ID : - - - CP ID : - -
+
+
+
+
+

확인매물 상세 내용

+
+ 매물ID: + + CP ID: + + +
- -
-
공인 중개사 정보
+
+
+
공인 중개사 정보
@@ -1105,17 +1086,14 @@ $usr_level = session('usr_level');
-
+
홍보확인서
- - + +
@@ -1158,7 +1136,7 @@ $usr_level = session('usr_level');
-
+
촬영동의서
@@ -1201,7 +1179,7 @@ $usr_level = session('usr_level');
-
+
현장확인내역서
@@ -1242,19 +1220,25 @@ $usr_level = session('usr_level');
-
- -
-
-
분양권 (최대 5장)
- -
+
+
+
+
분양권 (최대 5장)
+ + +
+ + + +
+
-
- -
+
+ - - 분양권1 - +
+
+ + 분양권 + +
+
- 분양권1 +
+
+ 분양권 +
+
-
- -
-
-
매물사진 (최대 15장)
+
+
+
+
매물사진 (최대 15장)
-
- - - - +
+ + + + +
-
-
+
- - -
- - -
- - 매물사진 + -
- -
- 매물사진 +
+
+ 매물사진
+
-
- -
-
-
-
-
동영상
- -
-
+
+ +
+
+
+
동영상
+ +
+
-
-
-
-
평면도
+ +
+
+
+
평면도
@@ -1440,10 +1426,11 @@ $usr_level = session('usr_level');
-
-
-
-
체크리스트
+ +
+
+
+
체크리스트
@@ -1758,8 +1745,9 @@ $usr_level = session('usr_level'); -
-
검수사항
+
+
+
검수사항
@@ -1770,13 +1758,20 @@ $usr_level = session('usr_level');
+
-
-
-
360이미지 (최대 5장) / 촬영위치
- +
+
+
+
360이미지 (최대 5장) / 촬영위치
+
+ + + +
@@ -1797,9 +1792,11 @@ $usr_level = session('usr_level'); $cnt++; ?>
-
- - 360이미지 +
-
- 360이미지 +
+ 360이미지
+
@@ -2005,28 +2003,33 @@ $usr_level = session('usr_level');