최초 커밋

This commit is contained in:
root
2026-07-23 16:15:57 +09:00
commit c8f5efb6b7
14652 changed files with 4812531 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json; charset=UTF-8');
set_time_limit(600);
// file_put_contents(__DIR__.'/debug_tenant.log', print_r($input, true), FILE_APPEND);
// === 환경변수 (실서비스 시 교체)
$PROJECT_ID = 'P2wON5fy1K6kyia269VpeIzYP8oP';
$MGMT_KEY = 'K32l5ORmzy32OvaaPvpdZsMY3JmKQb7a3vvrl10PgjlJUGk3K7EssMH3uW5VGQSbrgtEdPj';
// ✅ Tenant ID 매핑 테이블
$TENANT_IDS = [
'customer' => 'T2x4TDzxasp7auPCPcN8uOrxXchh',
'family' => 'T2wQcWCBhUfJgUWHWgNwLg4iUDVY',
'edu' => 'T31ZmUcwOZbwk0y3YmMxrPCpzpQR',
'egBIM' => 'T2yGFrGSnFX601G22JOMojJX7OMd',
];
// === 입력값(JSON)
$input = json_decode(file_get_contents('php://input'), true);
$csvUrl = trim($input['csv_url'] ?? '');
$expiryIn = trim($input['expiry'] ?? '');
$sendReset = !empty($input['send_reset']);
$tenantType = trim($input['tenant_type'] ?? '');
// echo json_encode(['tenantType' => $tenantType]);
// exit;
if (!$csvUrl) {
echo json_encode(['status'=>'fail','message'=>'CSV URL 누락']);
exit;
}
// ✅ cURL 방식으로 CSV 가져오기 (Cafe24 호환)
function fetch_csv($url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP CSV Importer)',
]);
$data = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($httpcode !== 200 || !$data) {
throw new Exception("CSV 다운로드 실패 (HTTP {$httpcode}) {$err}");
}
return $data;
}
try {
$csv = fetch_csv($csvUrl);
// ✅ UTF-8 BOM 제거
if (substr($csv, 0, 3) === "\xEF\xBB\xBF") {
$csv = substr($csv, 3);
}
} catch (Exception $e) {
echo json_encode(['status'=>'fail','message'=>'CSV 파일을 불러올 수 없습니다: '.$e->getMessage()]);
exit;
}
$rows = array_map('str_getcsv', preg_split("/\r\n|\n|\r/", trim($csv)));
if (count($rows) < 2) {
echo json_encode(['status'=>'fail','message'=>'CSV 데이터 없음']);
exit;
}
// ✅ 헤더 정규화 (BOM, 공백, 대소문자 통일)
$header = array_map(function($h) {
$h = trim($h);
$h = preg_replace('/^\xEF\xBB\xBF/', '', $h); // BOM 제거
return strtolower($h);
}, $rows[0]);
file_put_contents(__DIR__.'/debug_header.log', print_r($header, true));
$dataRows = array_slice($rows, 1);
// === 유틸 함수 ===
function is_true($v){ $s = strtolower(trim((string)$v)); return in_array($s, ['true','1','y','yes'], true); }
function parse_list($v){
$t = trim((string)$v);
if ($t === '') return [];
if ($t[0] === '[') { $a = json_decode($t, true); return is_array($a)? $a: []; }
return array_values(array_filter(array_map('trim', explode(',', $t)), fn($x)=>$x!==''));
}
function call_api($url, $opts){
$ch = curl_init($url);
curl_setopt_array($ch, $opts + [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>45]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
return [$code, $body, $err];
}
function mgmt_api($path, $payload, $PROJECT_ID, $MGMT_KEY){
return call_api("https://api.descope.com/v1/mgmt/$path", [
CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>[
"Authorization: Bearer {$PROJECT_ID}:{$MGMT_KEY}",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS=> json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
}
function update_password($PROJECT_ID, $MGMT_KEY, $email, $password){
$payload = ["loginId"=>$email, "newPassword"=>$password];
return call_api("https://api.descope.com/v1/mgmt/password/update", [
CURLOPT_POST=>true,
CURLOPT_HTTPHEADER=>[
"Authorization: Bearer {$PROJECT_ID}:{$MGMT_KEY}",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS=> json_encode($payload),
]);
}
// === 날짜 변환
$expiryISO = null;
if ($expiryIn !== '') {
$dt = date_create($expiryIn);
if ($dt) $expiryISO = $dt->format(DateTime::ATOM);
}
// === 메인 루프
$created=0; $updated=0; $failed=0; $errors=[];
foreach ($dataRows as $i => $row) {
if (count($row) !== count($header)) {
file_put_contents(__DIR__.'/debug_mismatch.log',
"Row ".($i+2)." 열 개수 불일치: ".count($row)." / ".count($header)." -> ".implode(',', $row)."\n",
FILE_APPEND
);
}
if (count($row) === 1 && trim($row[0])==='') continue;
$rec = array_combine($header, array_pad($row, count($header), ''));
$loginId = trim($rec['login_id'] ?? '');
$email = trim($rec['email'] ?? '');
if (!$loginId || !$email) {
$failed++; $errors[]=['row'=>$i+2,'error'=>'login_id/email 누락']; continue;
}
$status = trim($rec['status'] ?? 'activated');
$name = trim($rec['display_name'] ?? '');
// --- 전화번호 정제 로직 추가 시작 ---
$inputPhone = trim($rec['phone'] ?? '');
if ($inputPhone !== '') {
// 1. 숫자만 남기기 (하이픈 등 제거)
$purePhone = preg_replace('/[^0-9]/', '', $inputPhone);
// 2. 한국 번호(010...)인 경우 E.164 형식(+8210...)으로 변환
if (strpos($purePhone, '010') === 0) {
$phone = '+82' . substr($purePhone, 1);
} elseif (strpos($purePhone, '82') === 0) {
$phone = '+' . $purePhone;
} else {
$phone = $purePhone; // 기타 형식은 숫자만 전달
}
} else {
$phone = '';
}
$verifiedEmail = true; // ✅ 모든 계정 이메일 인증 완료로 처리
$verifiedPhone = is_true($rec['Phone Verified'] ?? '');
$tenants = array_map(fn($t)=>['tenantId'=>$t], parse_list($rec['tenants'] ?? ''));
$roles = parse_list($rec['role'] ?? '');
$company = trim($rec['company'] ?? '');
// $completeForm = is_true($rec['completeForm'] ?? '');
$completeForm = true; //추가정보입력 무조건 완료
$expiryDate = $rec['egBimLExpiryDate'] ?? '';
$isTestUser = trim($rec['Is Test User'] ?? '');
$newPassword = trim($rec['new_password'] ?? '');
// ✅ 테넌트 자동 지정 (팝업 선택 우선)
if (isset($TENANT_IDS[$tenantType])) {
$tenants = [['tenantId' => $TENANT_IDS[$tenantType]]];
// ✅ 복수 등록 규칙
if ($tenantType === 'customer') {
$tenants[] = ['tenantId' => $TENANT_IDS['egBIM']];
}
} else {
// fallback (CSV 내 tenants 열 직접 입력 시)
$tenants = array_map(fn($t)=>['tenantId'=>$t], parse_list($rec['tenants'] ?? ''));
}
// customAttributes 구성
$custom = [
'company' => $company ?: null,
'completeForm' => $completeForm,
'isTestUser' => $isTestUser,
];
// 시트에서 만료일이 따로 있을 경우 우선
if ($expiryDate) {
$dt = date_create($expiryDate);
if ($dt) $custom['egBimLExpiryDate'] = $dt->format(DateTime::ATOM);
} elseif ($expiryISO) {
$custom['egBimLExpiryDate'] = $expiryISO;
}
// 생성/수정 페이로드
$payload = array_filter([
'loginId' => $loginId,
'email' => $email,
'name' => $name,
'phone' => $phone,
'status' => 'enabled', // ✅ 명시적으로 활성상태
'invite' => false, // ✅ 초대 모드 비활성화
'verifiedEmail' => $verifiedEmail,
'verifiedPhone' => $verifiedPhone,
'password' => $newPassword ?: null, // ✅ 초기 비밀번호 직접 설정
'roleNames' => $roles ?: null,
'userTenants' => $tenants ?: null,
'customAttributes' => $custom,
], fn($v)=>$v!==null);
// 1) 생성
[$code, $body, $err] = mgmt_api('user/create', $payload, $PROJECT_ID, $MGMT_KEY);
if ($code === 200 || $code === 201) {
$created++;
usleep(200000); // 0.2초 대기 후 상태 갱신
// ✅ 생성 직후 강제 활성화 (대시보드 표시 즉시 반영)
mgmt_api('user/update/status', [
'loginId' => $loginId,
'status' => 'enabled'
], $PROJECT_ID, $MGMT_KEY);
} elseif ($code === 409) {
// 2) 이미 존재 시 업데이트
[$uCode, $uBody, $uErr] = mgmt_api('user/update', $payload, $PROJECT_ID, $MGMT_KEY);
if ($uCode === 200) $updated++;
else {
$failed++; $errors[]=['row'=>$i+2,'loginId'=>$loginId,'error'=>"update:$uCode $uBody"];
continue;
}
} else {
$failed++; $errors[]=['row'=>$i+2,'loginId'=>$loginId,'error'=>"create:$code $body"];
continue;
}
// 3) 비밀번호 설정(new_password)
if ($newPassword !== '') {
[$pwCode, $pwBody, $pwErr] = update_password($PROJECT_ID, $MGMT_KEY, $email, $newPassword);
if ($pwCode !== 200) {
$errors[]=['row'=>$i+2,'loginId'=>$loginId,'error'=>"비밀번호 변경 실패 ($pwCode)"];
}
}
// // 4) 재설정 이메일 발송 (옵션)
// if ($sendReset) {
// call_api("https://api.descope.com/v1/auth/password/reset/start", [
// CURLOPT_POST=>true,
// CURLOPT_HTTPHEADER=>[
// "x-descope-project-id: {$PROJECT_ID}",
// "Content-Type: application/json",
// ],
// CURLOPT_POSTFIELDS=> json_encode(["loginId"=>$loginId]),
// ]);
// }
usleep(120000); // rate-limit
}
// === 결과 출력
echo json_encode([
'status'=>'ok',
'created'=>$created,
'updated'=>$updated,
'failed'=>$failed,
'errors'=>$errors,
], JSON_UNESCAPED_UNICODE);