From b8adee18254af20a1713fc0a0ffb57367d490b1f Mon Sep 17 00:00:00 2001 From: kappa Date: Sat, 10 Jan 2026 17:22:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=20R2=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(QA,=20=ED=9A=8C=EC=9B=90,=20=EC=87=BC?= =?UTF-8?q?=ED=95=91=EB=AA=B0,=20=EC=BB=A8=ED=85=90=EC=B8=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1:1문의(QA) 첨부파일 업로드/다운로드 R2 연동 - 회원 아이콘/프로필 이미지 R2 마이그레이션 - 쇼핑몰 상품 이미지 R2 URL 지원 - 내용관리/FAQ 이미지 R2 마이그레이션 - 에디터 컨텐츠 URL R2 변환 - 관리자 회원 수정 R2 마이그레이션 Co-Authored-By: Claude Opus 4.5 --- extend/r2_hooks.php | 427 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) diff --git a/extend/r2_hooks.php b/extend/r2_hooks.php index 03e528b..c7b0e04 100644 --- a/extend/r2_hooks.php +++ b/extend/r2_hooks.php @@ -903,3 +903,430 @@ function r2_test_connection(): array return ['success' => false, 'error' => $e->getMessage()]; } } + +// ============================================ +// 1:1문의(QA) 첨부파일 R2 훅 +// ============================================ + +/** + * 1:1문의 파일 업로드 후 R2로 마이그레이션 + */ +add_event('qawrite_update', 'r2_hook_qa_upload', 10, 5); + +function r2_hook_qa_upload($qa_id, $write, $w, $qaconfig, $answer_id = null) +{ + if (!is_r2_enabled()) { + return; + } + + global $g5, $member; + + $handler = get_r2_handler(); + $member_id = isset($member['mb_no']) ? (int)$member['mb_no'] : 0; + + // 현재 QA 레코드 조회 + $qa = sql_fetch("SELECT qa_file1, qa_file2 FROM {$g5['qa_content_table']} WHERE qa_id = '{$qa_id}'"); + + // 파일1, 파일2 처리 + for ($i = 1; $i <= 2; $i++) { + $filename = $qa["qa_file{$i}"]; + if (empty($filename)) { + continue; + } + + $localPath = G5_DATA_PATH . '/qa/' . $filename; + if (!file_exists($localPath)) { + continue; + } + + // R2 키 생성 + $r2Key = $handler->generateR2Key($member_id, 'qa', $filename, 'qa'); + + // R2로 마이그레이션 + $result = $handler->migrateLocalFile($localPath, $r2Key, true); + + if ($result['success']) { + // DB에 R2 키 저장 (bf_r2_key 필드가 없으므로 파일명에 prefix 추가) + $newFilename = 'r2::' . $r2Key; + sql_query("UPDATE {$g5['qa_content_table']} SET qa_file{$i} = '" . sql_escape_string($newFilename) . "' WHERE qa_id = '{$qa_id}'"); + error_log("[R2] QA file migrated: {$filename} -> {$r2Key}"); + } else { + error_log("[R2] QA file migration failed: {$filename} - " . ($result['error'] ?? 'Unknown')); + } + } +} + +/** + * 1:1문의 파일 다운로드 존재 확인 + */ +add_replace('qa_download_file_exist_check', 'r2_hook_qa_download_check', 10, 2); + +function r2_hook_qa_download_check($exists, $file) +{ + if (!is_r2_enabled()) { + return $exists; + } + + // R2 파일인지 확인 + if (strpos($file, 'r2::') === 0) { + $r2Key = substr($file, 4); + $handler = get_r2_handler(); + + try { + return $handler->getAdapter()->exists($r2Key); + } catch (\Exception $e) { + error_log("[R2] QA download check failed: " . $e->getMessage()); + return false; + } + } + + return $exists; +} + +/** + * 1:1문의 파일 다운로드 리다이렉트 + */ +add_event('qa_download_file_header', 'r2_hook_qa_download_redirect', 10, 2); + +function r2_hook_qa_download_redirect($file, $filename) +{ + if (!is_r2_enabled()) { + return; + } + + // R2 파일인지 확인 + if (strpos($file, 'r2::') === 0) { + $r2Key = substr($file, 4); + $handler = get_r2_handler(); + + try { + $presignedUrl = $handler->getDownloadUrl($r2Key, $filename); + if ($presignedUrl) { + header("Location: {$presignedUrl}"); + exit; + } + } catch (\Exception $e) { + error_log("[R2] QA download redirect failed: " . $e->getMessage()); + } + } +} + +// ============================================ +// 회원 아이콘/이미지 R2 훅 +// ============================================ + +/** + * 회원 가입/수정 후 아이콘/이미지 R2 마이그레이션 + */ +add_event('register_form_update_after', 'r2_hook_member_image_upload', 10, 2); + +function r2_hook_member_image_upload($mb_id, $w) +{ + if (!is_r2_enabled()) { + return; + } + + $handler = get_r2_handler(); + $mb_dir = substr($mb_id, 0, 2); + $icon_name = get_mb_icon_name($mb_id) . '.gif'; + + // 회원 아이콘 처리 + $iconPath = G5_DATA_PATH . '/member/' . $mb_dir . '/' . $icon_name; + if (file_exists($iconPath)) { + $r2Key = "members/{$mb_id}/icon/{$icon_name}"; + $result = $handler->migrateLocalFile($iconPath, $r2Key, false); // 로컬 파일 유지 + + if ($result['success']) { + error_log("[R2] Member icon uploaded: {$mb_id} -> {$r2Key}"); + } + } + + // 회원 프로필 이미지 처리 + $imgPath = G5_DATA_PATH . '/member_image/' . $mb_dir . '/' . $icon_name; + if (file_exists($imgPath)) { + $r2Key = "members/{$mb_id}/profile/{$icon_name}"; + $result = $handler->migrateLocalFile($imgPath, $r2Key, false); // 로컬 파일 유지 + + if ($result['success']) { + error_log("[R2] Member profile image uploaded: {$mb_id} -> {$r2Key}"); + } + } +} + +/** + * 관리자 회원 수정 후 아이콘/이미지 R2 마이그레이션 + */ +add_event('admin_member_form_update_after', 'r2_hook_admin_member_image_upload', 10, 2); + +function r2_hook_admin_member_image_upload($mb_id, $w) +{ + // 동일한 처리 로직 사용 + r2_hook_member_image_upload($mb_id, $w); +} + +// ============================================ +// 에디터 이미지 URL 변환 (content 내 이미지) +// ============================================ + +/** + * 에디터 콘텐츠 URL을 R2 URL로 변환 + */ +add_replace('get_editor_content_url', 'r2_hook_editor_content_url', 10, 1); + +// ============================================ +// 쇼핑몰 상품 이미지 R2 훅 +// ============================================ + +/** + * 상품 이미지 존재 확인 (R2 지원) + */ +add_replace('is_exists_item_file', 'r2_hook_shop_image_exists', 10, 3); + +function r2_hook_shop_image_exists($exists, $it, $i) +{ + if (!is_r2_enabled()) { + return $exists; + } + + // 로컬에 있으면 그대로 반환 + if ($exists) { + return $exists; + } + + // R2에서 확인 + $it_id = $it['it_id'] ?? ''; + if (empty($it_id)) { + return $exists; + } + + $handler = get_r2_handler(); + $r2Key = "shop/item/{$it_id}/{$i}"; + + try { + return $handler->getAdapter()->exists($r2Key); + } catch (\Exception $e) { + return $exists; + } +} + +/** + * 상품 이미지 정보 (R2 URL 반환) + */ +add_replace('get_image_by_item', 'r2_hook_shop_image_info', 10, 4); + +function r2_hook_shop_image_info($infos, $it, $i, $size) +{ + if (!is_r2_enabled()) { + return $infos; + } + + $it_id = $it['it_id'] ?? ''; + if (empty($it_id)) { + return $infos; + } + + $handler = get_r2_handler(); + $r2Key = "shop/item/{$it_id}/{$i}"; + + try { + if ($handler->getAdapter()->exists($r2Key)) { + $presignedUrl = $handler->getDownloadUrl($r2Key); + if ($presignedUrl) { + $infos['src'] = $presignedUrl; + $infos['r2'] = true; + } + } + } catch (\Exception $e) { + error_log("[R2] Shop image info failed: " . $e->getMessage()); + } + + return $infos; +} + +/** + * 상품 이미지 태그 (R2 URL 지원) + */ +add_replace('get_it_image_tag', 'r2_hook_shop_image_tag', 10, 9); + +function r2_hook_shop_image_tag($img, $thumb, $it_id, $width, $height, $anchor, $img_id, $img_alt, $is_crop) +{ + if (!is_r2_enabled()) { + return $img; + } + + // 이미 R2 URL이면 그대로 반환 + if (strpos($img, 'r2.cloudflarestorage.com') !== false) { + return $img; + } + + // 로컬 이미지 경로에서 R2 URL로 변환 시도 + if (preg_match('/src=["\']([^"\']+)["\']/', $img, $matches)) { + $src = $matches[1]; + + // shop/item 경로인 경우 + if (preg_match('/\/item\/([^\/]+)\/([^\/\."\']+)/', $src, $itemMatches)) { + $itemId = $itemMatches[1]; + $imgNum = $itemMatches[2]; + + $handler = get_r2_handler(); + $r2Key = "shop/item/{$itemId}/{$imgNum}"; + + try { + if ($handler->getAdapter()->exists($r2Key)) { + $presignedUrl = $handler->getDownloadUrl($r2Key); + if ($presignedUrl) { + $img = str_replace($src, $presignedUrl, $img); + } + } + } catch (\Exception $e) { + error_log("[R2] Shop image tag failed: " . $e->getMessage()); + } + } + } + + return $img; +} + +// ============================================ +// 내용관리/FAQ 이미지 R2 훅 (goto_url 이벤트 활용) +// ============================================ + +/** + * 내용관리 이미지 업로드 후 R2 마이그레이션 + */ +add_event('admin_content_created', 'r2_hook_content_image_migrate', 20, 1); +add_event('admin_content_updated', 'r2_hook_content_image_migrate', 20, 1); + +function r2_hook_content_image_migrate($co_id) +{ + if (!is_r2_enabled()) { + return; + } + + // 업로드가 완료된 후 (goto_url 전) 파일 마이그레이션 예약 + // register_shutdown_function을 사용하여 스크립트 종료 직전에 마이그레이션 수행 + register_shutdown_function('r2_migrate_content_images', $co_id); +} + +function r2_migrate_content_images($co_id) +{ + if (!is_r2_enabled()) { + return; + } + + $handler = get_r2_handler(); + + // 헤더 이미지 + $himg_path = G5_DATA_PATH . "/content/{$co_id}_h"; + if (file_exists($himg_path)) { + $r2Key = "content/{$co_id}_h"; + $result = $handler->migrateLocalFile($himg_path, $r2Key, false); + if ($result['success']) { + error_log("[R2] Content header image migrated: {$co_id}"); + } + } + + // 타이틀 이미지 + $timg_path = G5_DATA_PATH . "/content/{$co_id}_t"; + if (file_exists($timg_path)) { + $r2Key = "content/{$co_id}_t"; + $result = $handler->migrateLocalFile($timg_path, $r2Key, false); + if ($result['success']) { + error_log("[R2] Content title image migrated: {$co_id}"); + } + } +} + +/** + * FAQ 마스터 이미지 업로드 후 R2 마이그레이션 + */ +add_event('admin_faq_master_created', 'r2_hook_faq_image_migrate', 20, 1); +add_event('admin_faq_master_updated', 'r2_hook_faq_image_migrate', 20, 1); + +function r2_hook_faq_image_migrate($fm_id) +{ + if (!is_r2_enabled()) { + return; + } + + register_shutdown_function('r2_migrate_faq_images', $fm_id); +} + +function r2_migrate_faq_images($fm_id) +{ + if (!is_r2_enabled()) { + return; + } + + $handler = get_r2_handler(); + + // 헤더 이미지 + $himg_path = G5_DATA_PATH . "/faq/{$fm_id}_h"; + if (file_exists($himg_path)) { + $r2Key = "faq/{$fm_id}_h"; + $result = $handler->migrateLocalFile($himg_path, $r2Key, false); + if ($result['success']) { + error_log("[R2] FAQ header image migrated: {$fm_id}"); + } + } + + // 타이틀 이미지 + $timg_path = G5_DATA_PATH . "/faq/{$fm_id}_t"; + if (file_exists($timg_path)) { + $r2Key = "faq/{$fm_id}_t"; + $result = $handler->migrateLocalFile($timg_path, $r2Key, false); + if ($result['success']) { + error_log("[R2] FAQ title image migrated: {$fm_id}"); + } + } +} + +/** + * 관리자 회원 수정 후 아이콘/이미지 R2 마이그레이션 (이벤트명 수정) + */ +add_event('admin_member_form_update', 'r2_hook_admin_member_upload', 10, 2); + +function r2_hook_admin_member_upload($w, $mb_id) +{ + r2_hook_member_image_upload($mb_id, $w); +} + +function r2_hook_editor_content_url($url) +{ + if (!is_r2_enabled()) { + return $url; + } + + // 이미 R2 URL이면 그대로 반환 + if (strpos($url, 'r2.cloudflarestorage.com') !== false) { + return $url; + } + + // 로컬 에디터 경로인 경우 R2 URL로 변환 시도 + if (preg_match('/\/data\/editor\/(\d+)\/(.+)$/', $url, $matches)) { + $date = $matches[1]; + $filename = $matches[2]; + + // R2 키 패턴으로 검색 + $handler = get_r2_handler(); + + // users/*/editor/날짜/파일명 패턴으로 검색 + try { + $searchPattern = "editor/" . substr($date, 0, 4) . "-" . substr($date, 4, 2); + $objects = $handler->getAdapter()->listObjects($searchPattern, 100); + + foreach ($objects as $obj) { + if (strpos($obj['Key'], $filename) !== false) { + $presignedUrl = $handler->getDownloadUrl($obj['Key']); + if ($presignedUrl) { + return $presignedUrl; + } + } + } + } catch (\Exception $e) { + error_log("[R2] Editor content URL conversion failed: " . $e->getMessage()); + } + } + + return $url; +}