'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);