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:
kappa
2026-01-10 17:22:43 +09:00
parent 6f92cacfea
commit b8adee1825

View File

@@ -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;
}