feat: 추가 R2 훅 구현 (QA, 회원, 쇼핑몰, 컨텐츠)
- 1:1문의(QA) 첨부파일 업로드/다운로드 R2 연동 - 회원 아이콘/프로필 이미지 R2 마이그레이션 - 쇼핑몰 상품 이미지 R2 URL 지원 - 내용관리/FAQ 이미지 R2 마이그레이션 - 에디터 컨텐츠 URL R2 변환 - 관리자 회원 수정 R2 마이그레이션 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user