From 672d05f9682230f1ff5678ee581c40fa2b2b9855 Mon Sep 17 00:00:00 2001 From: whitedot Date: Tue, 8 Sep 2026 06:49:33 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=BC=20=EB=AF=B8=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EB=A7=8C=EB=A3=8C=20=ED=9A=8C=EC=9B=90=EC=9D=84=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=ED=83=88=ED=87=B4=20=EC=A0=88=EC=B0=A8?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메일 인증 기한이 지난 미인증 회원을 일일 정리에서 탈퇴 처리하고 본인확인 정보와 소셜 연결을 해제한다. 나머지 회원자료는 기존 탈퇴 후 삭제 정책에 따라 정리하며 아이디는 보존한다. 자진 탈퇴와 자동 정리에 공통 함수를 사용하고 인증 완료·재발송과의 동시 변경을 조건부 갱신으로 보호한다. 관리자 상태 표시와 설정 안내, 운영 문서를 함께 반영한다. 검증: 변경 PHP 6개 문법 검사, 임시 DB에서 만료 경계·재발송·동시 변경·자진 탈퇴·유예 후 정리 검사 통과. --- adm/config_form.php | 2 +- adm/member_list.php | 2 + bbs/db_table.optimize.php | 19 ++++++- bbs/email_certify.php | 3 +- bbs/member_leave.php | 15 +---- docs/email-certification-expiration.md | 24 ++++++++ lib/common.lib.php | 79 ++++++++++++++++++++++++-- 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 docs/email-certification-expiration.md diff --git a/adm/config_form.php b/adm/config_form.php index 7c936106e..8544abd2c 100644 --- a/adm/config_form.php +++ b/adm/config_form.php @@ -761,7 +761,7 @@ if ($config['cf_sms_use'] && $config['cf_icode_id'] && $config['cf_icode_pw']) { - + 만료된 미인증 회원은 최고관리자 접속 시 하루 한 번 실행되는 정리에서 탈퇴 처리하며, 본인확인 정보와 소셜 연결을 해제합니다. 나머지 회원자료는 회원탈퇴후 삭제일 설정에 따라 정리합니다.') ?> 분 diff --git a/adm/member_list.php b/adm/member_list.php index e3bdc5003..4c8434c3e 100644 --- a/adm/member_list.php +++ b/adm/member_list.php @@ -275,6 +275,8 @@ $colspan = 16; 메일 미인증'; } else { echo "정상"; } diff --git a/bbs/db_table.optimize.php b/bbs/db_table.optimize.php index 6ec1875dd..06e5713d3 100644 --- a/bbs/db_table.optimize.php +++ b/bbs/db_table.optimize.php @@ -49,6 +49,23 @@ if($config['cf_memo_del'] > 0) { } } +// 메일 인증 기한이 지난 회원도 본인확인 정보를 해제하고 탈퇴 처리한다. +// 재발송된 토큰의 발급 시각을 사용하며 실제 변경 직전에 회원 상태를 다시 확인한다. +$email_certify_minutes = isset($config['cf_email_certify_minutes']) ? (int) $config['cf_email_certify_minutes'] : 60; +if ($config['cf_use_email_certify'] && $email_certify_minutes > 0) { + $sql = " select mb_id, mb_email_certify, mb_email_certify2, mb_datetime, mb_leave_date, mb_memo + from {$g5['member_table']} + where mb_leave_date = '' + and mb_email_certify not regexp '[1-9]' + and mb_memo not regexp '^[0-9]{8}.*삭제함' "; + $result = sql_query($sql); + while ($row = sql_fetch_array($result)) { + if (is_expired_email_certify_member($row)) { + g5_leave_member($row['mb_id'], true); + } + } +} + // 탈퇴회원 자동 삭제 if($config['cf_leave_day'] > 0) { $sql = " select mb_id from {$g5['member_table']} @@ -79,4 +96,4 @@ if(isset($config['cf_optimize_date'])) { run_event('cf_optimize_date_update', $config); } -run_event('db_table_optimize_end', $config); \ No newline at end of file +run_event('db_table_optimize_end', $config); diff --git a/bbs/email_certify.php b/bbs/email_certify.php index b315ec7d4..99f262923 100644 --- a/bbs/email_certify.php +++ b/bbs/email_certify.php @@ -36,7 +36,8 @@ if ($mb_md5) else { if ($mb_md5 === $row['mb_email_certify2']) { - sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' "); + $esc_mb_md5 = sql_real_escape_string($mb_md5); + sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' and mb_email_certify2 = '{$esc_mb_md5}' "); alert('메일인증 유효시간이 만료되었습니다. 인증메일을 다시 요청해 주십시오.', G5_URL); } diff --git a/bbs/member_leave.php b/bbs/member_leave.php index e24f98939..bd4545c54 100644 --- a/bbs/member_leave.php +++ b/bbs/member_leave.php @@ -12,12 +12,8 @@ $post_mb_password = isset($_POST['mb_password']) ? trim($_POST['mb_password']) : if (!($post_mb_password && check_password($post_mb_password, $member['mb_password']))) alert('비밀번호가 틀립니다.'); -// 회원탈퇴일을 저장 -$date = date("Ymd"); -$sql = " update {$g5['member_table']} set mb_leave_date = '{$date}', mb_memo = '".date('Ymd', G5_SERVER_TIME)." 탈퇴함\n".sql_real_escape_string($member['mb_memo'])."', mb_certify = '', mb_adult = 0, mb_dupinfo = '' where mb_id = '{$member['mb_id']}' "; -sql_query($sql); - -run_event('member_leave', $member); +if (!g5_leave_member($member['mb_id'])) + alert('회원탈퇴를 처리하지 못했습니다. 회원 상태를 확인해 주십시오.'); // 3.09 수정 (로그아웃) unset($_SESSION['ss_mb_id']); @@ -25,9 +21,4 @@ unset($_SESSION['ss_mb_id']); if (!$url) $url = G5_URL; -//소셜로그인 해제 -if(function_exists('social_member_link_delete')){ - social_member_link_delete($member['mb_id']); -} - -alert(''.$member['mb_nick'].'님께서는 '. date("Y년 m월 d일") .'에 회원에서 탈퇴 하셨습니다.', $url); \ No newline at end of file +alert(''.$member['mb_nick'].'님께서는 '. date("Y년 m월 d일") .'에 회원에서 탈퇴 하셨습니다.', $url); diff --git a/docs/email-certification-expiration.md b/docs/email-certification-expiration.md new file mode 100644 index 000000000..73c0f056a --- /dev/null +++ b/docs/email-certification-expiration.md @@ -0,0 +1,24 @@ +# 메일 미인증 회원의 만료 및 탈퇴 처리 + +메일인증을 사용하는 사이트에서 인증 링크의 유효시간이 지난 미인증 회원은 기존 회원탈퇴 절차로 정리한다. 기본 유효시간은 60분이며 기본환경설정에서 분 단위로 변경할 수 있다. `0`은 인증 링크 만료와 미인증 만료 정리를 모두 사용하지 않는 설정이다. + +## 실행 시점과 대상 + +- 기존 데이터 정리와 동일하게 최고관리자 접속 시 하루 한 번 실행한다. 만료 시각에 즉시 실행되는 예약 작업은 아니며 최고관리자가 접속하지 않으면 정리가 지연된다. +- 인증을 완료했거나 이미 탈퇴·삭제된 회원과 최고관리자는 제외한다. +- 재발송한 토큰은 새 발급 시각부터 유효시간을 계산한다. 기존 형식의 토큰이나 이미 폐기된 토큰은 회원가입 시각을 기준으로 한다. 유효한 기준 시각을 확인할 수 없는 회원은 자동 처리하지 않는다. +- 유효시간의 마지막 초까지는 인증할 수 있다. 실제 탈퇴 직전에 회원 상태를 다시 읽고, 그 이후 인증 완료·토큰 재발송·이메일 변경이 발생하면 해당 탈퇴 처리를 중단한다. +- 메일인증을 사용하지 않는 사이트는 미인증 만료 정리를 실행하지 않는다. 소셜 가입 시 이미 인증 처리된 회원도 대상이 아니다. + +## 두 단계의 처리 + +1. **만료 회원 탈퇴 처리**: 탈퇴일과 만료 사유를 기록하고 `mb_certify`, `mb_adult`, `mb_dupinfo`를 초기화한다. 이메일 인증·비밀번호 재설정 토큰을 폐기하고 소셜 연결을 해제한다. 일반 탈퇴와 같은 `member_leave` 이벤트를 실행한다. 이후 로그인 및 기존 인증 링크 사용은 불가능하다. +2. **탈퇴 후 회원자료 정리**: 기존 `회원탈퇴후 삭제일` 설정과 `member_delete()`를 사용한다. 연락처·주소·비밀번호·포인트 및 기존 삭제 함수가 처리하는 연관 데이터를 정리한다. 이 설정이 `0`이면 두 번째 자동 정리는 하지 않지만 첫 단계의 본인확인 해제는 실행한다. + +기존 정책대로 회원 아이디는 보존하여 재사용을 막는다. 첫 단계에서 회원 레코드를 삭제하거나 연락처·포인트를 즉시 제거하지 않는다. 개인정보 전체의 즉시 삭제 기능은 아니다. + +## 기존 사이트 적용 + +적용 시 이미 만료된 미인증 회원도 다음 정리 대상이 된다. 기본환경설정의 메일인증 유효시간과 회원탈퇴후 삭제일을 확인하고, 운영 중인 확장의 `member_leave` 이벤트가 자동 정리에도 적합한지 점검한다. 자동 정리는 만료 회원 본인의 요청이 아니라 최고관리자 요청에서 실행되므로 이벤트 인수의 회원정보를 기준으로 처리해야 한다. + +관리자 회원 목록은 정리 전에는 `메일 미인증`, 탈퇴 처리 후에는 `탈퇴함`으로 표시한다. 만료된 회원을 다시 정상 계정으로 복구하거나 동일 아이디로 재가입시키는 기능은 포함하지 않는다. diff --git a/lib/common.lib.php b/lib/common.lib.php index 8ba89d660..17fec1a85 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -2706,6 +2706,16 @@ function get_email_certify_token() return get_random_token_string(16) . '.' . G5_SERVER_TIME; } +// 기존 형식 또는 폐기된 토큰은 회원가입 시각을 기준으로 한다. +function get_email_certify_issued_at($stored_token, $mb_datetime) +{ + if (preg_match('/\.([0-9]{10})$/', $stored_token, $matches)) { + return (int) $matches[1]; + } + + return strtotime($mb_datetime); +} + /** * 메일 인증 토큰의 일치 여부와 유효시간을 확인한다. * @@ -2729,11 +2739,7 @@ function is_valid_email_certify_token($token, $stored_token, $mb_datetime, $vali return true; } - if (preg_match('/\.([0-9]{10})$/', $stored_token, $matches)) { - $issued_at = (int) $matches[1]; - } else { - $issued_at = strtotime($mb_datetime); - } + $issued_at = get_email_certify_issued_at($stored_token, $mb_datetime); if (!$issued_at) { return false; @@ -2744,6 +2750,25 @@ function is_valid_email_certify_token($token, $stored_token, $mb_datetime, $vali return $issued_at + ($valid_minutes * 60) >= $now; } +// 인증 완료·탈퇴·삭제 회원과 만료시간을 사용하지 않는 사이트는 제외한다. +function is_expired_email_certify_member($mb, $now = null) +{ + global $config; + + $valid_minutes = isset($config['cf_email_certify_minutes']) ? (int) $config['cf_email_certify_minutes'] : 60; + if (!$config['cf_use_email_certify'] || $valid_minutes < 1 || empty($mb['mb_id']) + || $mb['mb_id'] === $config['cf_admin'] || !empty($mb['mb_leave_date']) + || preg_match('/[1-9]/', $mb['mb_email_certify']) + || preg_match('#^[0-9]{8}.*삭제함#', $mb['mb_memo'])) { + return false; + } + + $issued_at = get_email_certify_issued_at($mb['mb_email_certify2'], $mb['mb_datetime']); + $now = $now === null ? G5_SERVER_TIME : (int) $now; + + return $issued_at > 0 && $issued_at + ($valid_minutes * 60) < $now; +} + /** * CSRF 방지용 Origin/Referer 검증 (OWASP 권장 패턴). * @@ -4049,6 +4074,50 @@ function conv_unescape_nl($str) return str_replace($search, $replace, $str); } +// 자진 탈퇴와 메일 미인증 만료 정리에서 동일한 탈퇴 처리를 사용한다. +function g5_leave_member($mb_id, $expired_email_only = false) +{ + global $config, $g5; + + $mb = get_member($mb_id); + if (empty($mb['mb_id']) || $mb['mb_id'] === $config['cf_admin'] || !empty($mb['mb_leave_date']) + || preg_match('#^[0-9]{8}.*삭제함#', $mb['mb_memo'])) { + return false; + } + + $condition = ''; + if ($expired_email_only) { + if (!is_expired_email_certify_member($mb)) { + return false; + } + + // 조회 이후 인증 완료·재발송·회원정보 변경이 있었다면 탈퇴시키지 않는다. + foreach (array('mb_email_certify', 'mb_email_certify2', 'mb_datetime', 'mb_email') as $field) { + $condition .= " and {$field} = '".sql_real_escape_string($mb[$field])."'"; + } + } + + $date = date('Ymd', G5_SERVER_TIME); + $memo = $date . ($expired_email_only ? ' 메일 미인증 만료로 탈퇴함' : ' 탈퇴함') . "\n"; + $esc_mb_id = sql_real_escape_string($mb['mb_id']); + $sql = " update {$g5['member_table']} + set mb_leave_date = '{$date}', + mb_memo = CONCAT('".sql_real_escape_string($memo)."', IFNULL(mb_memo, '')), + mb_certify = '', mb_adult = 0, mb_dupinfo = '', + mb_email_certify2 = '', mb_lost_certify = '' + where mb_id = '{$esc_mb_id}' and mb_leave_date = '' {$condition} "; + if (!sql_query($sql) || get_sql_affected_rows() < 1) { + return false; + } + + run_event('member_leave', $mb); + if (function_exists('social_member_link_delete')) { + social_member_link_delete($mb['mb_id']); + } + + return true; +} + // 회원 삭제 function member_delete($mb_id) {