From 9d9df394c5fc35b8ebefa4ba829af7c2d8cd3e8a Mon Sep 17 00:00:00 2001 From: jjstyle Date: Thu, 5 Mar 2026 18:17:31 +0900 Subject: [PATCH] =?UTF-8?q?=ED=98=84=EC=9E=A5=ED=99=95=EC=9D=B8=EB=A7=A4?= =?UTF-8?q?=EB=AC=BC=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Config/Routes.php | 2 + app/Config/Routes/Api.php | 2 +- app/Controllers/Article/Receipt.php | 161 +- app/Libraries/NaverApiClient.php | 42 +- app/Models/article/ReceiptModel.php | 89 +- app/Views/pages/article/dept/detail.php | 7 +- app/Views/pages/article/receipt/detail.php | 2148 +------------------ app/Views/pages/article/record/detail.php | 7 +- public/js/article/receipt/detail.js | 2210 ++++++++++++++++++++ 9 files changed, 2507 insertions(+), 2161 deletions(-) create mode 100644 public/js/article/receipt/detail.js diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 433a7b8..ec1f6ad 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -76,6 +76,8 @@ $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('getRecInfo', 'Receipt::getRecInfo'); // 거주인정보조회 + $routes->get('getHistory', 'Receipt::getHistory'); // 정보변경이력조회 $routes->get('getImages', 'Receipt::getImages'); // 이미지 목록 조회 $routes->post('uploadFile', 'Receipt::uploadFile'); // 파일업로드 $routes->post('removeUploadFile', 'Receipt::removeUploadFile'); // 파일삭제 diff --git a/app/Config/Routes/Api.php b/app/Config/Routes/Api.php index 9961c47..b618818 100644 --- a/app/Config/Routes/Api.php +++ b/app/Config/Routes/Api.php @@ -6,5 +6,5 @@ use CodeIgniter\Router\RouteCollection; /** @var RouteCollection $routes */ $routes->group('kiso', function(RouteCollection $routes) { - $routes->match(['get', 'post'], 'api/vrfcReq', 'KisoController::vrfcReq'); + $routes->match(['GET', 'POST'], 'api/vrfcReq', 'KisoController::vrfcReq'); }); \ No newline at end of file diff --git a/app/Controllers/Article/Receipt.php b/app/Controllers/Article/Receipt.php index 143b420..1124d56 100644 --- a/app/Controllers/Article/Receipt.php +++ b/app/Controllers/Article/Receipt.php @@ -202,7 +202,8 @@ class Receipt extends BaseController log_message('info', '[Receipt::detail] getDetail {ms}ms', ['ms' => (int) ((microtime(true) - $t3) * 1000)]); $t4 = microtime(true); - $history = $this->model->getHistory($id); + // rcpt_sq를 사용하여 이력 조회 + $history = $this->model->getHistory($data['rcpt_sq'] ?? $id); log_message('info', '[Receipt::detail] getHistory {ms}ms', ['ms' => (int) ((microtime(true) - $t4) * 1000)]); $t5 = microtime(true); @@ -346,23 +347,52 @@ class Receipt extends BaseController $this->model->saveResDB($rcpt_sq, $rsrv_sq, $res_yn, $dbUsageAgrYn); - $receipt = $this->getDetail($rcpt_key); + $receipt = $this->model->getDetail($rcpt_key); if ($res_yn == 'Y') { $isResidentsExist = true; } else { $isResidentsExist = false; } - $api_result = $naver->residentsExistence($rcpt_key, $isResidentsExist); + $updateData = [ + 'residentsExistence' => $isResidentsExist + ]; + $charger = session('usr_id'); - if (!isset($api_result['result'])) { - throw new \Exception('API 통신오류입니다.\n다시 저장하여 주십시요.'); + log_message('info', '[resDbYn] 네이버 API 호출 시작 - rcpt_key: ' . $rcpt_key . ', charger: ' . $charger . ', updateData: ' . json_encode($updateData, JSON_UNESCAPED_UNICODE)); + + $api_result = $naver->updateArticleInfo($rcpt_key, $updateData, $charger); + + log_message('info', '[resDbYn] 네이버 API 응답 - result: ' . json_encode($api_result, JSON_UNESCAPED_UNICODE) . ' (type: ' . gettype($api_result) . ')'); + + // API 호출 실패 체크 + if (isset($api_result['error']) && $api_result['error'] === true) { + // 에러 정보를 상세하게 표시 + if ($api_result['error_type'] === 'CURL_ERROR') { + $errorMsg = "네이버 API 통신 실패 (CURL 오류)\n"; + $errorMsg .= "오류코드: {$api_result['curl_errno']}\n"; + $errorMsg .= "오류내용: {$api_result['curl_error']}\n"; + $errorMsg .= "URL: {$api_result['url']}"; + } else { + $errorMsg = "네이버 API 응답 오류 (HTTP {$api_result['http_code']})\n"; + $errorMsg .= "응답내용: {$api_result['response']}\n"; + $errorMsg .= "URL: {$api_result['url']}"; + } + throw new \Exception($errorMsg); + } + + // 정상 응답이지만 성공이 아닌 경우 + if (!isset($api_result['code']) || $api_result['code'] !== 'success') { + $errorMsg = "네이버 API 응답 오류\n"; + $errorMsg .= "응답: " . json_encode($api_result, JSON_UNESCAPED_UNICODE); + throw new \Exception($errorMsg); } return $this->response->setJSON([ 'code' => '0', - 'msg' => 'success' + 'msg' => 'success', + 'debug_api_result' => $api_result // 디버그용 (확인 후 제거) ]); } catch (\Exception $e) { @@ -425,7 +455,7 @@ class Receipt extends BaseController $na_result = $naver->reserveSuccess($rcpt_key, 'Y', $bonbuInfo['dept_nm'], $deptInfo['dept_nm'], $userInfo['usr_nm'], $userInfo['usr_tel1'], $rsrv_date, $rsrv_tm_ap); /*** 네이버 연동[e] ***/ - if (array_key_exists('result', $na_result)) { //네이버연동 상태변경 완료 + if (isset($na_result['code']) && $na_result['code'] === 'success') { //네이버연동 상태변경 완료 $result = $this->model->assignRegist($rcpt_sq, $rsrv_date, $rsrv_tm_ap, $rsrv_tm_hour, $dept_sq, $usr_sq, $receipt); return $this->response->setJSON([ @@ -433,7 +463,7 @@ class Receipt extends BaseController 'msg' => 'success' ]); } else { - throw new \Exception($na_result['message']); + throw new \Exception($na_result['message'] ?? '네이버 API 연동 실패'); } @@ -530,11 +560,11 @@ class Receipt extends BaseController } /*** 네이버 연동[e] ***/ - if (array_key_exists('result', $na_result)) { //네이버연동 상태변경 완료 + if (isset($na_result['code']) && $na_result['code'] === 'success') { //네이버연동 상태변경 완료 $result = $this->model->rsrvcancel($rcpt_sq, $rsrv_sq, $result_cd2, $result_cd3, $result_msg, $receipt); } else { - throw new \Exception($na_result['message']); + throw new \Exception($na_result['message'] ?? '네이버 API 연동 실패'); } return $this->response->setJSON([ @@ -644,7 +674,6 @@ class Receipt extends BaseController $lib = new MyUpload(); try { - $rcpt_sq = $this->request->getPost('rcpt_sq'); $rcpt_key = $this->request->getPost('rcpt_key'); $rsrv_sq = $this->request->getPost('rsrv_sq'); @@ -657,8 +686,6 @@ class Receipt extends BaseController $rec_nm = $this->request->getPost('rec_nm'); $rec_remark = $this->request->getPost('rec_remark'); - $file = $this->request->getFile('rec_file'); - $data = [ 'rcpt_sq' => $rcpt_sq, 'rsrv_sq' => $rsrv_sq, @@ -668,43 +695,101 @@ class Receipt extends BaseController 'rec_remark' => $rec_remark, ]; + // 파일 업로드 처리 + $file = $this->request->getFile('rec_file'); if ($file && $file->isValid() && !$file->hasMoved()) { - $uploadPath = "/upload/result/" . $rsrv_sq . "/"; + $uploadData = $lib->do_upload2($file, $uploadPath); - $arrUploadfile = []; - if ($file->isValid() && !$file->hasMoved()) { - $uploadData = $lib->do_upload2($file, $uploadPath); - - if ($uploadData !== false) { - $arrUploadfile[] = $uploadData; - } + if ($uploadData !== false && is_array($uploadData)) { + // do_upload2 반환값 구조: object_key, object_storage_url, origin_name, file_name, base_name, ext + $data['file'] = [ + 'upload_path' => $uploadPath, + 'file_name' => isset($uploadData['file_name']) ? $uploadData['file_name'] : '', + 'origin_name' => isset($uploadData['origin_name']) ? $uploadData['origin_name'] : '', + 'ext' => isset($uploadData['ext']) ? $uploadData['ext'] : '', + 'size' => $file->getSize(), + ]; + } else { + throw new \Exception('파일 업로드에 실패했습니다.'); } - - if (!empty($arrUploadfile)) { - foreach ($arrUploadfile as $key => $uploadFile) { - $data['file'] = [ - 'orig_name' => $uploadFile['origin_name'], - 'new_name' => $uploadFile['file_name'], - 'file_path' => $uploadPath, // 필요에 따라 상대경로로만 저장 - 'ext' => '.' . $uploadFile['ext'], - 'size' => $file->getSize(), - ]; - } - - } - - } - $this->model->saveRecInfo($data); + $result = $this->model->saveRecInfo($data); return $this->response->setJSON([ 'code' => '0', - 'msg' => 'success' + 'msg' => 'success', + 'data' => [ + 'rec_tel' => $result['rec_tel'] ?? '', + 'rec_nm' => $result['rec_nm'] ?? '', + 'remark' => $result['remark'] ?? '', + 'record' => $result['record'] ?? null + ] ]); } catch (\Exception $e) { + log_message('error', '[Receipt::saveRecInfo] Error: ' . $e->getMessage()); + return $this->response->setJSON([ + 'code' => '9', + 'msg' => $e->getMessage(), + ]); + } + } + + // 거주인 정보 조회 + public function getRecInfo() + { + try { + $rcpt_sq = $this->request->getGet('rcpt_sq'); + + if (empty($rcpt_sq)) { + throw new \Exception('rcpt_sq가 필요합니다.'); + } + + // 거주인 정보 조회 + $recInfo = $this->model->getRecInfoByRcptSq($rcpt_sq); + + if (empty($recInfo)) { + throw new \Exception('거주인 정보를 찾을 수 없습니다.'); + } + + return $this->response->setJSON([ + 'code' => '0', + 'msg' => 'success', + 'data' => $recInfo + ]); + + } catch (\Exception $e) { + log_message('error', '[Receipt::getRecInfo] Error: ' . $e->getMessage()); + return $this->response->setJSON([ + 'code' => '9', + 'msg' => $e->getMessage(), + ]); + } + } + + // 정보변경 이력 조회 + public function getHistory() + { + try { + $rcpt_sq = $this->request->getGet('rcpt_sq'); + + if (empty($rcpt_sq)) { + throw new \Exception('rcpt_sq가 필요합니다.'); + } + + // 정보변경 이력 조회 + $history = $this->model->getHistory($rcpt_sq); + + return $this->response->setJSON([ + 'code' => '0', + 'msg' => 'success', + 'data' => $history ?? [] + ]); + + } catch (\Exception $e) { + log_message('error', '[Receipt::getHistory] Error: ' . $e->getMessage()); return $this->response->setJSON([ 'code' => '9', 'msg' => $e->getMessage(), diff --git a/app/Libraries/NaverApiClient.php b/app/Libraries/NaverApiClient.php index 4899e69..f76eb83 100644 --- a/app/Libraries/NaverApiClient.php +++ b/app/Libraries/NaverApiClient.php @@ -41,12 +41,34 @@ class NaverApiClient * object $space 면적정보[비공동] (supplySpace,exclusiveSpace,totalSpace,groundSpace,buildingSpace) * object $facilities 비공동시설정보 (roomCount) */ + /** 현장확인 수정시 */ public function updateArticleInfo(string $articleNumber, array $updateData, string $charger = 'admin'): ?array { $this->charger = $charger; - $url = "{$this->baseUrl}/kiso/center/verification-article/{$articleNumber}?charger={$this->charger}"; + $url = "{$this->baseUrl}/kiso/center/verification-site/{$articleNumber}?charger={$this->charger}"; + + log_message('info', "[updateArticleInfo] 호출됨 - URL: {$url}, 데이터: " . json_encode($updateData, JSON_UNESCAPED_UNICODE)); + + $result = $this->request('PUT', $url, $updateData); + + log_message('info', "[updateArticleInfo] 결과 - " . ($result === null ? 'NULL' : json_encode($result, JSON_UNESCAPED_UNICODE))); + + return $result; + } - return $this->request('PUT', $url, $updateData); + /** 일반매물 수정시 */ + public function v2updateArticleInfo(string $articleNumber, array $updateData, string $charger = 'admin'): ?array + { + $this->charger = $charger; + $url = "{$this->baseUrl}/kiso/center/verification-article/{$articleNumber}?charger={$this->charger}"; + + log_message('info', "[v2updateArticleInfo] 호출됨 - URL: {$url}, 데이터: " . json_encode($updateData, JSON_UNESCAPED_UNICODE)); + + $result = $this->request('PUT', $url, $updateData); + + log_message('info', "[v2updateArticleInfo] 결과 - " . ($result === null ? 'NULL' : json_encode($result, JSON_UNESCAPED_UNICODE))); + + return $result; } /** @@ -525,7 +547,13 @@ class NaverApiClient // CURL 오류 체크 if ($curlErrno !== 0) { log_message('error', "[Naver API $method CURL ERROR] URL: $url | Error ($curlErrno): $curlError"); - return null; + return [ + 'error' => true, + 'error_type' => 'CURL_ERROR', + 'curl_errno' => $curlErrno, + 'curl_error' => $curlError, + 'url' => $url + ]; } // 결과 로그 기록 (성공/실패 모두 기록하여 추적 가능하게 함) @@ -533,7 +561,13 @@ class NaverApiClient log_message('info', "[Naver API $method SUCCESS] URL: $url | Code: $httpCode | Response: $response"); } else { log_message('error', "[Naver API $method FAIL] URL: $url | Code: $httpCode | Response: $response"); - return null; + return [ + 'error' => true, + 'error_type' => 'HTTP_ERROR', + 'http_code' => $httpCode, + 'response' => $response, + 'url' => $url + ]; } return json_decode($response, true); diff --git a/app/Models/article/ReceiptModel.php b/app/Models/article/ReceiptModel.php index 80b6cfc..6487adb 100644 --- a/app/Models/article/ReceiptModel.php +++ b/app/Models/article/ReceiptModel.php @@ -1309,17 +1309,29 @@ class ReceiptModel extends Model // 정보변경이력 public function getHistory($id) { - $sql = "SELECT rcpt_sq FROM receipt WHERE rcpt_key = ?"; - $query = $query = $this->db->query($sql, [$id]); - $rcpt_sq = $query->getRow()->rcpt_sq; + // $id가 숫자면 rcpt_sq로 간주, 아니면 rcpt_key로 간주 + if (is_numeric($id) && strpos($id, '-') === false) { + // 순수 숫자면 rcpt_sq로 직접 사용 + $rcpt_sq = $id; + } else { + // rcpt_key로 간주하고 rcpt_sq 조회 + $sql = "SELECT rcpt_sq FROM receipt WHERE rcpt_key = ?"; + $query = $this->db->query($sql, [$id]); + $row = $query->getRow(); + if ($row) { + $rcpt_sq = $row->rcpt_sq; + } else { + log_message('warning', "[ReceiptModel::getHistory] rcpt_key '{$id}'에 해당하는 receipt를 찾을 수 없습니다."); + return []; + } + } $sql = "SELECT seq" . " ,rcpt_sq" . " ,rcpt_stat" . " ,get_code_name('RECEIPT_STATUS3', rcpt_stat) AS rcpt_stat_nm" . - " ,rcpt_stat" . - " ,get_code_name('CHANGED_TYPE', changed_type) AS changed_type_nm" . " ,changed_type" . + " ,get_code_name('CHANGED_TYPE', changed_type) AS changed_type_nm" . " ,changed_id" . " ,remark" . " ,DATE_FORMAT(changed_tm, '%Y.%m.%d %H:%i:%s') as changed_tm" . @@ -1434,12 +1446,14 @@ class ReceiptModel extends Model // 녹취파일정보 public function getRecordInfo($rsrv_sq) { - $sql = "SELECT record_sq, rsrv_sq, use_yn, record_path, record_filenm, record_orignm, record_size, record_tel, record_nm, record_dt, remark, insert_usr, insert_tm, update_usr, update_tm" . + $sql = "SELECT record_sq, rsrv_sq, use_yn, record_path, record_filenm, record_orignm, record_size, record_tel, record_nm, record_dt, remark, insert_usr, DATE_FORMAT(insert_tm, '%Y-%m-%d %H:%i') as insert_tm, update_usr, update_tm" . " FROM result_record" . " WHERE rsrv_sq = ?" . + " AND use_yn = 'Y'" . " AND record_sq = (SELECT MAX(record_sq)" . " FROM result_record" . - " WHERE rsrv_sq = ?)"; + " WHERE rsrv_sq = ?" . + " AND use_yn = 'Y')"; $data = array( $rsrv_sq, $rsrv_sq @@ -1489,7 +1503,11 @@ class ReceiptModel extends Model $this->db->transStart(); $usr_id = session('usr_id'); - $sql = "SELECT result_cd3, resYn, dbUsageAgrYn FROM result WHERE rsrv_sq = ?"; + // 변경 전 데이터와 현재 상태 조회 + $sql = "SELECT r.result_cd3, r.resYn, r.dbUsageAgrYn, rc.rcpt_stat + FROM result r + LEFT JOIN receipt rc ON rc.rcpt_sq = r.rcpt_sq + WHERE r.rsrv_sq = ?"; $data = array($rsrv_sq); $query = $this->db->query($sql, $data); $row = $query->getRowArray(); @@ -1508,7 +1526,13 @@ class ReceiptModel extends Model ]; } - $this->saveChangedHistory($rcpt_sq, $row['result_cd3'], 'C24', $usr_id, $row['resYn'] . "|" . $row['dbUsageAgrYn']); + // 정보변경 이력 저장 (변경 전 → 변경 후) + $before = "거주:" . ($row['resYn'] ?? 'N') . ", DB동의:" . ($row['dbUsageAgrYn'] ?? 'N'); + $after = "거주:" . $resYn . ", DB동의:" . $dbUsageAgrYn; + $remark = $before . " => " . $after; + $rcpt_stat = $row['rcpt_stat'] ?? $row['result_cd3'] ?? '100000'; + + $this->saveChangedHistory($rcpt_sq, $rcpt_stat, 'C36', $usr_id, $remark); $this->db->transComplete(); @@ -1521,6 +1545,12 @@ class ReceiptModel extends Model public function saveGround($rcpt_sq, $ground_plan) { $this->db->transStart(); + $usr_id = session('usr_id'); + + // 변경 전 데이터 조회 + $sql = "SELECT ground_plan, rcpt_stat FROM receipt WHERE rcpt_sq = ?"; + $query = $this->db->query($sql, [$rcpt_sq]); + $row = $query->getRowArray(); $sql = "UPDATE receipt" . " SET ground_plan = ?" . @@ -1535,6 +1565,10 @@ class ReceiptModel extends Model ]; } + // 정보변경 이력 저장 + $remark = "평면도요청: " . ($row['ground_plan'] ?? 'N') . " => " . $ground_plan; + $this->saveChangedHistory($rcpt_sq, $row['rcpt_stat'], 'C37', $usr_id, $remark); + $this->db->transComplete(); return [ @@ -1810,11 +1844,48 @@ class ReceiptModel extends Model $this->db->transComplete(); + // 저장 후 최신 거주인 정보 반환 + $record = $this->getRecordInfo($data['rsrv_sq']); + return [ 'success' => true, + 'rec_tel' => $data['rec_tel'], + 'rec_nm' => $data['rec_nm'], + 'remark' => $data['rec_remark'], + 'record' => $record ]; } + // 거주인 정보 조회 + public function getRecInfoByRcptSq($rcpt_sq) + { + try { + // 기존 getDetail 메서드 활용 (이미 최적화된 쿼리) + $data = $this->getDetail($rcpt_sq); + + if (empty($data)) { + log_message('error', '[ReceiptModel::getRecInfoByRcptSq] Receipt not found for rcpt_sq: ' . $rcpt_sq); + return null; + } + + // 녹취파일 정보 조회 (기존 getRecordInfo 활용) + $record = null; + if (!empty($data['rsrv_sq'])) { + $record = $this->getRecordInfo($data['rsrv_sq']); + } + + return [ + 'rec_tel' => $data['rec_tel'] ?? '', + 'rec_nm' => $data['rec_nm'] ?? '', + 'remark' => $data['remark'] ?? '', + 'record' => $record ?: null + ]; + } catch (\Exception $e) { + log_message('error', '[ReceiptModel::getRecInfoByRcptSq] Exception: ' . $e->getMessage()); + throw $e; + } + } + // 워터마크 조회 public function getWatermarkList($rcpt_cpid) { diff --git a/app/Views/pages/article/dept/detail.php b/app/Views/pages/article/dept/detail.php index 12e22b0..e349059 100644 --- a/app/Views/pages/article/dept/detail.php +++ b/app/Views/pages/article/dept/detail.php @@ -2675,9 +2675,10 @@ $usr_level = session('usr_level'); title: "정상 처리되었습니다.", icon: "success", draggable: true - }) - - location.reload(); + }).then(() => { + // 녹취파일 체크박스 활성화 및 체크 + $('#chk_record').prop('disabled', false).prop('checked', true); + }); } else { Swal.fire({ title: result.msg, diff --git a/app/Views/pages/article/receipt/detail.php b/app/Views/pages/article/receipt/detail.php index 3706726..3c16dcd 100644 --- a/app/Views/pages/article/receipt/detail.php +++ b/app/Views/pages/article/receipt/detail.php @@ -1,4 +1,4 @@ - @@ -457,7 +457,7 @@ $usr_level = session('usr_level'); - + @@ -1936,37 +1936,47 @@ $usr_level = session('usr_level');
-
정보변경 이력
+
정보변경 이력 + (총 건) +
- - - - - - - - - - +
진행상태변경내용처리자(ID)처리일시세부내용
+ + + + + + + + + + + + + + + + + + + + + - - - - - + - +
진행상태변경내용처리자(ID)처리일시세부내용
+ + + + + + + + + +
- - - - - - - - - - 이력이 없습니다.
@@ -2363,2081 +2373,13 @@ $usr_level = session('usr_level'); + endSection() ?> \ No newline at end of file diff --git a/app/Views/pages/article/record/detail.php b/app/Views/pages/article/record/detail.php index 12e22b0..e349059 100644 --- a/app/Views/pages/article/record/detail.php +++ b/app/Views/pages/article/record/detail.php @@ -2675,9 +2675,10 @@ $usr_level = session('usr_level'); title: "정상 처리되었습니다.", icon: "success", draggable: true - }) - - location.reload(); + }).then(() => { + // 녹취파일 체크박스 활성화 및 체크 + $('#chk_record').prop('disabled', false).prop('checked', true); + }); } else { Swal.fire({ title: result.msg, diff --git a/public/js/article/receipt/detail.js b/public/js/article/receipt/detail.js new file mode 100644 index 0000000..2732c87 --- /dev/null +++ b/public/js/article/receipt/detail.js @@ -0,0 +1,2210 @@ +// 거주인 녹취정보 상세 페이지 JavaScript +// 외부 의존성: jQuery, Bootstrap 5, SweetAlert2, Naver Maps, Dropzone 6.0, Sortable.js 1.15 + +var map; +var dz = null; // Dropzone 인스턴스 +var sortable = null; // Sortable 인스턴스 + +$(function () { + + trade_type_onchange(); + + if (isDefined(window.rcpt_hscp_nm)) { + $(".spc").hide(); + } + + map = new naver.maps.Map('mapArea', { + center: new naver.maps.LatLng(window.lat, window.lng), + useStyleMap: true, + zoom: 17, + minZoom: 10, + mapTypeControl: true, + mapTypeControlOptions: { + style: naver.maps.MapTypeControlStyle.BUTTON, + position: naver.maps.Position.TOP_LEFT + }, + zoomControl: true, + zoomControlOptions: { + position: naver.maps.Position.TOP_RIGHT + } + }); + + marker = new naver.maps.Marker({ + position: new naver.maps.LatLng(window.lat, window.lng), + map: map + }); + + // Dropzone auto discover 비활성화 + Dropzone.autoDiscover = false; + + // 파일업로드 open + $("#btnUploadModal").on("click", function () { + $("#uploadModal").modal("show"); + }); + +}); + +//공백 값 체크 +function isDefined(str) { + var isResult = false; + str_temp = str + ""; + str_temp = str_temp.replace(" ", ""); + if (str_temp != "undefined" && str_temp != "" && str_temp != "null") { + isResult = true; + } + + return isResult; +} + +// 부모 창에서 삭제된 이미지 제거하는 함수 +function removeImageFromParent(imgType, imgSq) { + console.log('[removeImageFromParent] 이미지 제거:', imgType, imgSq); + + // 이미지 타입에 따라 다른 처리 + switch(imgType) { + case 'I1': // 홍보확인서 - 단일 이미지를 photo.gif로 변경 + $('#photo-display2_I1').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('홍보확인서'); + break; + + case 'I8': // 분양권 - 해당 div 제거 + $('img[src*="' + imgSq + '"]').closest('div[style*="width: 150px"]').remove(); + break; + + case 'I4': // 매물사진 - 해당 thumb-card 제거 + $('input[name="i4_seq[]"][value="' + imgSq + '"]').closest('.thumb-card').remove(); + break; + + case 'I9': // 360이미지 - 해당 thumb-card 제거 + $('input[name^="img_location_' + imgSq + '"]').closest('.thumb-card').remove(); + break; + + case 'I10': // 촬영동의서 - 이미지를 photo.gif로 변경 + $('img[alt="촬영동의서"]').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('촬영동의서'); + break; + + case 'I2': // 현장확인내역서 - 이미지를 photo.gif로 변경 + $('img[alt="현장확인내역서"]').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('현장확인내역서'); + break; + + case 'V1': // 동영상 - 이미지를 photo.gif로 변경 + $('img[alt="동영상"]').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('동영상'); + break; + + case 'I5': // 평면도 - 이미지를 photo.gif로 변경 + $('img[alt="평면도"]').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('평면도'); + break; + + case 'I11': // 체크리스트 - 이미지를 photo.gif로 변경 + $('img[alt="체크리스트"]').attr('src', '/plugin/img/photo.gif') + .parent('a').replaceWith('체크리스트'); + break; + + default: + console.log('[removeImageFromParent] 알 수 없는 이미지 타입:', imgType); + } +} + +// 부모 창에 업로드된 이미지 추가/업데이트하는 함수 +function updateImageInParent(imgType, imageData) { + console.log('[updateImageInParent] 이미지 업데이트:', imgType, imageData); + + if (!imageData || !imageData.img_path || !imageData.img_filenm) { + console.error('[updateImageInParent] 이미지 데이터 부족:', imageData); + return; + } + + // 이미지 URL 생성 + const imgPath = imageData.img_path + imageData.img_filenm; + const thumbPath = imageData.img_path + imageData.img_filenm.replace(/\.\w+$/, '_thumb.jpg'); + + // 클라우드 URL 처리 + const NCLOUD_URL = 'https://kr.object.ncloudstorage.com/confirms-object/'; + const fullImageUrl = imageData.cloud_upload_yn === 'Y' ? NCLOUD_URL + imgPath : imgPath; + const thumbImageUrl = imageData.cloud_upload_yn === 'Y' ? NCLOUD_URL + thumbPath : thumbPath; + + // 이미지 타입에 따라 다른 처리 + switch(imgType) { + case 'I1': // 홍보확인서 - 단일 이미지 업데이트 + const $i1Container = $('#photo-display2_I1').closest('.ratio'); + $i1Container.html(` + + 홍보확인서 + + `); + break; + + case 'I10': // 촬영동의서 + const $i10Container = $('img[alt="촬영동의서"]').closest('.ratio'); + $i10Container.html(` + + 촬영동의서 + + `); + break; + + case 'I2': // 현장확인내역서 + const $i2Container = $('img[alt="현장확인내역서"]').closest('.ratio'); + $i2Container.html(` + + 현장확인내역서 + + `); + break; + + case 'I8': // 분양권 - 새 이미지 추가 + const $i8Container = $('img[alt="분양권"]').closest('.d-flex'); + // photo.gif인 placeholder 제거 + $i8Container.find('img[src="/plugin/img/photo.gif"]').closest('div[style*="width: 150px"]').remove(); + + // 새 이미지 추가 + const i8Html = ` +
+
+ + 분양권 + +
+
+ `; + $i8Container.append(i8Html); + break; + + case 'I4': // 매물사진 - 새 thumb-card 추가 + const $i4Container = $('input[name="i4_order[]"]').closest('.d-flex'); + // photo.gif인 placeholder 제거 + $i4Container.find('img[src="/plugin/img/photo.gif"]').closest('.thumb-card').remove(); + + // 새 thumb-card 추가 + const i4Html = ` +
+ + +
+ + 매물사진 + +
+
+ `; + $i4Container.append(i4Html); + break; + + case 'I9': // 360이미지 - 새 thumb-card 추가 + console.log('[updateImageInParent] I9 처리 시작', imageData); + + // 360이미지 컨테이너 찾기 + let $i9Container = $('.fw-semibold:contains("360이미지")').closest('.border').find('.d-flex.flex-wrap'); + console.log('[updateImageInParent] I9 컨테이너 찾음:', $i9Container.length); + + if ($i9Container.length === 0) { + console.error('[updateImageInParent] I9 컨테이너를 찾을 수 없습니다!'); + break; + } + + // placeholder 제거 (photo.gif가 있으면) + const $placeholder = $i9Container.find('img[src="/plugin/img/photo.gif"]').closest('.thumb-card'); + if ($placeholder.length > 0) { + console.log('[updateImageInParent] I9 placeholder 제거'); + $placeholder.remove(); + } + + // 새 이미지 추가 + const i9Html = ` +
+
+ + 360이미지 + +
+ +
+ `; + console.log('[updateImageInParent] I9 이미지 추가'); + $i9Container.append(i9Html); + break; + + case 'V1': // 동영상 + const $v1Container = $('img[alt="동영상"]').closest('.ratio'); + $v1Container.html(` + + 동영상 + + `); + break; + + case 'I5': // 평면도 + const $i5Container = $('img[alt="평면도"]').closest('.ratio'); + $i5Container.html(` + + 평면도 + + `); + break; + + case 'I11': // 체크리스트 + const $i11Container = $('img[alt="체크리스트"]').closest('.ratio'); + $i11Container.html(` + + 체크리스트 + + `); + break; + + default: + console.log('[updateImageInParent] 알 수 없는 이미지 타입:', imgType); + } +} + +// AJAX로 이미지 리스트 전체 불러오기 +var allImagesCache = []; // 이미지 데이터 캠시 + +function loadAllImages(rsrvSq) { + console.log('[loadAllImages] 시작, rsrv_sq:', rsrvSq); + + if (!rsrvSq) { + console.warn('[loadAllImages] rsrv_sq가 없음'); + return; + } + + $.ajax({ + url: '/article/receipt/getImages', + method: 'GET', + data: { rsrv_sq: rsrvSq }, + success: function(response) { + console.log('[loadAllImages] 응답:', response); + + if (response.success && response.images) { + const images = response.images; + allImagesCache = images; // 캠시에 저장 + console.log('[loadAllImages] 이미지 개수:', images.length); + + // 이미지 타입별로 렌더링 + renderImagesByType('I1', images); // 홍보확인서 + renderImagesByType('I10', images); // 촬영동의서 + renderImagesByType('I2', images); // 현장확인내역서 + renderImagesByType('I8', images); // 분양권 + renderImagesByType('I4', images); // 매물사진 + renderImagesByType('V1', images); // 동영상 + renderImagesByType('I5', images); // 평면도 + renderImagesByType('I11', images); // 체크리스트 + renderImagesByType('I9', images); // 360이미지 + + console.log('[loadAllImages] 렌더링 완료'); + } else { + console.error('[loadAllImages] 응답 오류:', response.msg || '알 수 없음'); + } + }, + error: function(xhr, status, error) { + console.error('[loadAllImages] AJAX 오류:', error); + } + }); +} + +// 이미지 타입별로 렌더링 +function renderImagesByType(imgType, allImages) { + const images = allImages.filter(img => img.img_type === imgType); + console.log('[renderImagesByType]', imgType, ':', images.length, '개'); + + switch(imgType) { + case 'I1': // 홍보확인서 (단일) + renderSingleImage(images[0], 'photo-display2_I1', '홍보확인서'); + break; + + case 'I10': // 촬영동의서 (단일) + renderSingleImage(images[0], null, '촬영동의서'); + break; + + case 'I2': // 현장확인내역서 (단일) + renderSingleImage(images[0], null, '현장확인내역서'); + break; + + case 'V1': // 동영상 (단일) + renderSingleImage(images[0], null, '동영상'); + break; + + case 'I5': // 평면도 (단일) + renderSingleImage(images[0], null, '평면도'); + break; + + case 'I11': // 체크리스트 (단일) + renderSingleImage(images[0], null, '체크리스트'); + break; + + case 'I8': // 분양권 (다중) + renderMultipleImages(images, 'I8', '분양권'); + break; + + case 'I4': // 매물사진 (다중, 순서O) + renderPropertyImages(images); + break; + + case 'I9': // 360이미지 (다중, 위치O) + render360Images(images); + break; + } +} + +// 단일 이미지 렌더링 +function renderSingleImage(img, imgId, altText) { + // 이미지가 없으면 기본 이미지 표시 + if (!img) { + const defaultHtml = `${altText}`; + + if (imgId) { + const existing = $(`#${imgId}`); + if (existing.length > 0) { + if (existing.parent('a').length > 0) { + existing.parent('a').replaceWith(defaultHtml); + } else { + existing.replaceWith(defaultHtml); + } + } + } else { + const existing = $(`img[alt="${altText}"]`); + if (existing.length > 0) { + if (existing.parent('a').length > 0) { + existing.parent('a').replaceWith(defaultHtml); + } else { + existing.replaceWith(defaultHtml); + } + } + } + return; + } + + const originalUrl = img.original_url || img.thumbnail_url; + const thumbnailUrl = img.thumbnail_url || img.original_url; + + const html = ` + ${altText} + `; + + if (imgId) { + $(`#${imgId}`).parent().replaceWith(html); + } else { + $(`img[alt="${altText}"]`).parent().replaceWith(html); + } +} + +// 다중 이미지 렌더링 (분양권용) +function renderMultipleImages(images, imgType, altText) { + const container = $(`img[alt="${altText}"]`).closest('.d-flex.flex-wrap'); + if (container.length === 0) return; + + container.empty(); + + // 이미지가 없으면 기본 이미지 표시 + if (images.length === 0) { + const defaultHtml = ` +
+
+ ${altText} +
+
+ `; + container.append(defaultHtml); + return; + } + + images.forEach(img => { + const originalUrl = img.original_url || img.thumbnail_url; + const thumbnailUrl = img.thumbnail_url || img.original_url; + + const html = ` +
+
+ + ${altText} + +
+
+ `; + container.append(html); + }); +} + +// 매물사진 렌더링 (분양권과 동일한 형태) +function renderPropertyImages(images) { + const container = $('#property-images-container'); + if (container.length === 0) return; + + container.empty(); + + // 이미지가 없으면 기본 이미지 표시 + if (images.length === 0) { + const defaultHtml = ` +
+
+ 매물사진 +
+
+ `; + container.append(defaultHtml); + return; + } + + // view_odr 기준으로 정렬 + images.sort((a, b) => (a.view_odr || 0) - (b.view_odr || 0)); + + images.forEach((img, index) => { + const originalUrl = img.original_url || img.thumbnail_url; + const thumbnailUrl = img.thumbnail_url || img.original_url; + const orderNum = index + 1; + + const html = ` +
+
${orderNum}
+
+ + 매물사진 + +
+
+ `; + container.append(html); + }); + + // Sortable 초기화 + initPropertyImagesSortable(); +} + +// 360이미지 렌더링 (위치 input 포함) +function render360Images(images) { + const container = $('img[alt="360이미지"]').closest('.d-flex.flex-wrap'); + if (container.length === 0) return; + + container.empty(); + + // 이미지가 없으면 기본 이미지 표시 + if (images.length === 0) { + const defaultHtml = ` +
+
+ 360이미지 +
+ +
+ `; + container.append(defaultHtml); + return; + } + + images.forEach(img => { + const originalUrl = img.original_url || img.thumbnail_url; + const thumbnailUrl = img.thumbnail_url || img.original_url; + const imgLocation = img.img_location || ''; + const imgSq = img.img_sq || ''; + + const html = ` +
+
+ + 360이미지 + +
+ +
+ `; + container.append(html); + }); +} + +// 특정 타입의 모든 이미지 일괄삭제 +function deleteAllImagesByType(imgType, imgTypeName) { + console.log('[deleteAllImagesByType] 시작:', imgType, imgTypeName); + + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + const rcptSq = $("#frm_file_info [name=rcpt_sq]").val(); + + if (!rsrvSq || !rcptSq) { + alert('필수 정보가 없습니다.'); + return; + } + + // 먼저 사용자 확인 + if (!confirm(`${imgTypeName} 이미지를 모두 삭제하시겠습니까?`)) { + return; + } + + // 로딩 화면 표시 + blockUI.blockPage({ + message: '
Loading...

삭제 중...

' + }); + + // 해당 타입의 이미지 목록 조회 후 삭제 + $.ajax({ + url: '/article/receipt/getImages', + method: 'GET', + data: { + rsrv_sq: rsrvSq, + img_type: imgType + }, + success: function(response) { + console.log('[deleteAllImagesByType] 조회 응답:', response); + + if (response.success && response.images && response.images.length > 0) { + const images = response.images; + const imageCount = images.length; + + // 삭제 Promise 배열 + const deletePromises = images.map(img => { + return $.ajax({ + url: '/article/receipt/removeUploadFile', + method: 'POST', + data: { + rcpt_sq: rcptSq, + img_sq: img.img_sq + } + }); + }); + + // 모든 삭제 완료 대기 + Promise.all(deletePromises) + .then(function(results) { + console.log('[deleteAllImagesByType] 모든 삭제 완료:', results); + + // 성공/실패 카운트 + const successCount = results.filter(r => r.code === '0').length; + const failCount = imageCount - successCount; + + // 로딩 화면 제거 + blockUI.unblockPage(); + + if (failCount > 0) { + alert(`${successCount}개 삭제 성공, ${failCount}개 실패`); + } else { + alert(`${imgTypeName} 이미지 ${successCount}개가 삭제되었습니다.`); + } + + // AJAX로 이미지 재로딩 + loadAllImages(rsrvSq); + }) + .catch(function(error) { + console.error('[deleteAllImagesByType] 삭제 오류:', error); + + // 로딩 화면 제거 + blockUI.unblockPage(); + + alert('이미지 삭제 중 오류가 발생했습니다.'); + loadAllImages(rsrvSq); // 오류 발생해도 재로딩 + }); + } else { + // 로딩 화면 제거 + blockUI.unblockPage(); + + alert(`삭제할 ${imgTypeName} 이미지가 없습니다.`); + } + }, + error: function(xhr, status, error) { + console.error('[deleteAllImagesByType] 조회 오류:', error); + + // 로딩 화면 제거 + blockUI.unblockPage(); + + alert('이미지 조회 중 오류가 발생했습니다.'); + } + }); +} + +// 매물사진 Sortable 초기화 +var propertyImagesSortable = null; + +function initPropertyImagesSortable() { + const container = document.getElementById('property-images-container'); + if (!container) { + console.warn('[initPropertyImagesSortable] 컨테이너를 찾을 수 없음'); + return; + } + + // 기존 Sortable 제거 + if (propertyImagesSortable) { + propertyImagesSortable.destroy(); + propertyImagesSortable = null; + } + + // 이미지가 없으면 초기화하지 않음 + const items = container.querySelectorAll('.property-image-item[data-img-sq]'); + if (items.length === 0) { + console.log('[initPropertyImagesSortable] 이미지가 없어서 Sortable 초기화 안 함'); + return; + } + + propertyImagesSortable = new Sortable(container, { + animation: 150, + ghostClass: 'sortable-ghost', + dragClass: 'sortable-drag', + draggable: '.property-image-item', + onStart: function(evt) { + // 드래그 시작시 클릭 이벤트 막기 + const links = container.querySelectorAll('a'); + links.forEach(link => { + link.style.pointerEvents = 'none'; + }); + }, + onEnd: function(evt) { + console.log('[PropertyImagesSortable] 순서 변경:', evt.oldIndex, '->', evt.newIndex); + updatePropertyImageOrderBadges(); + + // 드래그 종료 후 클릭 이벤트 복구 (약간의 딜레이 후) + setTimeout(function() { + const links = container.querySelectorAll('a'); + links.forEach(link => { + link.style.pointerEvents = ''; + }); + }, 100); + } + }); + + console.log('[initPropertyImagesSortable] Sortable 초기화 완료, 이미지 수:', items.length); +} + +// 매물사진 순서 배지 업데이트 +function updatePropertyImageOrderBadges() { + const items = document.querySelectorAll('#property-images-container .property-image-item'); + items.forEach((item, index) => { + const badge = item.querySelector('.image-order-badge'); + if (badge) { + badge.textContent = index + 1; + } + }); + console.log('[updatePropertyImageOrderBadges] 배지 업데이트 완료'); +} + +// 360이미지 촬영위치 저장 +function save360ImageLocations() { + console.log('[save360ImageLocations] 촬영위치 저장 시작'); + + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (!rsrvSq) { + alert('예약번호가 없습니다.'); + return; + } + + // img_location으로 시작하는 모든 input 수집 + const locationInputs = $('input[id^="img_location_"]'); + + if (locationInputs.length === 0) { + alert('저장할 360이미지가 없습니다.'); + return; + } + + // 각 input의 값을 수집하고 저장 + const savePromises = []; + let savedCount = 0; + + locationInputs.each(function() { + const imgSq = $(this).attr('id').replace('img_location_', ''); + const location = $(this).val(); + + console.log('[save360ImageLocations] img_sq:', imgSq, 'location:', location); + + const promise = $.ajax({ + url: '/article/receipt/saveImgLocation', + method: 'POST', + data: { + img_sq: imgSq, + rsrv_sq: rsrvSq, + location: location + } + }).done(function(response) { + if (response.code === '0') { + savedCount++; + console.log('[save360ImageLocations] 저장 완료:', imgSq); + } else { + console.error('[save360ImageLocations] 저장 실패:', response.msg); + } + }).fail(function(xhr, status, error) { + console.error('[save360ImageLocations] AJAX 오류:', error); + }); + + savePromises.push(promise); + }); + + // 모든 저장 완료 대기 + Promise.all(savePromises).then(function() { + console.log('[save360ImageLocations] 모든 촬영위치 저장 완료:', savedCount); + alert(`촬영위치가 저장되었습니다. (${savedCount}개)`); + }).catch(function(error) { + console.error('[save360ImageLocations] 저장 중 오류:', error); + alert('촬영위치 저장 중 오류가 발생했습니다.'); + }); +} + +// 이미지 타입별 일괄 다운로드 +function downloadImagesByType(imgType, imgTypeName) { + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + + if (!rsrvSq) { + alert('예약번호가 없습니다.'); + return; + } + + // 해당 타입의 이미지 개수 확인 + let imageCount = 0; + if (allImagesCache && allImagesCache.length > 0) { + imageCount = allImagesCache.filter(img => img.img_type === imgType).length; + } + + if (imageCount === 0) { + alert('다운로드할 ' + imgTypeName + ' 이미지가 없습니다.'); + return; + } + + if (!confirm(`${imgTypeName} ${imageCount}장을 다운로드하시겠습니까?`)) { + return; + } + + // 로딩 화면 표시 + blockUI.blockPage({ + message: '
Loading...

다운로드 준비 중...

' + }); + + // 다운로드 URL 생성 및 실행 + const downloadUrl = `/article/receipt/downloadAllImages?rsrv_sq=${rsrvSq}&img_type=${imgType}`; + + // iframe을 사용한 다운로드 (페이지 리로드 방지) + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = downloadUrl; + document.body.appendChild(iframe); + + // 다운로드 완료 후 정리 (3초 후) + setTimeout(function() { + blockUI.unblockPage(); + document.body.removeChild(iframe); + console.log(`[downloadImagesByType] ${imgTypeName} 다운로드 완료`); + }, 3000); +} + +// 매물사진 순서 저장 +function savePropertyImageOrder() { + const items = document.querySelectorAll('#property-images-container .property-image-item[data-img-sq]'); + + if (items.length === 0) { + alert('저장할 이미지가 없습니다.'); + return; + } + + // 순서 데이터 수집 + const orderData = []; + items.forEach((item, index) => { + const imgSq = item.getAttribute('data-img-sq'); + if (imgSq) { + orderData.push({ + img_sq: imgSq, + view_odr: index + 1 + }); + } + }); + + console.log('[savePropertyImageOrder] 저장할 순서:', orderData); + + if (!confirm(`매물사진 순서를 저장하시겠습니까? (총 ${orderData.length}장)`)) { + return; + } + + // 로딩 화면 표시 + blockUI.blockPage({ + message: '
Loading...

저장 중...

' + }); + + // 서버에 저장 요청 + $.ajax({ + url: '/article/receipt/updateImageOrder', + method: 'POST', + data: { + rcpt_sq: $("#frm_file_info [name=rcpt_sq]").val(), + img_type: 'I4', + orders: JSON.stringify(orderData) + }, + success: function(response) { + blockUI.unblockPage(); + + if (response.code === '0' || response.success) { + alert('매물사진 순서가 저장되었습니다.'); + + // 재로딩하여 최신 상태 반영 + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (rsrvSq) { + loadAllImages(rsrvSq); + } + } else { + alert('순서 저장 중 오류가 발생했습니다: ' + (response.msg || response.message || '알 수 없음')); + } + }, + error: function(xhr, status, error) { + blockUI.unblockPage(); + console.error('[savePropertyImageOrder] 오류:', error); + alert('순서 저장 중 오류가 발생했습니다.'); + } + }); +} + +function trade_type_onchange() { + var trade_type = $('#trade_type').val(); + if (trade_type == 'B2' || trade_type == 'B3') { + // 월세... + $('#div_trade_type_price_monthly').show(); + } else { + $('#div_trade_type_price_monthly').hide(); + } +} + +// 가격수정 btn +function editPriceInfo() { + var rcpt_product = $('#rcpt_product').val(); + var trade_type = $('#trade_type').val(); + + $("#trade_type").prop("disabled", false); + + $("#rcpt_product_info2").prop("disabled", false); + $("#rcpt_product_info3").prop("disabled", false); + + if (trade_type == "A1") { + if (rcpt_product == 'A01' || rcpt_product == 'A02' || rcpt_product == 'A03' || rcpt_product == 'B01' || rcpt_product == 'B02' || rcpt_product == 'B03') { + $("#rcpt_product_info4").prop("disabled", false); + $("#rcpt_product_info5").prop("disabled", false); + } + } +} + +// 가격수정 저장 +function modifyPriceInfo() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + var params = { + 'rcpt_sq': window.rcpt_sq, + 'agent_tel': $("#agent_tel").val(), + }; + + callAjax("/article/receipt/modifyPriceInfo", params, fn_result); + } + }); +} + +// 연락가능전화 저장 +function fn_save_tel() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + var params = { + 'rcpt_sq': window.rcpt_sq, + 'agent_tel': $("#agent_tel").val(), + }; + + callAjax("/article/receipt/saveAptMemo", params, fn_result); + } + }); +} + +// 평면도요청 저장 +function res_ground() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/resGround", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +// 거주여부 변경 이벤트 +function dbYn_change(value) { + // 거주여부가 Y이면 DB활용동의여부 활성화, N이면 비활성화 + if (value === 'Y') { + $('#dbUsageAgrYn').prop('disabled', false); + } else { + $('#dbUsageAgrYn').prop('disabled', true); + $('#dbUsageAgrYn').val('N'); + } +} + +// 거주여부 저장 +function saveResDbYn() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/resDbYn", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +//등록일로부터 3개월 체크 +function assign(mon, menuid) { + var frm = document.getElementById('rcptFrm'); + var to = frm.rsrv_date.value; + var mon_chk = mon.split("-"); + var to_chk = to.split("-"); + var date1 = new Date(mon_chk[0], mon_chk[1], mon_chk[2]).valueOf(); + var date2 = new Date(to_chk[0], to_chk[1], to_chk[2]).valueOf(); + if (date2 - date1 < 0) { + assignRegist(menuid); + } else { + Swal.fire({ + title: "등록일로부터 3개월 이전만 가능합니다.", + icon: "warning" + }) + return; + } +} + +/** + * 배정자 등록 + */ +function assignRegist(menuid) { + var frm = document.rcptFrm; + + if (frm.rsrv_date.value == "") { + Swal.fire({ + title: "방문희망일시를 선택해 주세요.", + icon: "warning" + }) + return; + } + + var date = new Date(); + var yy = date.getFullYear(); + var mm = date.getMonth() + 1; + var dd = date.getDate(); + if (mm < 10) mm = "0" + mm; + if (dd < 10) dd = "0" + dd; + var today = yy + mm + dd; + var rsrv = frm.rsrv_date.value.replace(/-/gi, ""); + + if (parseInt(today) > parseInt(rsrv)) { + Swal.fire({ + title: "방문희망일시는 금일 이전날짜는 불가능합니다.", + icon: "warning" + }) + return; + } + if (frm.rsrv_tm_ap.value == "") { + Swal.fire({ + title: "오전/오후를 선택해 주세요.", + icon: "warning" + }) + return; + } + if (frm.rsrv_tm_hour.value == "") { + Swal.fire({ + title: "시간을 선택해 주세요.", + icon: "warning" + }) + return; + } + if (frm.bonbu.value == "") { + Swal.fire({ + title: "본부를 선택해 주세요.", + icon: "warning" + }) + return; + } + if (frm.dept_sq.value == "") { + Swal.fire({ + title: "팀을 선택해 주세요.", + icon: "warning" + }) + return; + } + if (frm.usr_sq.value == "") { + Swal.fire({ + title: "담당자를 선택해 주세요.", + icon: "warning" + }) + return; + } + + + swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/assignRegist", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +// 동영상 촬영여부저장 +function requestMovie() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/requestMovie", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +// 중개사메모 저장 +function requestMessage() { + Swal.fire({ + text: "저장 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/requestMessage", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +// 예약취소 +function rsrvcancel() { + var stat = frm.rcpt_stat1.value; + var cd2 = frm.result_cd2.value; + + if (stat == '70' && cd2 == '9030') { + Swal.fire({ + title: "방문 전 취소가 불가능합니다.", + icon: "warning" + }) + return; + } + + if (frm.result_cd2.value == "") { + Swal.fire({ + title: "분류1을 선택해 주세요.", + icon: "warning" + }) + return; + } + if (frm.result_cd3.value == "") { + Swal.fire({ + title: "분류2를 선택해 주세요.", + icon: "warning" + }) + return; + } + + if (frm.result_msg.value == "") { + Swal.fire({ + title: "취소사유를 입력해 주세요.", + icon: "warning" + }) + return; + } + + Swal.fire({ + text: "취소 하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/rsrvcancel", $("#rcptFrm").serialize(), fn_result); + } + }); +} + +// 상태변경 +function chgStatus(cd, rcpt_product, chg_floor_yn) { + if (cd == "") return; + + var frm = document.rcptFrm; + var i1 = frm.I1.value; // 홍보확인서 + var i4 = frm.I4.value; // 매물사진 + var rr_yn = frm.req_rec_yn.value; //녹취필요여부 + var chk_record = frm.chk_record.value; //녹취파일 확인 체크 + var r_yn = frm.rec_yn.value; //녹취 완료여부 + var req_rec_yn = "N"; + + var rcpt_product = frm.rcpt_product.value; + var r = ''; + +} + +// 문자발송 modal +function viewSmsPop(cd) { + + if (window.smsArr.length > 0) { + var tel = ""; + if (cd == "S7" || cd == "S14") { + tel = window.rec_tel; + } else if (cd == "S10") { + tel = window.agent_contact_tel; + } else if (cd == "15") { + tel = window.agent_contact_tel; + } else { + tel = window.agent_head_tel; + } + + + for (const sms of window.smsArr) { + if (sms.cd == cd) { + $("#smsForm [name=cd]").val(cd); + $("#smsForm [name=phone]").val(tel); + $("#smsForm [name=content]").val(sms.cd_nm); + } + } + } + + $("#smsModal").modal("show"); +} + +// 문자발송 +function sendSms() { + if ($("#smsForm [name=phone]").val() == "") { + Swal.fire({ + title: "수신번호를 입력해 주세요.", + icon: "warning" + }) + return; + } + + if ($("#smsForm [name=content]").val() == "") { + Swal.fire({ + title: "내용을 입력해 주세요.", + icon: "warning" + }) + return; + } + + Swal.fire({ + text: "SMS를 발송하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + callAjax("/article/receipt/sendSms", $("#smsForm").serialize(), fn_result); + } + }); +} + +function showFileName(input) { + if (input.files && input.files.length > 0) { + document.getElementById('file_name').textContent = input.files[0].name; + } +} + +// 거주인 녹취정보 저장 +function saveRecInfo() { + const rec_tel1 = $("#rec_tel1").val(); + const rec_tel2 = $("#rec_tel2").val(); + const rec_tel3 = $("#rec_tel3").val(); + + if (rec_tel1 == "" || rec_tel2 == "" || rec_tel3 == "") { + Swal.fire({ + title: "거주자 전화번호를 입력해 주세요.", + icon: "warning" + }); + + return; + } + + Swal.fire({ + text: "거주인정보를 저장하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "예", + cancelButtonText: "아니오", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + var form = $('#rcptFrm')[0]; + var formData = new FormData(form); + + $.ajax({ + url: "/article/receipt/saveRecInfo", + method: 'POST', + data: formData, + processData: false, + contentType: false, + cache: false, + beforeSend: function () { + blockUI.blockPage({ + message: tpl + }) + }, + complete: function () { + blockUI.unblockPage() + }, + success: function (result) { + if (result.code == '0') { + Swal.fire({ + title: "정상 처리되었습니다.", + icon: "success", + draggable: true + }).then(() => { + // 서버에서 반환된 데이터로 화면 갱신 + if (result.data) { + updateRecInfoUI(result.data); + } + }); + } else { + Swal.fire({ + title: result.msg, + icon: "error", + draggable: true + }) + } + } + }); + } + }); + + +} + +// 거주인 정보 UI 업데이트 +function updateRecInfoUI(data) { + // 전화번호 업데이트 + if (data.rec_tel) { + const telParts = data.rec_tel.split('-'); + $('input[name="rec_tel1"]').val(telParts[0] || ''); + $('input[name="rec_tel2"]').val(telParts[1] || ''); + $('input[name="rec_tel3"]').val(telParts[2] || ''); + } + + // 거주인 이름 업데이트 + $('input[name="rec_nm"]').val(data.rec_nm || ''); + + // 거주인 요청사항 업데이트 + $('textarea[name="rec_remark"]').val(data.remark || ''); + + // 음성파일 정보 업데이트 + const fileInfoHtml = data.record ? ` +
+ + ${data.record.record_orignm} + + ${data.record.record_size} KB + ${data.record.insert_tm} +
+ ` : '등록된 파일 없음'; + + // 음성파일 영역 업데이트 (파일 업로드 버튼 앞까지) + const fileContainer = $('input[name="rec_file"]').closest('td').find('.d-flex').first(); + fileContainer.find('.d-flex.align-items-center.gap-2.flex-wrap, .text-muted.small').first().replaceWith(fileInfoHtml); + + // 녹취파일 체크박스 활성화 및 체크 + if (data.record) { + $('#chk_record').prop('disabled', false).prop('checked', true); + } else { + $('#chk_record').prop('disabled', true).prop('checked', false); + } + + // 파일명 표시 초기화 + $('#file_name').text(''); + $('input[name="rec_file"]').val(''); +} + +// 거주인 정보 다시 불러오기 +function loadRecInfo() { + const rcpt_sq = $('input[name="rcpt_sq"]').val(); + + $.ajax({ + url: "/article/receipt/getRecInfo", + method: 'GET', + data: { rcpt_sq: rcpt_sq }, + dataType: 'json', + success: function (result) { + if (result.code == '0' && result.data) { + updateRecInfoUI(result.data); + } + }, + error: function() { + console.error('거주인 정보를 불러오는데 실패했습니다.'); + } + }); +} + + + +function fn_preview(src, type = 'img') { + + const $img = $('#imgPreview'); + const $video = $('#vdoPreview'); + const video = document.getElementById('vdoPreview'); + const source = document.getElementById('videoSource'); + + if (type === 'vdo') { + // 이미지 숨김 + $img.hide().attr('src', ''); + + // video source 세팅 + source.src = src; + + // video 표시 + 로드 + $video.show(); + video.load(); + + $('#previewTitle').text('동영상 미리보기'); + } else { + // video 정지 및 초기화 + video.pause(); + source.src = ''; + video.load(); + $video.hide(); + + // 이미지 표시 + $img.attr('src', src).show(); + + $('#previewTitle').text('이미지 미리보기'); + } + + const modal = new bootstrap.Modal(document.getElementById('previewModal')); + modal.show(); +} + +function callAjax(target, params, callback) { + $.ajax({ + url: target, + contentType: 'application/x-www-form-urlencoded;charset=UTF-8', + method: "POST", + data: params, + beforeSend: function () { + blockUI.blockPage({ + message: tpl + }) + }, + complete: function () { + blockUI.unblockPage() + }, + success: function (result) { + callback(result); + } + }); +} + + +function fn_result(result) { + if (result.code == '0') { + Swal.fire({ + title: "정상 처리되었습니다.", + icon: "success", + draggable: true + }); + + // 정보변경 이력 갱신 + loadHistory(); + } else { + Swal.fire({ + title: result.msg, + icon: "error", + draggable: true + }) + } +} + +// 정보변경 이력 AJAX 로드 +function loadHistory() { + const rcptSq = $("#rcptFrm input[name='rcpt_sq']").val(); + console.log('[loadHistory] rcpt_sq:', rcptSq); + + if (!rcptSq) { + console.warn('[loadHistory] rcpt_sq가 없습니다.'); + return; + } + + $.ajax({ + url: "/article/receipt/getHistory", + type: "GET", + data: { rcpt_sq: rcptSq }, + success: function(result) { + console.log('[loadHistory] 응답:', result); + if (result.code === '0' && result.data) { + console.log('[loadHistory] 이력 데이터:', result.data); + updateHistoryUI(result.data); + } else { + console.warn('[loadHistory] 올바르지 않은 응답:', result); + } + }, + error: function(xhr, status, error) { + console.error('[History] 로드 실패:', error); + console.error('[History] 응답:', xhr.responseText); + } + }); +} + +// 정보변경 이력 UI 업데이트 +function updateHistoryUI(historyData) { + const $historyTable = $('#history-table'); + if (!$historyTable.length) { + console.error('[updateHistoryUI] 정보변경 이력 테이블을 찾을 수 없습니다.'); + return; + } + + let $tbody = $historyTable.find('tbody'); + if (!$tbody.length) { + // tbody가 없으면 생성 + const $thead = $historyTable.find('thead'); + if ($thead.length) { + $tbody = $('').insertAfter($thead); + } + } + + $tbody.empty(); + + if (historyData && historyData.length > 0) { + historyData.forEach(h => { + const row = ` + + ${h.rcpt_stat_nm || ''} + ${h.changed_type_nm || ''} + ${h.changed_id || ''} + ${h.changed_tm || ''} + ${h.remark || ''} + + `; + $tbody.append(row); + }); + } else { + $tbody.append('이력이 없습니다.'); + } +} + +// Dropzone 초기화 함수 +function initDropzone(imgType) { + console.log('[Dropzone] initDropzone 호출, imgType:', imgType); + + // 기존 Dropzone 인스턴스가 있으면 제거 + if (dz) { + console.log('[Dropzone] 기존 인스턴스 제거'); + dz.destroy(); + dz = null; + } + + // 파일 타입별 최대 개수 설정 + const maxFilesConfig = { + 'I4': 15, // 매물사진 + 'I8': 5, // 평면도 + 'I9': 5, // 녹취파일 + 'V1': 5 // 동영상 + }; + + const maxFiles = maxFilesConfig[imgType] || 10; // 기본 10개 + console.log('[Dropzone] maxFiles:', maxFiles); + + let uploadStarted = false; + let uploadedCount = 0; + let totalFiles = 0; + + dz = new Dropzone("#myDropzone", { + url: "/article/receipt/uploadFile", + method: "post", + paramName: "files", + autoProcessQueue: false, // 자동 업로드 끄기 + uploadMultiple: false, // 파일을 개별적으로 업로드 + parallelUploads: 1, // 한 번에 한 개씩 업로드 + maxFiles: maxFiles, // 최대 파일 수 + maxFilesize: 100, + addRemoveLinks: true, + clickable: "#uploadPick", // 파일선택 버튼만 클릭 가능 + dictRemoveFile: "삭제", + dictDefaultMessage: "최대 " + maxFiles + "개", + dictFallbackMessage: "브라우저가 드래그앤드롭을 지원하지 않습니다.", + dictFileTooBig: "파일이 너무 큽니다 (최대 {{maxFilesize}}MB)", + dictInvalidFileType: "허용되지 않은 파일 형식입니다.", + dictResponseError: "서버 오류가 발생했습니다.", + dictCancelUpload: "업로드 취소", + dictMaxFilesExceeded: "최대 " + maxFiles + "개까지만 업로드할 수 있습니다.", + }); + + dz.on("addedfile", function (file) { + console.log('[Dropzone] addedfile 후처리:', file.name, 'isExisting:', file.isExisting); + + // 교체 버튼 추가 + const filePreview = file.previewElement; + const removeButton = filePreview.querySelector('.dz-remove'); + + if (removeButton && !filePreview.querySelector('.dz-replace')) { + // 버튼 영역 div 생성 + const buttonsDiv = document.createElement('div'); + buttonsDiv.className = 'file-buttons'; + + // 교체 버튼 생성 + const replaceButton = document.createElement('a'); + replaceButton.href = 'javascript:void(0);'; + replaceButton.className = 'dz-replace'; + replaceButton.textContent = '교체'; + replaceButton.onclick = function(e) { + e.preventDefault(); + e.stopPropagation(); + replaceFile(file); + }; + + // 기존 삭제 버튼을 buttonsDiv로 감싸기 + removeButton.parentNode.insertBefore(buttonsDiv, removeButton); + buttonsDiv.appendChild(removeButton); + buttonsDiv.appendChild(replaceButton); + + // 기존 파일인 경우 삭제 확인 메시지 추가 + if (file.isExisting) { + removeButton.textContent = 'DB삭제'; + removeButton.style.color = '#dc3545'; + + // 삭제 버튼 클릭 시 확인 메시지 + const originalHref = removeButton.getAttribute('data-dz-remove'); + removeButton.removeAttribute('data-dz-remove'); + removeButton.onclick = function(e) { + e.preventDefault(); + e.stopPropagation(); + if (confirm('이 파일을 DB와 클라우드에서 삭제하시겠습니까?\n\n파일명: ' + file.name)) { + dz.removeFile(file); + } + }; + } + } + + // 파일 순서 배지 추가 + updateFileOrderBadges(); + + // Sortable 초기화 (파일이 추가될 때마다) + initSortable(); + }); + + dz.on("removedfile", function (file) { + console.log('[Dropzone] removedfile:', file.name, 'isExisting:', file.isExisting, 'isDeleting:', file.isDeleting, 'isModalClosing:', window.isModalClosing); + + // 모달이 닫히는 중에는 서버 삭제 요청 하지 않음 (UI 정리만 수행) + if (window.isModalClosing) { + console.log('[Dropzone] 모달 닫힘 중이므로 서버 삭제 요청 안 함'); + return; + } + + // 기존 파일(DB에 있는 파일)인 경우 서버에서 삭제 + if (file.isExisting && file.img_sq) { + // 이미 삭제 중인 경우 중복 요청 방지 + if (file.isDeleting) { + console.log('[Dropzone] 이미 삭제 중인 파일:', file.img_sq); + return; + } + + file.isDeleting = true; // 삭제 플래그 설정 + console.log('[Dropzone] 기존 파일 삭제 요청, img_sq:', file.img_sq); + + // 삭제 Promise 저장 (전체삭제 시 사용) + if (!window.deletePromises) { + window.deletePromises = []; + } + + const deletePromise = $.ajax({ + url: '/article/receipt/removeUploadFile', + method: 'POST', + data: { + rcpt_sq: $("#frm_file_info [name=rcpt_sq]").val(), + img_sq: file.img_sq + } + }).done(function(response) { + console.log('[Dropzone] 파일 삭제 완료:', response); + if (response.code === '0') { + // 개별 삭제인 경우 (전체삭제가 아닌 경우) + if (!window.isBulkDeleting) { + alert('파일이 삭제되었습니다.'); + // AJAX로 이미지 재로딩 + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (rsrvSq) { + loadAllImages(rsrvSq); + } + } + } else { + console.error('[Dropzone] 삭제 응답 에러:', response.msg); + file.isDeleting = false; // 실패 시 플래그 해제 + if (!window.isBulkDeleting) { + alert('파일 삭제 중 오류가 발생했습니다: ' + (response.msg || '알 수 없는 오류')); + } + } + }).fail(function(xhr, status, error) { + console.error('[Dropzone] 파일 삭제 실패:', error); + file.isDeleting = false; // 실패 시 플래그 해제 + if (!window.isBulkDeleting) { + alert('파일 삭제 중 오류가 발생했습니다.'); + } + }); + + window.deletePromises.push(deletePromise); + } + + // 파일 삭제 후 순서 배지 업데이트 + setTimeout(updateFileOrderBadges, 100); + }); + + dz.on("sending", function (file, xhr, formData) { + console.log('[Dropzone] sending 이벤트:', file.name, 'isExisting:', file.isExisting); + + // 기존 파일은 업로드하지 않음 + if (file.isExisting) { + console.log('[Dropzone] 기존 파일이므로 업로드 스킵:', file.name); + return false; + } + + // 현재 순서 찾기 + const fileIndex = dz.files.indexOf(file); + + formData.append("rcpt_key", $("#frm_file_info [name=rcpt_key]").val()); + formData.append("rsrv_sq", $("#frm_file_info [name=rsrv_sq]").val()); + formData.append("rcpt_sq", $("#frm_file_info [name=rcpt_sq]").val()); + formData.append("img_type", $("#frm_file_info [name=img_type]").val()); + formData.append("img_sub_type", $("#frm_file_info [name=img_sub_type]").val()); + formData.append("vr_sq", $("#frm_file_info [name=vr_sq]").val()); + formData.append("file_order", fileIndex + 1); // 파일 순서 추가 + + uploadStarted = true; + }); + + dz.on("processing", function (file) { + console.log('[Dropzone] processing 이벤트:', file.name); + }); + + dz.on("success", function (file, response) { + console.log('[Dropzone] success 이벤트:', file.name, response); + uploadedCount++; + console.log('[Dropzone] 업로드 진행:', uploadedCount + '/' + totalFiles); + + // 업로드된 파일 정보 저장 + if (!window.uploadedFiles) { + window.uploadedFiles = []; + } + + // 서버 응답에서 이미지 정보 추출 + if (response && response.code === '0' && response.data) { + window.uploadedFiles.push({ + imgType: $("#frm_file_info [name=img_type]").val(), + imageData: response.data + }); + console.log('[Dropzone] 업로드된 파일 정보 저장:', response.data); + } + + // 다음 파일이 있으면 처리 + if (dz.getQueuedFiles().length > 0) { + console.log('[Dropzone] 다음 파일 처리 시작'); + dz.processQueue(); + } + }); + + dz.on("error", function (file, errorMessage, xhr) { + console.log('[Dropzone] error 이벤트:', file.name, errorMessage, 'status:', file.status); + uploadedCount++; + + // 모달이 닫히는 중이거나 파일이 취소된 경우 에러 메시지 표시 안 함 + if (window.isModalClosing || file.status === 'canceled' || errorMessage === 'Upload canceled.') { + console.log('[Dropzone] 모달 닫힘 중이거나 취소된 파일이므로 에러 무시'); + return; + } + + // 에러 메시지 파싱 + let displayMessage = '업로드 실패: ' + file.name; + if (typeof errorMessage === 'object' && errorMessage.msg) { + displayMessage = errorMessage.msg; + } else if (typeof errorMessage === 'string') { + displayMessage = errorMessage; + } + + // 사용자에게 에러 알림 + alert('⚠️ 파일 업로드 실패\n\n' + displayMessage); + + // 파일에 에러 표시 + if (file.previewElement) { + file.previewElement.classList.add('dz-error'); + } + + // 업로드 중단 (다음 파일 처리 안 함) + uploadStarted = false; + console.log('[Dropzone] 에러로 인해 업로드 중단'); + }); + + dz.on("queuecomplete", function () { + console.log('[Dropzone] queuecomplete 이벤트, uploadStarted:', uploadStarted, 'uploadedCount:', uploadedCount, 'totalFiles:', totalFiles); + if (uploadStarted && uploadedCount >= totalFiles && totalFiles > 0) { + console.log('[Dropzone] 모든 파일 업로드 완료'); + + // 업로드된 파일 목록 초기화 + window.uploadedFiles = []; + + // AJAX로 최신 상태 불러오기 + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (rsrvSq) { + console.log('[Dropzone] AJAX로 이미지 재로딩 시작'); + loadAllImages(rsrvSq); + } else { + console.warn('[Dropzone] rsrv_sq가 없어서 재로딩 불가'); + } + + alert('파일이 업로드되었습니다.'); + + // 모달 닫기 + $("#uploadModal").modal("hide"); + } + }); + + // 버튼 이벤트 재등록 (파일선택 버튼은 Dropzone이 자동 처리) + + $("#btnUpload").off("click").on("click", function () { + console.log('[Dropzone] btnUpload 클릭'); + + // 새로 추가된 파일만 필터링 (기존 파일 제외) + const newFiles = dz.files.filter(file => !file.isExisting && file.status === Dropzone.QUEUED); + const existingFiles = dz.files.filter(file => file.isExisting); + + console.log('[Dropzone] 전체 파일:', dz.files.length, '기존 파일:', existingFiles.length, '새 파일:', newFiles.length); + + if (newFiles.length === 0) { + if (existingFiles.length > 0) { + alert("변경사항이 저장되었습니다."); + } else { + alert("업로드할 파일을 먼저 선택해주세요."); + } + return; + } + + totalFiles = newFiles.length; + uploadedCount = 0; + + console.log('[Dropzone] 업로드 시작, totalFiles:', totalFiles); + uploadStarted = true; + dz.processQueue(); // 업로드 실행 + }); + + $("#btnRemove").off("click").on("click", function () { + console.log('[Dropzone] btnRemove 클릭'); + const files = dz.files.slice(); // 배열 복사 + + if (files.length === 0) { + alert("삭제할 파일이 없습니다."); + return; + } + + const existingFilesCount = files.filter(f => f.isExisting).length; + const newFilesCount = files.length - existingFilesCount; + + let confirmMsg = `총 ${files.length}개의 파일을 삭제하시겠습니까?`; + if (existingFilesCount > 0) { + confirmMsg += `\n- 기존 파일 ${existingFilesCount}개 (DB 및 클라우드에서 삭제)`; + } + if (newFilesCount > 0) { + confirmMsg += `\n- 신규 파일 ${newFilesCount}개 (목록에서만 제거)`; + } + + if (!confirm(confirmMsg)) { + return; + } + + // 전체삭제 플래그 설정 + window.isBulkDeleting = true; + window.deletePromises = []; + window.deletedFiles = []; // 삭제된 파일 정보 저장 + + // 모든 파일 삭제 (removedfile 이벤트에서 AJAX 호출) + files.forEach(function (file) { + if (file.isExisting && file.img_sq) { + window.deletedFiles.push({ + imgSq: file.img_sq, + imgType: $("#frm_file_info [name=img_type]").val() + }); + } + dz.removeFile(file); + }); + + // 기존 파일이 있었으면 삭제 완료 대기 + if (existingFilesCount > 0) { + console.log('[Dropzone] 삭제 대기 중... Promise 개수:', window.deletePromises.length); + + Promise.all(window.deletePromises) + .then(function() { + console.log('[Dropzone] 모든 파일 삭제 완료'); + window.isBulkDeleting = false; + window.isModalClosing = true; // 모달 닫힘 플래그 설정 + + // AJAX로 이미지 재로딩 + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (rsrvSq) { + loadAllImages(rsrvSq); + } + + alert('파일이 삭제되었습니다.'); + + // 모달 닫기 + $("#uploadModal").modal("hide"); + }) + .catch(function(error) { + console.error('[Dropzone] 삭제 중 오류:', error); + window.isBulkDeleting = false; + alert('일부 파일 삭제 중 오류가 발생했습니다.'); + location.reload(); + }); + } else { + // 신규 파일만 있었으면 즉시 완료 + window.isBulkDeleting = false; + window.isModalClosing = true; // 모달 닫힘 플래그 설정 + alert('파일이 목록에서 제거되었습니다.'); + + // 모달 닫기 + $("#uploadModal").modal("hide"); + } + }); + + // Sortable 초기화 함수 + function initSortable() { + // 기존 Sortable 제거 + if (sortable) { + sortable.destroy(); + } + + const dropzoneElement = document.querySelector("#myDropzone .dz-preview"); + if (!dropzoneElement || !dropzoneElement.parentElement) { + return; + } + + sortable = new Sortable(dropzoneElement.parentElement, { + animation: 150, + ghostClass: 'sortable-ghost', + handle: '.dz-image', + draggable: '.dz-preview', + onEnd: function(evt) { + console.log('[Sortable] 순서 변경:', evt.oldIndex, '->', evt.newIndex); + + // Dropzone 파일 배열 순서 업데이트 + const movedFile = dz.files[evt.oldIndex]; + dz.files.splice(evt.oldIndex, 1); + dz.files.splice(evt.newIndex, 0, movedFile); + + // 순서 배지 업데이트 + updateFileOrderBadges(); + } + }); + } + + // 파일 교체 함수 + function replaceFile(oldFile) { + console.log('[Dropzone] 파일 교체 시작:', oldFile.name); + + // 숨겨진 파일 input 생성 + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.style.display = 'none'; + + input.onchange = function(e) { + const newFile = e.target.files[0]; + if (!newFile) return; + + console.log('[Dropzone] 새 파일 선택됨:', newFile.name); + + // 기존 파일의 인덱스 찾기 + const oldIndex = dz.files.indexOf(oldFile); + + // 기존 파일 제거 (UI에서만) + dz.removeFile(oldFile); + + // 새 파일 추가 + dz.addFile(newFile); + + // 파일 순서 재정렬 (새 파일을 원래 위치로) + setTimeout(() => { + const newIndex = dz.files.length - 1; + if (oldIndex !== newIndex && oldIndex < dz.files.length) { + const movedFile = dz.files[newIndex]; + dz.files.splice(newIndex, 1); + dz.files.splice(oldIndex, 0, movedFile); + + // DOM 순서도 변경 + const previews = document.querySelectorAll('#myDropzone .dz-preview'); + const newPreview = previews[previews.length - 1]; + const parent = newPreview.parentNode; + const targetPreview = previews[oldIndex]; + parent.insertBefore(newPreview, targetPreview); + } + + updateFileOrderBadges(); + }, 100); + }; + + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + } + + // 파일 순서 배지 업데이트 함수 + function updateFileOrderBadges() { + const previews = document.querySelectorAll('#myDropzone .dz-preview'); + previews.forEach((preview, index) => { + // 기존 배지 제거 + const existingBadge = preview.querySelector('.file-order-badge'); + if (existingBadge) { + existingBadge.remove(); + } + + // 새 배지 추가 - 이미지 위에 표시 + const badge = document.createElement('div'); + badge.className = 'file-order-badge'; + badge.textContent = index + 1; + + const imageDiv = preview.querySelector('.dz-image'); + if (imageDiv) { + imageDiv.style.position = 'relative'; + imageDiv.appendChild(badge); + } + }); + } +} + +// 부모 페이지의 이미지 업데이트 +function updateParentImage(imgType) { + if (!imgType) { + console.log('[updateParentImage] imgType이 없음'); + return; + } + + // I8 등 여러 이미지를 갤러리로 표시하는 타입은 업데이트 복잡하므로 스킵 + const multiImageTypes = ['I8', 'I4', 'I5', 'I9']; + if (multiImageTypes.includes(imgType)) { + console.log('[updateParentImage] 복수 이미지 타입, 업데이트 스킵:', imgType); + return; + } + + const rsrv_sq = $("#frm_file_info [name=rsrv_sq]").val(); + if (!rsrv_sq) { + console.log('[updateParentImage] rsrv_sq가 없음'); + return; + } + + console.log('[updateParentImage] 부모 페이지 이미지 업데이트 시작:', imgType); + + // 해당 img_type의 남은 이미지 조회 + $.ajax({ + url: '/article/receipt/getImages', + method: 'GET', + data: { + rsrv_sq: rsrv_sq, + img_type: imgType + }, + success: function(response) { + console.log('[updateParentImage] 응답:', response); + + const imgElement = $('#photo-display2_' + imgType); + if (imgElement.length === 0) { + console.log('[updateParentImage] 이미지 엘리먼트를 찾을 수 없음, 업데이트 스킵:', '#photo-display2_' + imgType); + return; + } + + const parentAnchor = imgElement.parent('a'); + + if (response.success && response.images && response.images.length > 0) { + // 첫 번째 이미지로 업데이트 + const firstImage = response.images[0]; + imgElement.attr('src', firstImage.original_url); + + // a 태그가 있으면 onclick도 업데이트 + if (parentAnchor.length > 0) { + parentAnchor.attr('onclick', "fn_preview('" + firstImage.original_url + "')"); + } + + console.log('[updateParentImage] 이미지 업데이트 완료:', firstImage.original_url); + } else { + // 이미지가 없으면 기본 이미지로 + imgElement.attr('src', '/plugin/img/photo.gif'); + + // a 태그가 있으면 제거 (기본 이미지는 클릭 불가) + if (parentAnchor.length > 0) { + parentAnchor.removeAttr('onclick'); + } + + console.log('[updateParentImage] 기본 이미지로 변경'); + } + }, + error: function(xhr, status, error) { + console.error('[updateParentImage] AJAX 실패, 업데이트 스킵:', error); + } + }); +} + +// 기존 이미지를 Dropzone에 로드 +function loadExistingImages(imgType, imgSubType) { + console.log('[loadExistingImages] 호출:', imgType, imgSubType); + + const rsrv_sq = $("#frm_file_info [name=rsrv_sq]").val(); + if (!rsrv_sq) { + console.log('[loadExistingImages] rsrv_sq가 없음'); + return; + } + + $.ajax({ + url: '/article/receipt/getImages', + method: 'GET', + data: { + rsrv_sq: rsrv_sq, + img_type: imgType + }, + success: function(response) { + console.log('[loadExistingImages] 응답:', response); + + if (response.success && response.images && response.images.length > 0) { + response.images.forEach(function(img) { + // 가짜 File 객체 생성 + const mockFile = { + name: img.img_filenm, + size: img.file_size || 0, + img_sq: img.img_sq, + img_type: img.img_type, + isExisting: true, // 기존 파일 표시 + accepted: true, + status: Dropzone.ADDED, + original_url: img.original_url + }; + + // Dropzone에 파일 추가 + dz.emit("addedfile", mockFile); + dz.emit("thumbnail", mockFile, img.thumbnail_url); + dz.emit("complete", mockFile); + + // 썸네일 이미지 로드 실패 시 원본으로 대체 + setTimeout(function() { + const preview = mockFile.previewElement; + if (preview) { + const thumbnailImg = preview.querySelector('[data-dz-thumbnail]'); + if (thumbnailImg) { + thumbnailImg.onerror = function() { + console.log('[loadExistingImages] 썸네일 로드 실패, 원본 사용:', mockFile.name); + this.src = img.original_url; + this.onerror = null; // 무한 루프 방지 + }; + } + } + }, 100); + + // files 배열에 추가 (Dropzone 내부 상태 관리) + dz.files.push(mockFile); + + console.log('[loadExistingImages] 파일 추가됨:', mockFile.name); + }); + } else { + console.log('[loadExistingImages] 이미지 없음'); + } + }, + error: function(xhr, status, error) { + console.error('[loadExistingImages] 실패:', error); + } + }); +} + +// 파일업로드 모달 오픈 +function viewFilePop(imgType, imgSubType) { + console.log('[viewFilePop] 호출:', imgType, imgSubType); + $("#frm_file_info [name=img_type]").val(imgType || ""); + $("#frm_file_info [name=img_sub_type]").val(imgSubType || ""); + + // Dropzone 초기화 (imgType 전달) + initDropzone(imgType); + + // 기존 이미지 로드 + setTimeout(function() { + loadExistingImages(imgType, imgSubType); + }, 300); + + $("#uploadModal").modal("show"); +} + +// 모달 닫힘 이벤트 처리 +$(document).ready(function() { + // 중복 등록 방지를 위해 기존 핸들러 제거 후 재등록 + $('#uploadModal').off('hide.bs.modal').on('hide.bs.modal', function () { + console.log('[Modal] ========== hide.bs.modal 시작 =========='); + console.log('[Modal] isModalClosing 설정 전:', window.isModalClosing); + window.isModalClosing = true; + console.log('[Modal] isModalClosing 설정 후:', window.isModalClosing); + + // 중요: Dropzone removeFile() 호출하지 않음! + // removeFile()을 호출하면 removedfile 이벤트가 발생하여 의도치 않은 삭제가 발생할 수 있음 + // 대신 모달이 완전히 닫힌 후 Dropzone을 destroy하여 정리함 + if (dz) { + console.log('[Modal] 현재 Dropzone 파일 목록:'); + dz.files.forEach(function(f, idx) { + console.log('[Modal] ' + (idx+1) + '. ' + f.name + ', isExisting:', f.isExisting + ', status:', f.status + ', img_sq:', f.img_sq); + }); + console.log('[Modal] 주의: 파일 제거 하지 않음 (destroy로 정리 예정)'); + } + }); + + $('#uploadModal').off('hidden.bs.modal').on('hidden.bs.modal', function () { + console.log('[Modal] ========== hidden.bs.modal 시작 =========='); + + // 삭제 플래그 초기화 + window.isBulkDeleting = false; + window.deletePromises = []; + window.uploadedFiles = []; + + if (sortable) { + console.log('[Modal] Sortable 정리'); + sortable.destroy(); + sortable = null; + } + if (dz) { + console.log('[Modal] Dropzone 정리 (destroy)'); + dz.destroy(); + dz = null; + } + + // 모든 정리 완료 후 플래그 초기화 + window.isModalClosing = false; + console.log('[Modal] 모든 정리 완료, isModalClosing:', window.isModalClosing); + }); + + // ========== AJAX로 이미지 불러오기 ========== + // 페이지 로드 시 한 번에 모든 이미지를 불러와서 렌더링 + const rsrvSq = $("#frm_file_info [name=rsrv_sq]").val(); + if (rsrvSq) { + console.log('[PageLoad] AJAX로 이미지 로딩 시작, rsrv_sq:', rsrvSq); + loadAllImages(rsrvSq); + } else { + console.warn('[PageLoad] rsrv_sq가 없어서 이미지를 불러올 수 없습니다.'); + } +});