회원가입 필터 sms발송 인증 가입저장
This commit is contained in:
parent
28ec93ac1f
commit
5f950a4420
@ -5,7 +5,7 @@ namespace App\Http\Controllers\Web\Auth;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SmsService;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
use App\Models\MemInfo;
|
||||
use App\Models\Member\MemInfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@ -95,24 +95,24 @@ class FindIdController extends Controller
|
||||
|
||||
// SMS 발송
|
||||
try {
|
||||
// $smsPayload = [
|
||||
// 'from_number' => config('services.sms.from', '1833-4856'), // 기본 발신번호
|
||||
// 'to_number' => $phone,
|
||||
// 'message' => "[PIN FOR YOU] 인증번호 {$code} 를 입력해 주세요. (3분 이내)",
|
||||
// 'sms_type' => 'sms', // 짧으니 sms 고정(원하면 자동 판단으로 빼도 됨)
|
||||
// // 'country' => '82', // 필요 시 강제
|
||||
// ];
|
||||
//
|
||||
// $ok = app(SmsService::class)->send($smsPayload, 'lguplus');
|
||||
//
|
||||
// if (!$ok) {
|
||||
// // 실패 시 세션 정리(인증 진행 꼬임 방지)
|
||||
// $request->session()->forget('find_id');
|
||||
// return response()->json([
|
||||
// 'ok' => false,
|
||||
// 'message' => '문자 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
// ], 500);
|
||||
// }
|
||||
$smsPayload = [
|
||||
'from_number' => config('services.sms.from', '1833-4856'), // 기본 발신번호
|
||||
'to_number' => $phone,
|
||||
'message' => "[PIN FOR YOU] 인증번호 {$code} 를 입력해 주세요. (3분 이내)",
|
||||
'sms_type' => 'sms', // 짧으니 sms 고정(원하면 자동 판단으로 빼도 됨)
|
||||
// 'country' => '82', // 필요 시 강제
|
||||
];
|
||||
|
||||
$ok = app(SmsService::class)->send($smsPayload, 'lguplus');
|
||||
|
||||
if (!$ok) {
|
||||
// 실패 시 세션 정리(인증 진행 꼬임 방지)
|
||||
$request->session()->forget('find_id');
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => '문자 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
], 500);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$request->session()->forget('find_id');
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ namespace App\Http\Controllers\Web\Auth;
|
||||
|
||||
use App\Services\MailService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MemInfo;
|
||||
use App\Models\Member\MemInfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
111
app/Http/Controllers/Web/Auth/LoginController.php
Normal file
111
app/Http/Controllers/Web/Auth/LoginController.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\MemInfoService;
|
||||
use App\Rules\RecaptchaV3Rule;
|
||||
use App\Support\AuthSession;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
final class LoginController extends Controller
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
return view('web.auth.login');
|
||||
}
|
||||
|
||||
public function prc(Request $request, MemInfoService $memInfoService)
|
||||
{
|
||||
$rules = [
|
||||
'mem_email' => ['required', 'string', 'email', 'max:60'],
|
||||
'mem_pw' => ['required', 'string', 'max:100'],
|
||||
'return_url'=> ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
|
||||
if (app()->environment('production')) {
|
||||
$rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('login')];
|
||||
}
|
||||
|
||||
$v = Validator::make($request->all(), $rules, [
|
||||
'mem_email.required' => '아이디 혹은 비밀번호가 일치하지 않습니다.',
|
||||
'mem_email.email' => '아이디 혹은 비밀번호가 일치하지 않습니다.',
|
||||
'mem_pw.required' => '아이디 혹은 비밀번호가 일치하지 않습니다.',
|
||||
'g-recaptcha-response.required' => '올바른 접근이 아닙니다.',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return back()->withErrors($v)->withInput();
|
||||
}
|
||||
|
||||
$email = strtolower(trim((string)$request->input('mem_email')));
|
||||
$pw = (string)$request->input('mem_pw');
|
||||
|
||||
// return_url: 오픈리다이렉트 방지 (내부 path만 허용)
|
||||
$returnUrl = (string)($request->input('return_url') ?? '/');
|
||||
if ($returnUrl === '' || str_starts_with($returnUrl, 'http://') || str_starts_with($returnUrl, 'https://') || str_starts_with($returnUrl, '//')) {
|
||||
$returnUrl = '/';
|
||||
}
|
||||
if (!str_starts_with($returnUrl, '/')) {
|
||||
$returnUrl = '/';
|
||||
}
|
||||
|
||||
$res = $memInfoService->attemptLegacyLogin([
|
||||
'email' => $email,
|
||||
'password' => $pw,
|
||||
'ip' => $request->ip(),
|
||||
'ua' => substr((string)$request->userAgent(), 0, 500),
|
||||
'return_url' => $returnUrl,
|
||||
]);
|
||||
|
||||
if (!$res['ok']) {
|
||||
// UI 처리 방식은 프로젝트 스타일에 맞춰
|
||||
// (일단 errors로 처리)
|
||||
return back()
|
||||
->withErrors(['login' => $res['message']])
|
||||
->withInput(['mem_email' => $email]);
|
||||
}
|
||||
|
||||
// 세션 저장
|
||||
AuthSession::putMember($res['session']);
|
||||
|
||||
|
||||
return redirect()->to($res['redirect'] ?? $returnUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* (옵션) 휴면 해제 링크 처리 - 최소 골격
|
||||
* 실제 로직은 다음 단계에서 dormancy 테이블 검증/만료/상태변경까지 붙이면 됨
|
||||
*/
|
||||
public function dormancyPrc(Request $request)
|
||||
{
|
||||
// TODO: Crypt::decryptString(authnum) -> "auth_key|seq"
|
||||
// TODO: mem_dormancy 검증/만료/인증완료 처리
|
||||
return redirect()->route('web.auth.login')
|
||||
->withErrors(['login' => '휴면 해제 처리는 다음 단계에서 연결합니다.']);
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
$request->session()->forget('_sess');
|
||||
|
||||
// (선택) 회원가입/본인인증 진행 세션까지 같이 정리하고 싶으면 추가
|
||||
$request->session()->forget('signup');
|
||||
$request->session()->forget('register');
|
||||
|
||||
// (선택) 디버그 세션 정리
|
||||
$request->session()->forget('debug');
|
||||
|
||||
// ✅ 세션 저장
|
||||
$request->session()->save();
|
||||
|
||||
return redirect()->route('web.home')
|
||||
->with('ui_dialog', [
|
||||
'type' => 'alert',
|
||||
'title' => '안내',
|
||||
'message' => '로그아웃 되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,11 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
public function showStep0()
|
||||
public function showStep0(Request $request)
|
||||
{
|
||||
//기존 인증 받다가 중지된 정보가 있다면 세션값 지우기
|
||||
$this->clearSession($request, 'signup');
|
||||
$this->clearSession($request, 'register');
|
||||
return view('web.auth.register');
|
||||
}
|
||||
|
||||
@ -32,9 +35,12 @@ class RegisterController extends Controller
|
||||
{
|
||||
$v = Validator::make($request->all(), [
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'carrier' => ['required', 'in:01,02,03,04,05,06'], //01:SKT,02:KT,03:LGU+,04:SKT(알뜰폰),05:KT(알뜰폰),06:LGU+(알뜰폰)'
|
||||
'g-recaptcha-response' => ['required', new RecaptchaV3Rule('register_phone_check')],
|
||||
], [
|
||||
'phone.required' => '휴대폰 번호를 입력해 주세요.',
|
||||
'carrier.required' => '통신사를 선택해 주세요.',
|
||||
'carrier.in' => '통신사 선택 값이 올바르지 않습니다.',
|
||||
'g-recaptcha-response.required' => '보안 검증에 실패했습니다. 다시 시도해 주세요.',
|
||||
]);
|
||||
|
||||
@ -42,8 +48,43 @@ class RegisterController extends Controller
|
||||
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
|
||||
}
|
||||
|
||||
$ip4 = $request->ip() ?: '';
|
||||
$result = $repo->step0PhoneCheck((string)$request->input('phone'), $ip4);
|
||||
/* 회원가입 필터 아이피로 차단 및 관리자 알림*/
|
||||
$ip = $request->ip();
|
||||
$ip4c = implode('.', array_slice(explode('.', $ip), 0, 3)); //C-CLASS CHECK (210.96.177)
|
||||
if (!$request->session()->has('signup.ipf_result')) {
|
||||
$ipCheck = $repo->precheckJoinIpFilterAndLog([
|
||||
'mem_no' => 0,
|
||||
'cell_corp' => $request->input('carrier'),
|
||||
'cell_phone' => $request->input('phone'),
|
||||
'email' => '-',
|
||||
'ip4' => $ip,
|
||||
'ip4_c' => $ip4c,
|
||||
]);
|
||||
|
||||
$request->session()->put('signup.ipf_result', $ipCheck['result']); // A/S/P
|
||||
$request->session()->put('signup.ipf_seq', (int)($ipCheck['seq'] ?? 0));
|
||||
|
||||
if ($ipCheck['result'] === 'A') {
|
||||
// 가입 차단
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => '회원가입에 문제가 발생하였습니다. 고객센터에 문의하세요!',
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
/* 회원가입 필터 아이피로 차단 및 관리자 알림*/
|
||||
|
||||
$map = [ //다날로 보내는 값
|
||||
'01' => 'SKT', '02' => 'KTF', '03' => 'LGT',
|
||||
'04' => 'MVNO','05' => 'MVNO','06' => 'MVNO',
|
||||
];
|
||||
|
||||
$result = $repo->step0PhoneCheck(
|
||||
(string) $request->input('phone'),
|
||||
(string) $request->input('carrier'),
|
||||
);
|
||||
|
||||
$danalCarrier = $map[$result['carrier']] ?? null;
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$reason = $result['reason'] ?? 'error';
|
||||
@ -59,7 +100,8 @@ class RegisterController extends Controller
|
||||
}
|
||||
|
||||
$request->session()->put('signup.step', 1);
|
||||
$request->session()->put('signup.phone', $result['phone']); // 필요하면
|
||||
$request->session()->put('signup.phone', $result['phone']);
|
||||
$request->session()->put('signup.carrier', $danalCarrier);
|
||||
$request->session()->save();
|
||||
|
||||
return response()->json([
|
||||
@ -68,6 +110,7 @@ class RegisterController extends Controller
|
||||
'message' => $result['message'] ?? '',
|
||||
'redirect' => $result['redirect'] ?? null,
|
||||
'phone' => $result['phone'] ?? null,
|
||||
'carrier' => $danalCarrier,
|
||||
], 200);
|
||||
}
|
||||
|
||||
@ -154,21 +197,42 @@ class RegisterController extends Controller
|
||||
public function danalStart(Request $request)
|
||||
{
|
||||
// fields는 JSON으로 받을 것
|
||||
$fieldsJson = (string)$request->input('fields', '');
|
||||
$fieldsJson = (string) $request->input('fields', '');
|
||||
$fields = json_decode($fieldsJson, true);
|
||||
|
||||
if (!is_array($fields) || empty($fields)) {
|
||||
abort(400, 'Invalid Danal fields');
|
||||
}
|
||||
|
||||
$platform = strtolower((string) $request->input('platform', ($fields['platform'] ?? '')));
|
||||
$isMobile = false;
|
||||
if ($platform === 'mobile') {
|
||||
$isMobile = true;
|
||||
} elseif ($platform === 'web') {
|
||||
$isMobile = false;
|
||||
} else {
|
||||
$ua = (string) $request->header('User-Agent', '');
|
||||
$isMobile = (bool) preg_match('/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i', $ua);
|
||||
}
|
||||
|
||||
$fields['IsDstAddr'] = $request->session()->get('signup.phone');
|
||||
$fields['IsCarrier'] = $request->session()->get('signup.carrier');
|
||||
|
||||
$action = $isMobile
|
||||
? 'https://wauth.teledit.com/Danal/WebAuth/Mobile/Start.php'
|
||||
: 'https://wauth.teledit.com/Danal/WebAuth/Web/Start.php';
|
||||
|
||||
unset($fields['platform']);
|
||||
|
||||
return view('web.auth.danal_autosubmit', [
|
||||
'action' => 'https://wauth.teledit.com/Danal/WebAuth/Web/Start.php',
|
||||
'action' => $action,
|
||||
'fields' => $fields,
|
||||
'isMobile' => $isMobile,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function danalResult(Request $request, DanalAuthtelService $danal)
|
||||
public function danalResult(Request $request, DanalAuthtelService $danal, MemberAuthRepository $repo)
|
||||
{
|
||||
$payload = $request->all();
|
||||
|
||||
@ -198,6 +262,12 @@ class RegisterController extends Controller
|
||||
Log::info('[DANAL][CONFIRM] res', $this->maskDanalPayloadForLog($res));
|
||||
}
|
||||
|
||||
//repo로 로그 1차 저장” (성공/실패 무조건 저장)
|
||||
$logSeq = $repo->insertDanalAuthLog('J', (array)$res);
|
||||
if ($logSeq > 0) {
|
||||
$request->session()->put('register.danal_log_seq', $logSeq);
|
||||
}
|
||||
|
||||
$ok = (($res['RETURNCODE'] ?? '') === '0000');
|
||||
|
||||
if (!$ok) {
|
||||
@ -243,8 +313,6 @@ class RegisterController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 성공: 다음 단계에서 쓸 데이터 세션 저장
|
||||
$request->session()->put('register.pass_verified', true);
|
||||
$request->session()->put('register.pass_payload', $res);
|
||||
@ -271,6 +339,160 @@ class RegisterController extends Controller
|
||||
return view('web.auth.profile');
|
||||
}
|
||||
|
||||
public function checkLoginId(Request $request, MemberAuthRepository $repo)
|
||||
{
|
||||
$v = Validator::make($request->all(), [
|
||||
'login_id' => ['required', 'string', 'max:60', 'email'],
|
||||
], [
|
||||
'login_id.required' => '아이디(이메일)를 입력해 주세요.',
|
||||
'login_id.email' => '이메일 형식으로 입력해 주세요.',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $v->errors()->first(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$email = (string) $request->input('login_id');
|
||||
|
||||
// ✅ repo에서 mem_info.email 중복 체크
|
||||
$exists = $repo->existsEmail($email);
|
||||
|
||||
return response()->json([
|
||||
'ok' => !$exists,
|
||||
'message' => $exists ? '이미 사용 중인 아이디입니다.' : '사용 가능한 아이디입니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function submitProfile(Request $request, MemberAuthRepository $repo)
|
||||
{
|
||||
// 0) PASS 인증 안됐으면 컷
|
||||
if (!$request->session()->get('register.pass_verified')) {
|
||||
return redirect()->route('web.auth.register.terms')
|
||||
->with('ui_dialog', [
|
||||
'type' => 'alert',
|
||||
'title' => '안내',
|
||||
'message' => '본인인증 후 진행해 주세요.',
|
||||
]);
|
||||
}
|
||||
|
||||
// 1) 입력값 검증 (아이디=이메일)
|
||||
$v = Validator::make($request->all(), [
|
||||
'login_id' => ['required', 'string', 'email', 'max:60'],
|
||||
'password' => [
|
||||
'required', 'string', 'min:8', 'max:20',
|
||||
'regex:/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z0-9\s]).+$/'
|
||||
],
|
||||
'password_confirmation' => ['required', 'same:password'],
|
||||
'pin2' => ['required', 'digits:4'],
|
||||
'pin2_confirmation' => ['required', 'same:pin2'],
|
||||
], [
|
||||
'login_id.required' => '아이디(이메일)를 입력해 주세요.',
|
||||
'login_id.email' => '아이디는 이메일 형식이어야 합니다.',
|
||||
'password.required' => '비밀번호를 입력해 주세요.',
|
||||
'password.min' => '비밀번호는 8자리 이상이어야 합니다.',
|
||||
'password.max' => '비밀번호는 20자리를 초과할 수 없습니다.',
|
||||
'password.regex' => '비밀번호는 영문+숫자+특수문자를 포함해야 합니다.',
|
||||
'password_confirmation.same' => '비밀번호 확인이 일치하지 않습니다.',
|
||||
'pin2.required' => '2차 비밀번호(숫자 4자리)를 입력해 주세요.',
|
||||
'pin2.digits' => '2차 비밀번호는 숫자 4자리여야 합니다.',
|
||||
'pin2_confirmation.same' => '2차 비밀번호 확인이 일치하지 않습니다.',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return back()->withErrors($v)->withInput();
|
||||
}
|
||||
|
||||
// 2) 아이디(=이메일) 정규화
|
||||
$email = strtolower(trim((string)$request->input('login_id')));
|
||||
|
||||
// 3) 중복 체크는 Repository로만
|
||||
if ($repo->existsEmail($email)) {
|
||||
return back()->withErrors(['login_id' => '이미 사용 중인 아이디입니다.'])->withInput();
|
||||
}
|
||||
|
||||
// 4) PASS payload + signup 세션값 가져오기
|
||||
$pv = (array) $request->session()->get('register.pass_payload', []);
|
||||
$signupPhone = (string) $request->session()->get('signup.phone', '');
|
||||
$signupCarrier = (string) $request->session()->get('signup.carrier', '');
|
||||
|
||||
if ($signupPhone !== $pv['PHONE'] || $signupCarrier !== $pv['CARRIER']) {
|
||||
return back()->withErrors(['login_id' => '처음등록한 전화번호와 인증받은 전화번호 정보가 일치 하지 않습니다..'])->withInput();
|
||||
}
|
||||
|
||||
|
||||
// 5) 가공(서버에서 최종 사용 데이터 구성)
|
||||
$final = [
|
||||
// 회원 입력
|
||||
'email' => $email,
|
||||
'password_plain' => (string)$request->input('password'), // 실제 저장은 Hash로 (아래에서 처리)
|
||||
'pin2_plain' => (string) $request->input('pin2'),
|
||||
// PASS 인증 결과
|
||||
'pass' => [
|
||||
'return_code' => $pv['RETURNCODE'] ?? null,
|
||||
'tid' => $pv['TID'] ?? null,
|
||||
'name' => $pv['NAME'] ?? null,
|
||||
'dob' => $pv['DOB'] ?? null,
|
||||
'sex' => $pv['SEX'] ?? null,
|
||||
|
||||
'foreigner' => $pv['FOREIGNER'] ?? null,
|
||||
'phone' => $pv['PHONE'] ?? $signupPhone,
|
||||
'carrier' => $pv['CARRIER'] ?? $signupCarrier,
|
||||
'ci' => $pv['CI'] ?? null,
|
||||
'di' => $pv['DI'] ?? null,
|
||||
],
|
||||
'meta' => [
|
||||
'ip' => $request->ip(),
|
||||
'ua' => substr((string)$request->userAgent(), 0, 200),
|
||||
],
|
||||
];
|
||||
|
||||
// 비밀번호는 로그에 남기면 사고남 → 마스킹/제거
|
||||
$logFinal = $final;
|
||||
$logFinal['password_plain'] = '***masked***';
|
||||
$logFinal['pin2_plain'] = '***masked***';
|
||||
|
||||
// 6) 컨트롤러에서 "넘어온 값" 확인: 로그로 보기
|
||||
Log::info('[register.profile.submit] payload', $logFinal);
|
||||
|
||||
// 7) 여기부터 실제 저장 로직 (Repository로 위임)
|
||||
try {
|
||||
$res = $repo->createMemberFromSignup($final, $request->session()->all());
|
||||
|
||||
// 성공 처리 후 세션 정리/이동
|
||||
$this->clearSession($request, 'signup');
|
||||
$this->clearSession($request, 'register');
|
||||
|
||||
return redirect()->route('web.auth.login')
|
||||
->with('ui_dialog', [
|
||||
'type'=>'alert',
|
||||
'title'=>'완료',
|
||||
'message'=>'가입이 완료되었습니다. 로그인해 주세요.',
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[register.profile.submit] commit failed', [
|
||||
'email' => $final['email'] ?? null,
|
||||
'err' => $e->getMessage(),
|
||||
'trace' => substr($e->getTraceAsString(), 0, 2000),
|
||||
]);
|
||||
|
||||
// PASS 세션은 살아있게 두고(=register/pass 유지),
|
||||
// 입력값은 withInput()으로 다시 채워서 profile로 복귀
|
||||
return redirect()->route('web.auth.register.profile')
|
||||
->withInput()
|
||||
->with('ui_dialog', [
|
||||
'type' => 'alert',
|
||||
'title' => '가입 실패',
|
||||
'message' => '저장 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function maskDanalPayloadForLog(array $payload): array
|
||||
{
|
||||
@ -294,5 +516,10 @@ class RegisterController extends Controller
|
||||
return $masked;
|
||||
}
|
||||
|
||||
private function clearSession(Request $request, string $key = 'signup'): void
|
||||
{
|
||||
$request->session()->forget($key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
29
app/Http/Middleware/LegacyAuth.php
Normal file
29
app/Http/Middleware/LegacyAuth.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegacyAuth
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$loggedIn = (bool) $request->session()->get('_sess._login_', false);
|
||||
|
||||
if (!$loggedIn) {
|
||||
// 로그인 성공 후 원래 가려던 곳으로 보내기 위해 intended 저장
|
||||
// (Laravel auth 안 쓰더라도 이 키는 redirect()->intended()가 알아서 씀)
|
||||
$request->session()->put('url.intended', $request->fullUrl());
|
||||
|
||||
return redirect()->route('web.auth.login')
|
||||
->with('ui_dialog', [
|
||||
'type' => 'alert',
|
||||
'title' => '안내',
|
||||
'message' => '로그인 필요합니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
22
app/Http/Middleware/LegacyGuest.php
Normal file
22
app/Http/Middleware/LegacyGuest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegacyGuest
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$loggedIn = (bool) $request->session()->get('_sess._login_', false);
|
||||
|
||||
if ($loggedIn) {
|
||||
// 이미 로그인 상태면 auth 페이지 접근 막고 홈(또는 intended)으로
|
||||
$to = $request->session()->pull('url.intended', '/');
|
||||
return redirect($to ?: '/');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ class AdminUser extends Authenticatable
|
||||
use Notifiable;
|
||||
|
||||
protected $table = 'admin_users';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'email','password',
|
||||
|
||||
@ -12,8 +12,6 @@ class MemInfo extends Model
|
||||
protected $primaryKey = 'mem_no';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
|
||||
// mem_info는 created_at/updated_at 컬럼이 dt_reg/dt_mod 라서 기본 timestamps 안 씀
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
|
||||
@ -7,13 +7,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
||||
class MemAddress extends Model
|
||||
{
|
||||
|
||||
|
||||
protected $table = 'mem_address';
|
||||
{ protected $table = 'mem_address';
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ class MemAuth extends Model
|
||||
protected $primaryKey = null;
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class MemAuthInfo extends Model
|
||||
protected $primaryKey = 'mem_no';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class MemAuthLog extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -2,24 +2,44 @@
|
||||
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class MemInfo extends Model
|
||||
{
|
||||
//
|
||||
|
||||
protected $table = 'mem_info';
|
||||
protected $primaryKey = 'mem_no';
|
||||
protected $keyType = 'int';
|
||||
public $incrementing = true;
|
||||
public $timestamps = false;
|
||||
|
||||
// 테이블이 legacy라 fillable 대신 guarded 추천 (내부에서만 쓰면 []도 가능)
|
||||
protected $guarded = [];
|
||||
/**
|
||||
* ✅ 보안: guarded=[](전부 허용) 는 위험하니,
|
||||
* 기존 App\Models\MemInfo 의 allowlist(fillable) 방식 유지.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'stat_1','stat_2','stat_3','stat_4','stat_5',
|
||||
'name','name_first','name_mid','name_last',
|
||||
'birth','gender','native',
|
||||
'cell_corp','cell_phone','email','pv_sns',
|
||||
'bank_code','bank_name','bank_act_num','bank_vact_num',
|
||||
'rcv_email','rcv_sms','rcv_push',
|
||||
'login_fail_cnt',
|
||||
'dt_login','dt_reg','dt_mod',
|
||||
'dt_rcv_email','dt_rcv_sms','dt_rcv_push',
|
||||
'dt_stat_1','dt_stat_2','dt_stat_3','dt_stat_4','dt_stat_5',
|
||||
'ip_reg','ci_v','ci','di',
|
||||
'country_code','country_name',
|
||||
'admin_memo','modify_log',
|
||||
];
|
||||
|
||||
// zero-date 때문에 datetime/date cast는 걸지 않음 (필요시 별도 accessor로 안전 파싱)
|
||||
/**
|
||||
* ✅ 레거시 zero-date(0000-00-00 ...)가 있으면 datetime/date cast는 예외/오작동 가능.
|
||||
* 안전하게 JSON 컬럼만 cast (나머지는 safe accessor로 뽑아 쓰자)
|
||||
*/
|
||||
protected $casts = [
|
||||
'admin_memo' => 'array',
|
||||
'modify_log' => 'array',
|
||||
@ -36,7 +56,7 @@ class MemInfo extends Model
|
||||
|
||||
public function authRows(): HasMany
|
||||
{
|
||||
// mem_auth 복합키 테이블이지만 조회 관계는 문제 없음
|
||||
// mem_auth 복합키 테이블이어도 조회 관계는 가능
|
||||
return $this->hasMany(MemAuth::class, 'mem_no', 'mem_no');
|
||||
}
|
||||
|
||||
@ -71,17 +91,90 @@ class MemInfo extends Model
|
||||
}
|
||||
|
||||
/* =====================
|
||||
* Helpers (optional)
|
||||
* Scopes (기존 App\Models\MemInfo에서 가져옴)
|
||||
* ===================== */
|
||||
|
||||
public function scopeActive(Builder $q): Builder
|
||||
{
|
||||
// CI에서 stat_3 == 3 접근금지 / 4 탈퇴신청 / 5 탈퇴완료
|
||||
return $q->whereNotIn('stat_3', ['3','4','5']);
|
||||
}
|
||||
|
||||
public function scopeByEmail(Builder $q, string $email): Builder
|
||||
{
|
||||
return $q->where('email', strtolower($email));
|
||||
}
|
||||
|
||||
public function scopeByPhoneLookup(Builder $q, string $phoneNormalized): Builder
|
||||
{
|
||||
// TODO: cell_phone이 암호화면 단순 where 비교 불가
|
||||
// 추천: cell_phone_hash 같은 정규화+해시 컬럼 만들어 lookup
|
||||
return $q;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
* Helpers (둘 모델 통합)
|
||||
* ===================== */
|
||||
|
||||
public function isBlocked(): bool
|
||||
{
|
||||
return (string) $this->stat_3 === '3';
|
||||
}
|
||||
|
||||
public function isWithdrawnOrRequested(): bool
|
||||
{
|
||||
return in_array((string) $this->stat_3, ['4','5'], true);
|
||||
}
|
||||
|
||||
public function isWithdrawn(): bool
|
||||
{
|
||||
// legacy: dt_out 기본값이 0000-00-00... 이므로 문자열 비교로 처리
|
||||
return isset($this->attributes['dt_out']) && $this->attributes['dt_out'] !== '0000-00-00 00:00:00';
|
||||
// legacy: dt_out 기본값이 0000-00-00 00:00:00 일 수 있음
|
||||
$v = $this->attributes['dt_out'] ?? null;
|
||||
return !empty($v) && $v !== '0000-00-00 00:00:00';
|
||||
}
|
||||
|
||||
public function hasEmail(): bool
|
||||
{
|
||||
return !empty($this->attributes['email']);
|
||||
}
|
||||
|
||||
public function isFirstLogin(): bool
|
||||
{
|
||||
$dtLogin = $this->dt_login_at();
|
||||
$dtReg = $this->dt_reg_at();
|
||||
|
||||
if (!$dtLogin || !$dtReg) return false;
|
||||
return $dtLogin->equalTo($dtReg);
|
||||
}
|
||||
|
||||
/* =====================
|
||||
* Safe datetime accessors
|
||||
* ===================== */
|
||||
|
||||
private function safeCarbon(?string $value): ?Carbon
|
||||
{
|
||||
if (!$value) return null;
|
||||
if ($value === '0000-00-00 00:00:00' || $value === '0000-00-00') return null;
|
||||
|
||||
try {
|
||||
return Carbon::parse($value);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function dt_login_at(): ?Carbon
|
||||
{
|
||||
return $this->safeCarbon($this->attributes['dt_login'] ?? null);
|
||||
}
|
||||
|
||||
public function dt_reg_at(): ?Carbon
|
||||
{
|
||||
return $this->safeCarbon($this->attributes['dt_reg'] ?? null);
|
||||
}
|
||||
|
||||
public function dt_mod_at(): ?Carbon
|
||||
{
|
||||
return $this->safeCarbon($this->attributes['dt_mod'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class MemJoinFilter extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class MemJoinLog extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class MemLoginRecent extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ class MemLoginYear extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -7,12 +7,11 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MemModLog extends Model
|
||||
{
|
||||
|
||||
|
||||
protected $table = 'mem_mod_log';
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ class MemPasswdModify extends Model
|
||||
protected $primaryKey = 'seq';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
@ -1,24 +1,34 @@
|
||||
<?php
|
||||
namespace App\Repositories\Member;
|
||||
|
||||
namespace App\Models\Member;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
||||
class MemStRing extends Model
|
||||
final class MemStRingRepository
|
||||
{
|
||||
|
||||
|
||||
protected $table = 'mem_st_ring';
|
||||
protected $primaryKey = 'mem_no';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'int';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function member(): BelongsTo
|
||||
public function getLegacyPass1(int $memNo, int $set = 0): ?string
|
||||
{
|
||||
return $this->belongsTo(MemInfo::class, 'mem_no', 'mem_no');
|
||||
$col = 'str_' . (int)$set; // str_0 / str_1 / str_2
|
||||
|
||||
$row = DB::table('mem_st_ring')
|
||||
->select($col)
|
||||
->where('mem_no', $memNo)
|
||||
->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$val = $row->{$col} ?? null;
|
||||
$val = is_string($val) ? trim($val) : null;
|
||||
|
||||
return $val !== '' ? $val : null;
|
||||
}
|
||||
|
||||
public function getLegacyPass2(int $memNo): ?string
|
||||
{
|
||||
$val = DB::table('mem_st_ring')
|
||||
->where('mem_no', $memNo)
|
||||
->value('passwd2');
|
||||
|
||||
$val = is_string($val) ? trim($val) : null;
|
||||
return $val !== '' ? $val : null;
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Repositories/Member/MemStRingRepository.php
Normal file
64
app/Repositories/Member/MemStRingRepository.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Member;
|
||||
|
||||
use App\Support\LegacyCrypto\CiPassword;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class MemStRingRepository
|
||||
{
|
||||
/**
|
||||
* CI3 기본(1차+2차) 저장과 동일한 upsert
|
||||
*/
|
||||
public function upsertBoth(int $memNo, string $plainPassword, string $pin2, ?string $dtNow = null): void
|
||||
{
|
||||
$dt = $dtNow ?: Carbon::now()->toDateTimeString();
|
||||
|
||||
[$str0, $str1, $str2] = CiPassword::makeAll($plainPassword);
|
||||
$pass2 = CiPassword::makePass2($pin2);
|
||||
|
||||
DB::statement(
|
||||
"INSERT INTO mem_st_ring (mem_no, str_0, str_1, str_2, dt_reg, passwd2, passwd2_reg)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
str_0 = ?, str_1 = ?, str_2 = ?, dt_reg = ?, passwd2 = ?, passwd2_reg = ?",
|
||||
[
|
||||
$memNo, $str0, $str1, $str2, $dt, $pass2, $dt,
|
||||
$str0, $str1, $str2, $dt, $pass2, $dt,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI3 modify_type == "1_passwd" 대응 (1차만)
|
||||
*/
|
||||
public function upsertPassword1(int $memNo, string $plainPassword, ?string $dtNow = null): void
|
||||
{
|
||||
$dt = $dtNow ?: Carbon::now()->toDateTimeString();
|
||||
[$str0, $str1, $str2] = CiPassword::makeAll($plainPassword);
|
||||
|
||||
DB::statement(
|
||||
"INSERT INTO mem_st_ring (mem_no, str_0, str_1, str_2, dt_reg)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE str_0=?, str_1=?, str_2=?, dt_reg=?",
|
||||
[$memNo, $str0, $str1, $str2, $dt, $str0, $str1, $str2, $dt]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI3 modify_type == "2_passwd" 대응 (2차만)
|
||||
*/
|
||||
public function upsertPassword2(int $memNo, string $pin2, ?string $dtNow = null): void
|
||||
{
|
||||
$dt = $dtNow ?: Carbon::now()->toDateTimeString();
|
||||
$pass2 = CiPassword::makePass2($pin2);
|
||||
|
||||
DB::statement(
|
||||
"INSERT INTO mem_st_ring (mem_no, passwd2, passwd2_reg)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE passwd2=?, passwd2_reg=?",
|
||||
[$memNo, $pass2, $dt, $pass2, $dt]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -8,12 +8,22 @@ use App\Models\Member\MemAuthLog;
|
||||
use App\Models\Member\MemInfo;
|
||||
use App\Models\Member\MemJoinFilter;
|
||||
use App\Models\Member\MemJoinLog;
|
||||
use App\Support\Legacy\Carrier;
|
||||
use App\Services\SmsService;
|
||||
use App\Services\MemInfoService;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
use App\Support\LegacyCrypto\CiPassword;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
|
||||
class MemberAuthRepository
|
||||
{
|
||||
public function __construct(private readonly MemInfoService $memInfoService)
|
||||
{
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* mem_auth (기존)
|
||||
* ========================================================= */
|
||||
@ -99,15 +109,6 @@ class MemberAuthRepository
|
||||
->value('auth_state');
|
||||
}
|
||||
|
||||
public function isVerified(int $memNo, string $authType): bool
|
||||
{
|
||||
return $this->getState($memNo, $authType) === MemAuth::STATE_Y;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* Step0: phone check + join_filter + join_log
|
||||
* ========================================================= */
|
||||
|
||||
private function normalizeKoreanPhone(string $raw): ?string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $raw ?? '');
|
||||
@ -156,25 +157,10 @@ class MemberAuthRepository
|
||||
return ['hit' => true, 'block' => false, 'gubun' => $r->gubun ?? 'filter_hit', 'row' => $r];
|
||||
}
|
||||
|
||||
public function writeJoinLog(array $data): void
|
||||
{
|
||||
MemJoinLog::query()->create([
|
||||
'gubun' => $data['gubun'] ?? null,
|
||||
'mem_no' => (int)($data['mem_no'] ?? 0),
|
||||
'cell_corp' => $data['cell_corp'] ?? 'n',
|
||||
'cell_phone' => $data['cell_phone'] ?? '',
|
||||
'email' => $data['email'] ?? null,
|
||||
'ip4' => $data['ip4'] ?? '',
|
||||
'ip4_c' => $data['ip4_c'] ?? '',
|
||||
'error_code' => $data['error_code'] ?? '',
|
||||
'dt_reg' => Carbon::now()->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step0 통합 처리
|
||||
*/
|
||||
public function step0PhoneCheck(string $rawPhone, string $ip4 = ''): array
|
||||
public function step0PhoneCheck(string $rawPhone, string $carrier = '', string $ip4 = ''): array
|
||||
{
|
||||
$base = [
|
||||
'ok' => false,
|
||||
@ -182,6 +168,7 @@ class MemberAuthRepository
|
||||
'message' => '',
|
||||
'redirect' => null,
|
||||
'phone' => null,
|
||||
'carrier' => null,
|
||||
'filter' => null,
|
||||
'admin_phones' => [],
|
||||
];
|
||||
@ -190,6 +177,9 @@ class MemberAuthRepository
|
||||
$ip4c = $this->ipToCClass($ip4);
|
||||
$ipHit = $this->checkJoinFilterByIp($ip4, $ip4c);
|
||||
|
||||
$carrier = trim($carrier);
|
||||
$base['carrier'] = $carrier ?: null;
|
||||
|
||||
if ($ipHit) {
|
||||
$base['filter'] = $ipHit;
|
||||
$base['admin_phones'] = $ipHit['admin_phones'] ?? [];
|
||||
@ -215,16 +205,17 @@ class MemberAuthRepository
|
||||
$base['phone'] = $phone;
|
||||
|
||||
// 2) 이미 회원인지 체크
|
||||
$member = $this->findMemberByPhone($phone);
|
||||
if ($member && !empty($member->mem_no)) {
|
||||
return array_merge($base, [
|
||||
'ok' => false,
|
||||
'reason' => 'already_member',
|
||||
'message' => "이미 가입된 전화번호 입니다.\n\n아이디 찾기로 이동할까요?",
|
||||
'redirect' => route('web.auth.find_id'), // ✅ 이미가입이면 아이디 찾기
|
||||
'matched_mem_no' => (int)$member->mem_no, // 필요하면 유지
|
||||
]);
|
||||
}
|
||||
$member = $this->findMemberByPhone($phone, $carrier);
|
||||
// if ($member && !empty($member->mem_no)) {
|
||||
// return array_merge($base, [
|
||||
// 'ok' => false,
|
||||
// 'reason' => 'already_member',
|
||||
// 'message' => "이미 가입된 전화번호 입니다.\n\n아이디 찾기로 이동할까요?",
|
||||
// 'redirect' => route('web.auth.find_id'),
|
||||
// 'matched_mem_no' => (int) $member->mem_no,
|
||||
// 'matched_cell_corp' => $member->cell_corp ?? null, // ✅ 필요시
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// 3) 기존 phone+ip 필터
|
||||
$filter = $this->checkJoinFilter($phone, $ip4, $ip4c);
|
||||
@ -247,9 +238,7 @@ class MemberAuthRepository
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function findMemberByPhone(string $phone): ?object
|
||||
private function findMemberByPhone(string $phone, string $carrier): ?object
|
||||
{
|
||||
/** @var CiSeedCrypto $seed */
|
||||
$seed = app(CiSeedCrypto::class);
|
||||
@ -259,10 +248,21 @@ class MemberAuthRepository
|
||||
return DB::table('mem_info')
|
||||
->select('mem_no', 'cell_phone')
|
||||
->where('cell_phone', $phoneEnc)
|
||||
->where('cell_corp', $carrier)
|
||||
->limit(1)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function existsEmail(string $email): bool
|
||||
{
|
||||
$email = trim($email);
|
||||
if ($email === '') return false;
|
||||
|
||||
return DB::table('mem_info')
|
||||
->where('email', $email) // ✅ mem_info.email
|
||||
->exists();
|
||||
}
|
||||
|
||||
|
||||
public function ipToCClass(string $ip): string
|
||||
{
|
||||
@ -274,13 +274,12 @@ class MemberAuthRepository
|
||||
}
|
||||
|
||||
private function checkJoinFilterByIp(string $ip4, string $ip4c): ?array
|
||||
{$ip4 = "19dd.0";
|
||||
{
|
||||
// IPv4 아니면 필터 적용 안 함
|
||||
if (!filter_var($ip4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 우선순위: A(차단) > S(관리자알림) > N(무시)
|
||||
$row = DB::table('mem_join_filter')
|
||||
->whereIn('join_block', ['A', 'S'])
|
||||
@ -338,6 +337,271 @@ class MemberAuthRepository
|
||||
$parts = array_map('trim', explode(',', $s));
|
||||
return array_values(array_filter($parts));
|
||||
}
|
||||
|
||||
public function createMemberFromSignup(array $final, array $sessionAll = []): array
|
||||
{
|
||||
// 0) 필수값 최소 체크(컨트롤러에서 검증했지만 방어)
|
||||
$email = strtolower(trim((string)($final['email'] ?? '')));
|
||||
$pwPlain = (string)($final['password_plain'] ?? '');
|
||||
$pin2 = (string)($final['pin2_plain'] ?? '');
|
||||
|
||||
if ($email === '' || $pwPlain === '' || !preg_match('/^\d{4}$/', $pin2)) {
|
||||
throw new \RuntimeException('invalid_payload');
|
||||
}
|
||||
|
||||
$pass = (array)($final['pass'] ?? []);
|
||||
$name = (string)($pass['name'] ?? '');
|
||||
$carrier = Carrier::toCode((string)($pass['carrier'] ?? 'n')); //통신사 코드로 변경
|
||||
$phone = preg_replace('/\D+/', '', (string)($pass['phone'] ?? ''));
|
||||
$ci = $pass['ci'] ?? null;
|
||||
$di = $pass['di'] ?? null;
|
||||
|
||||
// DOB: YYYYMMDD -> YYYY-MM-DD
|
||||
$dob = preg_replace('/\D+/', '', (string)($pass['dob'] ?? ''));
|
||||
$birth = '0000-00-00';
|
||||
if (strlen($dob) >= 8) {
|
||||
$birth = substr($dob, 0, 4).'-'.substr($dob, 4, 2).'-'.substr($dob, 6, 2);
|
||||
}
|
||||
|
||||
// foreigner/native/country
|
||||
$foreigner = (string)($pass['foreigner'] ?? '0'); // 값 정의는 네 PASS 규격대로
|
||||
$native = ($foreigner === '1') ? '2' : '1'; // 외국인=2, 내국인=1
|
||||
$countryCode = ($native === '1') ? '82' : '';
|
||||
$countryName = ($native === '1') ? 'Republic of Korea(대한민국)' : '';
|
||||
|
||||
// gender mapping (프로젝트 규칙에 맞춰 조정)
|
||||
$sex = (string)($pass['sex'] ?? '');
|
||||
$gender = match (strtoupper($sex)) {
|
||||
'1', 'M', 'MALE' => '1',
|
||||
'0', 'F', 'FEMALE', '2' => '0',
|
||||
default => '0',
|
||||
};
|
||||
|
||||
$ip = (string)($final['meta']['ip'] ?? request()->ip());
|
||||
$promotion = (string)(data_get($sessionAll, 'signup.promotion', '')) === 'on';
|
||||
|
||||
|
||||
return DB::transaction(function () use (
|
||||
$email, $pwPlain, $pin2,
|
||||
$name, $birth, $carrier, $phone,
|
||||
$native, $countryCode, $countryName, $gender,
|
||||
$ci, $di, $ip, $promotion, $sessionAll
|
||||
) {
|
||||
// 1) 접근금지회원 체크(간단 버전)
|
||||
// prohibit_access() 정확한 조건이 있으면 그 조건대로 바꾸면 됨)
|
||||
if (!empty($ci)) {
|
||||
$blocked = DB::table('mem_info')
|
||||
->where('ci', $ci)
|
||||
->whereIn('stat_3', ['3','4','5'])
|
||||
->exists();
|
||||
|
||||
if ($blocked) {
|
||||
throw new \RuntimeException('prohibit_access');
|
||||
}
|
||||
}
|
||||
|
||||
// 2) mem_info 생성 (휴대폰 암호화 포함은 MemInfoService::register가 처리)
|
||||
$mem = $this->memInfoService->register([
|
||||
'email' => $email,
|
||||
'name' => $name,
|
||||
'pv_sns' => 'self',
|
||||
'promotion' => $promotion,
|
||||
'ip_reg' => $ip,
|
||||
'country_code' => $countryCode,
|
||||
'country_name' => $countryName,
|
||||
'birth' => $birth,
|
||||
'cell_corp' => $carrier,
|
||||
'cell_phone' => $phone, // 평문 → register()에서 암호화
|
||||
'native' => $native,
|
||||
'gender' => $gender,
|
||||
'ci' => $ci,
|
||||
'di' => $di,
|
||||
'ci_v' => '1',
|
||||
]);
|
||||
|
||||
$memNo = (int)$mem->mem_no;
|
||||
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||
|
||||
//다날 로그 업데이트
|
||||
$danalLogSeq = (int) data_get($sessionAll, 'register.danal_log_seq', 0);
|
||||
if ($danalLogSeq > 0) {
|
||||
$this->updateDanalAuthLogMemNo($danalLogSeq, $memNo);
|
||||
}
|
||||
|
||||
/*회원가입 아이피 필터 업데이트*/
|
||||
$ipfResult = (string) data_get($sessionAll, 'signup.ipf_result', '');
|
||||
$ipfSeq = (int) data_get($sessionAll, 'signup.ipf_seq', 0);
|
||||
|
||||
// if ($ipfResult === 'A') {
|
||||
// // 이 케이스는 원래 step0에서 막혀야 정상이지만, 방어적으로 한번 더
|
||||
// throw new \RuntimeException('회원가입이 제한된 IP입니다.');
|
||||
// }
|
||||
if ($ipfResult === 'S' && $ipfSeq > 0) {
|
||||
$this->updateJoinLogAfterSignup($ipfSeq, $memNo, $email);
|
||||
}
|
||||
/*회원가입 아이피 필터 업데이트*/
|
||||
|
||||
// 3) mem_st_ring 비번 저장 (CI3 macro->pass + sha512(pin2)와 동일)
|
||||
[$str0, $str1, $str2] = CiPassword::makeAll($pwPlain);
|
||||
$passwd2 = CiPassword::makePass2($pin2);
|
||||
|
||||
DB::statement(
|
||||
"INSERT INTO mem_st_ring (mem_no, str_0, str_1, str_2, dt_reg, passwd2, passwd2_reg)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE str_0=VALUES(str_0), str_1=VALUES(str_1), str_2=VALUES(str_2),
|
||||
dt_reg=VALUES(dt_reg), passwd2=VALUES(passwd2), passwd2_reg=VALUES(passwd2_reg)",
|
||||
[$memNo, $str0, $str1, $str2, $now, $passwd2, $now]
|
||||
);
|
||||
|
||||
// 4) mem_auth 휴대폰 인증 처리
|
||||
DB::table('mem_auth')->updateOrInsert(
|
||||
['mem_no' => $memNo, 'auth_type' => 'cell'],
|
||||
['auth_state' => 'Y', 'auth_date' => date('Y-m-d')]
|
||||
);
|
||||
|
||||
// 5) mem_auth_log 가입 로그 저장 (CI3처럼 세션 스냅샷)
|
||||
DB::table('mem_auth_log')->insert([
|
||||
'mem_no' => $memNo,
|
||||
'type' => 'signup',
|
||||
'state' => 'S',
|
||||
'info' => json_encode($sessionAll, JSON_UNESCAPED_UNICODE),
|
||||
'rgdate' => $now,
|
||||
]);
|
||||
|
||||
// 6) (TODO) email_auth init + 메일 발송은 다음 단계에서 연결
|
||||
// 지금은 우선 회원 저장까지 완성
|
||||
|
||||
return ['ok' => true, 'mem_no' => $memNo];
|
||||
});
|
||||
}
|
||||
|
||||
public function insertDanalAuthLog(string $gubun, array $res): int
|
||||
{
|
||||
// gubun: 'J' (회원가입), 'M'(정보수정)
|
||||
try {
|
||||
return (int) DB::table('mem_danalauthtel_log')->insertGetId([
|
||||
'gubun' => $gubun,
|
||||
'TID' => (string)($res['TID'] ?? ''),
|
||||
'res_code' => (string)($res['RETURNCODE'] ?? ''),
|
||||
'mem_no' => null,
|
||||
'info' => json_encode($res, JSON_UNESCAPED_UNICODE),
|
||||
'rgdate' => now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[danal] insert log failed', [
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
return 0; // 로그 실패해도 플로우는 계속
|
||||
}
|
||||
}
|
||||
|
||||
public function updateDanalAuthLogMemNo(int $logSeq, int $memNo): void
|
||||
{
|
||||
if ($logSeq <= 0 || $memNo <= 0) return;
|
||||
|
||||
try {
|
||||
DB::table('mem_danalauthtel_log')
|
||||
->where('seq', $logSeq)
|
||||
->update(['mem_no' => $memNo]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[danal] update log mem_no failed', [
|
||||
'seq' => $logSeq,
|
||||
'mem_no' => $memNo,
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
// 여기서 throw 할지 말지는 정책인데,
|
||||
// 레거시 흐름대로면 "가입은 살리고 로그만 실패"가 맞음.
|
||||
}
|
||||
}
|
||||
|
||||
public function precheckJoinIpFilterAndLog(array $userInfo): array
|
||||
{
|
||||
// $userInfo keys:
|
||||
// mem_no(없으면 0), cell_corp, cell_phone(암호화), email('-'), ip4, ip4_c(앞 3옥텟), dt_reg(optional)
|
||||
|
||||
$ip4 = (string)($userInfo['ip4'] ?? '');
|
||||
$ip4c = (string)($userInfo['ip4_c'] ?? '');
|
||||
if ($ip4 === '' || $ip4c === '') {
|
||||
return ['result' => 'P', 'seq' => 0, 'admin_phones' => []];
|
||||
}
|
||||
|
||||
// join_block 우선순위 A > S
|
||||
$row = DB::table('mem_join_filter')
|
||||
->whereIn('join_block', ['A', 'S'])
|
||||
->whereIn('gubun_code', ['01', '02'])
|
||||
->where(function ($q) use ($ip4, $ip4c) {
|
||||
$q->where('filter', $ip4c)
|
||||
->orWhere('filter', $ip4);
|
||||
})
|
||||
->orderByRaw("CASE join_block WHEN 'A' THEN 0 WHEN 'S' THEN 1 ELSE 9 END")
|
||||
->orderByDesc('seq')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return ['result' => 'P', 'seq' => 0, 'admin_phones' => []];
|
||||
}
|
||||
|
||||
$result = (string)($row->join_block ?? 'P'); // 'A' or 'S'
|
||||
$gubun = (string)($row->gubun_code ?? '');
|
||||
|
||||
// admin_phone JSON decode
|
||||
$adminPhones = [];
|
||||
$raw = $row->admin_phone ?? null;
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$j = json_decode($raw, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($j)) {
|
||||
$adminPhones = array_values(array_filter(array_map('trim', $j)));
|
||||
}
|
||||
}
|
||||
|
||||
// 관리자 SMS (S만 보낼지, A도 보낼지는 정책인데)
|
||||
// CI3 코드는 S에서만 발송했지만, 주석/정책상 A도 발송하는게 더 안전해서 A도 발송하도록 권장.
|
||||
if (in_array($result, ['S','A'], true) && !empty($adminPhones)) {
|
||||
foreach ($adminPhones as $phone) {
|
||||
$smsPayload = [
|
||||
'from_number' => config('services.sms.from', '1833-4856'),
|
||||
'to_number' => $phone,
|
||||
'message' => '[PIN FOR YOU] 회원가입필터 IP에서 가입 시도됨! 관리자 확인요망 '.date('m-d H:i'),
|
||||
'sms_type' => 'sms',
|
||||
];
|
||||
app(SmsService::class)->send($smsPayload, 'lguplus');
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ mem_join_log 기록 (CI3: S 또는 A 일때만 기록)
|
||||
$seq = (int) DB::table('mem_join_log')->insertGetId([
|
||||
'gubun' => $gubun, // CI3: gubun_code를 gubun에 저장
|
||||
'mem_no' => (int)($userInfo['mem_no'] ?? 0), // 가입 전이면 0
|
||||
'cell_corp' => (string)($userInfo['cell_corp'] ?? 'n'),
|
||||
'cell_phone' => (string)($userInfo['cell_phone'] ?? ''),
|
||||
'email' => (string)($userInfo['email'] ?? '-'),
|
||||
'ip4' => $ip4,
|
||||
'ip4_c' => $ip4c,
|
||||
'error_code' => $result, // ✅ join_block을 error_code에 저장(추적용)
|
||||
'dt_reg' => $userInfo['dt_reg'] ?? date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'result' => $result, // 'A' or 'S'
|
||||
'seq' => $seq,
|
||||
'admin_phones' => $adminPhones,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 가입 성공 후 mem_no/email 업데이트 (CI3 ip_check_update 대응)
|
||||
*/
|
||||
public function updateJoinLogAfterSignup(int $seq, int $memNo, string $email): void
|
||||
{
|
||||
DB::table('mem_join_log')
|
||||
->where('seq', $seq)
|
||||
->update([
|
||||
'mem_no' => $memNo,
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2,9 +2,14 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\MemInfo;
|
||||
use App\Models\Member\MemInfo;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
use App\Support\LegacyCrypto\CiPassword;
|
||||
use App\Support\Legacy\LoginReason;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MemInfoService
|
||||
{
|
||||
@ -49,13 +54,12 @@ class MemInfoService
|
||||
/**
|
||||
* CI: mem_reg() (간소화 버전)
|
||||
* - 실제로는 validation은 FormRequest에서 처리 권장
|
||||
* - stat_3=3 (1969 이전 출생 접근금지) 룰 포함
|
||||
*/
|
||||
public function register(array $data): MemInfo
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
|
||||
$email = strtolower($data['email']);
|
||||
$email = strtolower((string)($data['email'] ?? ''));
|
||||
|
||||
// 중복 체크 + 잠금 (CI for update)
|
||||
$exists = MemInfo::query()
|
||||
@ -68,11 +72,12 @@ class MemInfoService
|
||||
}
|
||||
|
||||
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||
$notnull = "1000-01-01 00:00:00";
|
||||
|
||||
$mem = new MemInfo();
|
||||
$mem->email = $email;
|
||||
$mem->name = $data['name'] ?? '';
|
||||
$mem->pv_sns = $data['pv_sns'] ?? 'self';
|
||||
$mem->name = (string)($data['name'] ?? '');
|
||||
$mem->pv_sns = (string)($data['pv_sns'] ?? 'self');
|
||||
|
||||
$promotion = !empty($data['promotion']) ? 'y' : 'n';
|
||||
$mem->rcv_email = $promotion;
|
||||
@ -85,26 +90,42 @@ class MemInfoService
|
||||
$mem->dt_rcv_email = $now;
|
||||
$mem->dt_rcv_sms = $now;
|
||||
$mem->dt_rcv_push = $now;
|
||||
|
||||
$mem->dt_stat_1 = $now;
|
||||
$mem->dt_stat_2 = $now;
|
||||
$mem->dt_stat_3 = $now;
|
||||
$mem->dt_stat_4 = $now;
|
||||
$mem->dt_stat_5 = $now;
|
||||
|
||||
$mem->ip_reg = $data['ip_reg'] ?? request()->ip();
|
||||
$mem->dt_mod = $notnull;
|
||||
$mem->dt_vact = $notnull;
|
||||
$mem->dt_dor = $notnull;
|
||||
$mem->dt_ret_dor = $notnull;
|
||||
$mem->dt_req_out = "1000-01-01";
|
||||
$mem->dt_out = $notnull;
|
||||
|
||||
$mem->ip_reg = (string)($data['ip_reg'] ?? request()->ip());
|
||||
|
||||
// 국가/본인인증 값들
|
||||
$mem->country_code = $data['country_code'] ?? '';
|
||||
$mem->country_name = $data['country_name'] ?? '';
|
||||
$mem->birth = $data['birth'] ?? '0000-00-00';
|
||||
$mem->cell_corp = $data['cell_corp'] ?? 'n';
|
||||
$mem->cell_phone = $data['cell_phone'] ?? ''; // ⚠️ 암호화 저장이라면 여기서 암호화해서 넣어야 함
|
||||
$mem->native = $data['native'] ?? 'n';
|
||||
$mem->country_code = (string)($data['country_code'] ?? '');
|
||||
$mem->country_name = (string)($data['country_name'] ?? '');
|
||||
$mem->birth = (string)($data['birth'] ?? '0000-00-00');
|
||||
$mem->cell_corp = (string)($data['cell_corp'] ?? 'n');
|
||||
|
||||
// 휴대폰 암호화 (빈값이면 빈값)
|
||||
$rawPhone = (string)($data['cell_phone'] ?? '');
|
||||
if ($rawPhone !== '') {
|
||||
/** @var CiSeedCrypto $seed */
|
||||
$seed = app(CiSeedCrypto::class);
|
||||
$mem->cell_phone = (string)$seed->encrypt($rawPhone);
|
||||
} else {
|
||||
$mem->cell_phone = '';
|
||||
}
|
||||
|
||||
$mem->native = (string)($data['native'] ?? 'n');
|
||||
$mem->ci = $data['ci'] ?? null;
|
||||
$mem->ci_v = $data['ci_v'] ?? '';
|
||||
$mem->ci_v = (string)($data['ci_v'] ?? '');
|
||||
$mem->di = $data['di'] ?? null;
|
||||
$mem->gender = $data['gender'] ?? 'n';
|
||||
$mem->gender = (string)($data['gender'] ?? 'n');
|
||||
|
||||
// 1969년 이전 출생 접근금지(stat_3=3)
|
||||
$birthY = (int)substr((string)$mem->birth, 0, 4);
|
||||
@ -118,9 +139,6 @@ class MemInfoService
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: last_login()
|
||||
*/
|
||||
public function updateLastLogin(int $memNo): void
|
||||
{
|
||||
MemInfo::query()
|
||||
@ -132,9 +150,6 @@ class MemInfoService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: fail_count()
|
||||
*/
|
||||
public function incrementLoginFail(int $memNo): void
|
||||
{
|
||||
MemInfo::query()
|
||||
@ -144,4 +159,333 @@ class MemInfoService
|
||||
'dt_mod' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function dt6(): string
|
||||
{
|
||||
return Carbon::now()->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
|
||||
private function ip4c(string $ip): string
|
||||
{
|
||||
$oct = explode('.', $ip);
|
||||
if (count($oct) >= 3) {
|
||||
return $oct[0].'.'.$oct[1].'.'.$oct[2];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
private function parseUa(string $ua): array
|
||||
{
|
||||
$platform = '';
|
||||
if (stripos($ua, 'Windows') !== false) $platform = 'Windows';
|
||||
elseif (stripos($ua, 'Mac OS X') !== false) $platform = 'macOS';
|
||||
elseif (stripos($ua, 'Android') !== false) $platform = 'Android';
|
||||
elseif (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false) $platform = 'iOS';
|
||||
elseif (stripos($ua, 'Linux') !== false) $platform = 'Linux';
|
||||
|
||||
$browser = 'Unknown';
|
||||
$version = '';
|
||||
$candidates = [
|
||||
'Edg/' => 'Edge',
|
||||
'Chrome/' => 'Chrome',
|
||||
'Firefox/' => 'Firefox',
|
||||
'Safari/' => 'Safari',
|
||||
];
|
||||
|
||||
foreach ($candidates as $needle => $name) {
|
||||
$pos = stripos($ua, $needle);
|
||||
if ($pos !== false) {
|
||||
$browser = $name;
|
||||
$sub = substr($ua, $pos + strlen($needle));
|
||||
$version = preg_split('/[^0-9\.]/', $sub)[0] ?? '';
|
||||
// Safari는 Chrome UA에도 같이 끼므로 Chrome 우선순위를 위에서 처리
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [$platform, trim($browser), trim($version)];
|
||||
}
|
||||
|
||||
/**
|
||||
* mem_auth 기반 레벨 계산 (CI3 get_mem_level 이식)
|
||||
*/
|
||||
private function getMemLevel(int $memNo): array
|
||||
{
|
||||
$rows = DB::table('mem_auth')
|
||||
->select(['auth_type','auth_state'])
|
||||
->where('mem_no', $memNo)
|
||||
->where('auth_state', 'Y')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$state = ['email'=>false,'cell'=>false,'account'=>false,'otp'=>false,'ars'=>false];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$t = strtolower((string)$r->auth_type);
|
||||
if (array_key_exists($t, $state)) $state[$t] = true;
|
||||
}
|
||||
|
||||
$level = 0;
|
||||
if ($state['email']) $level = 1;
|
||||
if ($level === 1 && $state['cell']) $level = 2;
|
||||
if ($level === 2 && $state['account']) $level = 3;
|
||||
if ($level === 3 && $state['otp']) $level = 4;
|
||||
if ($level === 2 && $state['ars']) $level = 5;
|
||||
|
||||
return ['level'=>$level, 'auth_state'=>$state];
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도별 로그인 테이블 자동 생성 (B 선택)
|
||||
* - mem_login_recent 스키마를 그대로 복제
|
||||
*/
|
||||
private function ensureLoginYearlyTable(int $year): string
|
||||
{
|
||||
$year = (int)$year;
|
||||
if ($year < 2000 || $year > 2100) {
|
||||
// 안전장치
|
||||
return 'mem_login_recent';
|
||||
}
|
||||
|
||||
$table = "mem_login_{$year}";
|
||||
|
||||
// DB마다 동작이 달라서 가장 단순하고 확실한 DDL로 처리
|
||||
// CREATE TABLE IF NOT EXISTS mem_login_YYYY LIKE mem_login_recent
|
||||
DB::statement("CREATE TABLE IF NOT EXISTS `{$table}` LIKE `mem_login_recent`");
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
private function insertLoginLog(string $table, array $d): void
|
||||
{
|
||||
DB::statement(
|
||||
"INSERT INTO `{$table}`
|
||||
SET mem_no=?,
|
||||
sf=?,
|
||||
conn=?,
|
||||
ip4_aton=inet_aton(?),
|
||||
ip4=?,
|
||||
ip4_c=SUBSTRING_INDEX(?,'.',3),
|
||||
dt_reg=?,
|
||||
platform=?,
|
||||
browser=?,
|
||||
pattern=?,
|
||||
error_code=?",
|
||||
[
|
||||
$d['mem_no'],
|
||||
$d['sf'],
|
||||
$d['conn'],
|
||||
$d['ip4'],
|
||||
$d['ip4'],
|
||||
$d['ip4'],
|
||||
$d['dt_reg'],
|
||||
$d['platform'],
|
||||
$d['browser'],
|
||||
$d['pattern'],
|
||||
$d['error_code'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function attemptLegacyLogin(array $in): array
|
||||
{
|
||||
$email = strtolower(trim((string)($in['email'] ?? '')));
|
||||
$pw = (string)($in['password'] ?? '');
|
||||
$ip = (string)($in['ip'] ?? request()->ip());
|
||||
$ua = (string)($in['ua'] ?? '');
|
||||
$returnUrl = (string)($in['return_url'] ?? '/');
|
||||
|
||||
if ($email === '' || $pw === '') {
|
||||
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.'];
|
||||
}
|
||||
|
||||
$dtNow6 = $this->dt6();
|
||||
$dtY = (int)substr($dtNow6, 0, 4);
|
||||
|
||||
// UA 파싱
|
||||
[$platform, $browser, $version] = $this->parseUa($ua);
|
||||
$browserFull = trim($browser.' '.$version);
|
||||
|
||||
//$yearTable = $this->ensureLoginYearlyTable((int)$dtY);
|
||||
|
||||
|
||||
return DB::transaction(function () use ($email, $pw, $ip, $ua, $returnUrl, $dtNow6, $dtY, $platform, $browserFull) {
|
||||
$yearTable = "mem_login_".(int)$dtY;
|
||||
|
||||
/** @var MemInfo|null $mem */
|
||||
$mem = MemInfo::query()
|
||||
->select([
|
||||
'mem_no','email','name','cell_phone','cell_corp',
|
||||
'dt_login','dt_reg','login_fail_cnt',
|
||||
'pv_sns',
|
||||
'stat_1','stat_2','stat_3','stat_4','stat_5',
|
||||
'native','country_code'
|
||||
])
|
||||
->where('email', $email)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
// 아이디 없음 -> mem_no가 없으니 fail_count/log 저장 불가. 메시지만 통일.
|
||||
if (!$mem) {
|
||||
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.1'];
|
||||
}
|
||||
|
||||
// stat_3 차단 로직 (CI3 id_exists 반영)
|
||||
if ((string)$mem->stat_3 === '3') {
|
||||
return ['ok'=>false, 'message'=>"접근금지 계정입니다.<br><br>고객센터 1833-4856로 문의 하세요"];
|
||||
}
|
||||
if ((string)$mem->stat_3 === '4') {
|
||||
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.2'];
|
||||
}
|
||||
if ((string)$mem->stat_3 === '5') {
|
||||
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.3'];
|
||||
}
|
||||
|
||||
// 휴면(stat_3=6) 처리: 지금은 테이블 저장 + 안내까지만 (메일 연결은 다음 단계에서)
|
||||
if ((string)$mem->stat_3 === '6') {
|
||||
// TODO: mem_dormancy insert + authnum 생성 + 메일 발송 연결
|
||||
return ['ok'=>false, 'message'=>'회원님 계정은 휴면계정입니다. 이메일 인증 후 이용 가능합니다.4'];
|
||||
}
|
||||
|
||||
// mem_st_ring 비번 로드
|
||||
$ring = DB::table('mem_st_ring')->where('mem_no', $mem->mem_no)->first();
|
||||
if (!$ring || empty($ring->str_0)) {
|
||||
$reason = config('legacy.login_reason.L_NOT_EXISTS_PASS');
|
||||
// 실패 카운트 + 실패 로그
|
||||
$this->incrementLoginFail((int)$mem->mem_no);
|
||||
|
||||
$log = [
|
||||
'mem_no' => (int)$mem->mem_no,
|
||||
'sf' => 'f',
|
||||
'conn' => '1',
|
||||
'ip4' => $ip,
|
||||
'ip4_c' => $this->ip4c($ip),
|
||||
'dt_reg' => $dtNow6,
|
||||
'platform' => $platform,
|
||||
'browser' => $browserFull,
|
||||
'pattern' => 'self',
|
||||
'error_code' => $reason,
|
||||
];
|
||||
|
||||
|
||||
$this->insertLoginLog('mem_login_recent', $log);
|
||||
$this->insertLoginLog($yearTable, $log);
|
||||
|
||||
return ['ok'=>false, 'message'=>'아이디 혹은 비밀번호가 일치하지 않습니다.5'];
|
||||
}
|
||||
|
||||
// 비번 검증 (PASS_SET=0)
|
||||
$try = CiPassword::make($pw, 0);
|
||||
$dbPass = (string)$ring->str_0;
|
||||
|
||||
if ($try === '' || strcmp($try, $dbPass) !== 0) {
|
||||
$reason = config('legacy.login_reason.L_INCORRECT_PASS');
|
||||
$failCnt = (int)$mem->login_fail_cnt;
|
||||
|
||||
// 5회 실패 안내(>=4면 이번이 5회)
|
||||
if ($failCnt >= 4) {
|
||||
$reason = config('legacy.login_reason.L_LOGIN_FAIL');
|
||||
}
|
||||
|
||||
// 실패 카운트 + 실패 로그
|
||||
$this->incrementLoginFail((int)$mem->mem_no);
|
||||
|
||||
$log = [
|
||||
'mem_no' => (int)$mem->mem_no,
|
||||
'sf' => 'f',
|
||||
'conn' => '1',
|
||||
'ip4' => $ip,
|
||||
'ip4_c' => $this->ip4c($ip),
|
||||
'dt_reg' => $dtNow6,
|
||||
'platform' => $platform,
|
||||
'browser' => $browserFull,
|
||||
'pattern' => 'self',
|
||||
'error_code' => $reason,
|
||||
];
|
||||
|
||||
$this->insertLoginLog('mem_login_recent', $log);
|
||||
$this->insertLoginLog($yearTable, $log);
|
||||
|
||||
if ($failCnt >= 4) {
|
||||
return ['ok'=>false, 'message'=>"<a href='/member/find_pass?menu=pass' style='color: #fff;font-size:13px'>5회이상 실패시 비밀번호찾기 후 이용 바랍니다.(클릭)</a>"];
|
||||
}
|
||||
|
||||
return ['ok'=>false, 'message'=>"비밀번호가 일치하지 않습니다.\n비밀번호 실패횟수 : ".($failCnt+1)."\n5회 이상 실패시 인증을 다시받아야 합니다."];
|
||||
}
|
||||
|
||||
// 레벨 체크 (email 인증 필수)
|
||||
$levelInfo = $this->getMemLevel((int)$mem->mem_no);
|
||||
if (($levelInfo['level'] ?? 0) < 1 || empty($levelInfo['auth_state']['email'])) {
|
||||
return [
|
||||
'ok'=>false,
|
||||
'message'=>"<br>이메일 인증 완료후 이용가능합니다. \n이메일주소(".$mem->email.") 메일을 확인하세요\n",
|
||||
];
|
||||
}
|
||||
|
||||
// 로그인 차단 IP 대역 체크
|
||||
$ip4c = $this->ip4c($ip);
|
||||
$blocked = DB::table('filter_login_ip_reject')
|
||||
->where('mem_no', $mem->mem_no)
|
||||
->where('ip4_c', $ip4c)
|
||||
->exists();
|
||||
|
||||
if ($blocked) {
|
||||
return ['ok'=>false, 'message'=>"회원님의 설정에 의해 접속이 차단되었습니다.6"];
|
||||
}
|
||||
|
||||
// 최근 로그인 업데이트(성공 시 fail_count reset)
|
||||
$this->updateLastLogin((int)$mem->mem_no);
|
||||
|
||||
// 성공 로그 저장(최근 + 연도별)
|
||||
$log = [
|
||||
'mem_no' => (int)$mem->mem_no,
|
||||
'sf' => 's',
|
||||
'conn' => '1',
|
||||
'ip4' => $ip,
|
||||
'ip4_c' => $ip4c,
|
||||
'dt_reg' => $dtNow6,
|
||||
'platform' => $platform,
|
||||
'browser' => $browserFull,
|
||||
'pattern' => 'self',
|
||||
'error_code' => '',
|
||||
];
|
||||
|
||||
$this->insertLoginLog('mem_login_recent', $log);
|
||||
$this->insertLoginLog($yearTable, $log);
|
||||
|
||||
// 첫 로그인 여부
|
||||
$login1st = 'n';
|
||||
if ((string)$mem->dt_login === (string)$mem->dt_reg) {
|
||||
$login1st = 'y';
|
||||
}
|
||||
|
||||
// ✅ 세션 payload (CI3 키 유지)
|
||||
$session = [
|
||||
'_login_' => true,
|
||||
'_mid' => (string)$mem->email,
|
||||
'_mno' => (int)$mem->mem_no,
|
||||
'_mname' => (string)$mem->name,
|
||||
'_mstat_1' => (string)$mem->stat_1,
|
||||
'_mstat_2' => (string)$mem->stat_2,
|
||||
'_mstat_3' => (string)$mem->stat_3,
|
||||
'_mstat_4' => (string)$mem->stat_4,
|
||||
'_mstat_5' => (string)$mem->stat_5,
|
||||
'_mcell' => (string)$mem->cell_phone,
|
||||
'_mpv_sns' => (string)$mem->pv_sns,
|
||||
'_mnative' => (string)$mem->native,
|
||||
'_mcountry_code' => (string)$mem->country_code,
|
||||
'_ip' => $ip,
|
||||
'_login_1st' => $login1st,
|
||||
'_dt_reg' => (string)$mem->dt_reg,
|
||||
'auth_ars' => !empty($levelInfo['auth_state']['ars']) ? 'Y' : 'N',
|
||||
];
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'session' => $session,
|
||||
'redirect' => $returnUrl ?: '/',
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
10
app/Support/AuthSession.php
Normal file
10
app/Support/AuthSession.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace App\Support;
|
||||
|
||||
final class AuthSession
|
||||
{
|
||||
public static function putMember(array $data): void
|
||||
{
|
||||
session()->put('_sess', $data); // 예시 (너희 세션 구조에 맞게)
|
||||
}
|
||||
}
|
||||
23
app/Support/Legacy/Carrier.php
Normal file
23
app/Support/Legacy/Carrier.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Legacy;
|
||||
|
||||
final class Carrier
|
||||
{
|
||||
public static function toCode(?string $raw): string
|
||||
{
|
||||
$raw = trim((string)$raw);
|
||||
if ($raw === '') return (string) config('legacy.carrier.default', 'n');
|
||||
|
||||
$upper = strtoupper($raw);
|
||||
|
||||
// 이미 코드면 그대로
|
||||
$allowed = (array) config('legacy.carrier.allowed_codes', ['n']);
|
||||
if (in_array($upper, $allowed, true)) {
|
||||
return $upper;
|
||||
}
|
||||
|
||||
$map = (array) config('legacy.carrier.codes', []);
|
||||
return (string) ($map[$upper] ?? config('legacy.carrier.default', 'n'));
|
||||
}
|
||||
}
|
||||
50
app/Support/LegacyCrypto/CiPassword.php
Normal file
50
app/Support/LegacyCrypto/CiPassword.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace App\Support\LegacyCrypto;
|
||||
|
||||
final class CiPassword
|
||||
{
|
||||
private const PASS_KEY = 'WmhEbHNGbmFAKTE3MSEwJmNvSW5Sb29NKw==';
|
||||
private const PASS_SET = 0;
|
||||
|
||||
private const METHOD = 'aes-256-ctr';
|
||||
private const IV = '567-av23-901-345';
|
||||
|
||||
private static function opt(): int
|
||||
{
|
||||
// CI3에서 define('OPENSSL_ZERO_PADDING', 2) 하던 것과 동일하게 맞춤
|
||||
return defined('OPENSSL_ZERO_PADDING') ? OPENSSL_ZERO_PADDING : 2;
|
||||
}
|
||||
|
||||
/** @return array{0:string,1:string,2:string} */
|
||||
public static function makeAll(string $plain): array
|
||||
{
|
||||
$key = self::PASS_KEY;
|
||||
$iv = self::IV;
|
||||
$opt = self::opt();
|
||||
|
||||
$enc0 = hash_hmac('ripemd320', $plain, $key);
|
||||
$enc0 = hash('sha512', $enc0);
|
||||
$enc0 = openssl_encrypt($enc0, self::METHOD, $key, $opt, $iv);
|
||||
|
||||
$enc1 = hash_hmac('ripemd320', $plain, $key);
|
||||
$enc1 = openssl_encrypt($enc1, self::METHOD, $key, $opt, $iv);
|
||||
$enc1 = hash('sha512', $enc1);
|
||||
|
||||
$enc2 = openssl_encrypt($plain, self::METHOD, $key, $opt, $iv);
|
||||
$enc2 = hash('sha512', $enc2);
|
||||
$enc2 = hash_hmac('ripemd320', $enc2, $key);
|
||||
|
||||
return [$enc0, $enc1, $enc2];
|
||||
}
|
||||
|
||||
public static function make(string $plain, int $set = self::PASS_SET): string
|
||||
{
|
||||
$all = self::makeAll($plain);
|
||||
return $all[$set] ?? $all[0];
|
||||
}
|
||||
|
||||
public static function makePass2(string $pin2): string
|
||||
{
|
||||
return hash('sha512', $pin2);
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
$middleware->validateCsrfTokens(except: [
|
||||
'auth/register/danal/result', //다날 PASS 콜백 (외부 서버가 호출)
|
||||
]);
|
||||
|
||||
//페이지 접근권한 미들웨어 등록
|
||||
$middleware->alias([
|
||||
'legacy.auth' => \App\Http\Middleware\LegacyAuth::class, //로그인후 접근가능
|
||||
'legacy.guest' => \App\Http\Middleware\LegacyGuest::class, //게스트 접근가능페이지
|
||||
]);
|
||||
|
||||
})
|
||||
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
@ -6,5 +6,46 @@ return [
|
||||
'inner_encoding' => 'UTF-8',
|
||||
'block' => 16,
|
||||
'iv' => [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
|
||||
];
|
||||
|
||||
//로그인 오류 분류
|
||||
'login_reason' => [
|
||||
'L_NOT_EXISTS_ID' => 'E1', # 아이디 없음
|
||||
'L_NOT_EXISTS_PASS' => 'E2', # 패스워드 없음
|
||||
'L_INCORRECT_PASS' => 'E3', # 패스워드 불일치
|
||||
'L_REJECT' => 'E4', # 회원설정 접속차단
|
||||
'L_LOGIN_TIME' => 'E5', # 최근로그인일시 업데이트실패
|
||||
'L_LOGIN_LOG_RECENT' => 'E6', # 로그인성공 로그 기록 실패
|
||||
'L_LOGIN_LOG_YYYY' => 'E7', # 로그인성공 로그 기록 실패
|
||||
'L_NOT_SNS_REG' => 'E8', # SNS 연동 가입 아님
|
||||
'L_INCORRECT_SNS' => 'E9', # 아이디는 같지만 가입 sns 틀림
|
||||
'L_LOGIN_FAIL' => 'E0', # 로그인5회오류
|
||||
],
|
||||
|
||||
//통신사.
|
||||
'carrier' => [
|
||||
'default' => 'n',
|
||||
|
||||
// 표준 코드
|
||||
'codes' => [
|
||||
'SKT' => '01',
|
||||
'KT' => '02',
|
||||
'LGU+' => '03',
|
||||
'LGU' => '03',
|
||||
'LGT' => '03',
|
||||
|
||||
// 알뜰폰(표기 케이스 방어)
|
||||
'SKT_MVNO' => '04',
|
||||
'MVNO_SKT' => '04',
|
||||
|
||||
'KT_MVNO' => '05',
|
||||
'MVNO_KT' => '05',
|
||||
|
||||
'LGU_MVNO' => '06',
|
||||
'LGU+_MVNO' => '06',
|
||||
'MVNO_LGU' => '06',
|
||||
],
|
||||
|
||||
// 이미 코드로 들어온 경우 허용
|
||||
'allowed_codes' => ['01','02','03','04','05','06','n'],
|
||||
],
|
||||
];
|
||||
|
||||
@ -152,15 +152,6 @@ img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Header layout fix */
|
||||
.site-header .container{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center; /* 전체 세로 가운데 */
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-left{
|
||||
display: flex;
|
||||
align-items: center; /* 로고+메뉴 세로 가운데 */
|
||||
@ -298,11 +289,12 @@ h1, h2, h3, h4, h5, h6 {
|
||||
background-color: var(--color-bg-tint);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
@ -314,6 +306,20 @@ h1, h2, h3, h4, h5, h6 {
|
||||
height: var(--header-height-scroll);
|
||||
}
|
||||
|
||||
/* 헤더에 가려지지 않게 상단 여백 확보 */
|
||||
body {
|
||||
padding-top: var(--header-height-desktop);
|
||||
}
|
||||
|
||||
/* Header layout fix */
|
||||
.site-header .container{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center; /* 전체 세로 가운데 */
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
@ -4039,3 +4045,331 @@ body.is-drawer-open{
|
||||
.pagination-wrap { margin-top: 18px; }
|
||||
.pg-btn, .pg-page, .pg-ellipsis { height: 32px; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Header Profile Dropdown (Tailwind-less fallback)
|
||||
- Works with the exact Blade markup you pasted
|
||||
========================================================= */
|
||||
|
||||
/* wrapper positioning */
|
||||
.header-profile {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* button (btn btn-ghost fallback) */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: rgba(15, 23, 42, 0.85); /* slate-ish */
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: rgba(2, 6, 23, 0.06);
|
||||
border-color: rgba(2, 6, 23, 0.08);
|
||||
}
|
||||
|
||||
.btn-ghost:active {
|
||||
background: rgba(2, 6, 23, 0.10);
|
||||
}
|
||||
|
||||
/* truncate + max width (truncate max-w-[120px]) */
|
||||
.header-profile .truncate {
|
||||
display: inline-block;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* dropdown panel wrapper:
|
||||
markup uses: absolute right-0 mt-2 w-48 hidden group-hover:block
|
||||
We'll style the dropdown container and show on hover */
|
||||
.header-profile .profile-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%; /* 버튼 바로 아래에 붙임 */
|
||||
margin-top: 0; /* ✅ gap 제거 */
|
||||
width: 192px;
|
||||
display: none;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.profile-dropdown .profile-card {
|
||||
margin-top: 10px; /* ✅ 시각적 간격은 카드에만 */
|
||||
}
|
||||
|
||||
/* show on hover (group-hover:block replacement) */
|
||||
.header-profile:hover .profile-dropdown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-profile:hover .profile-dropdown,
|
||||
.header-profile .profile-dropdown:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-profile .profile-dropdown::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -12px; /* 버튼 아래 ~ 메뉴 위 연결 */
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* dropdown card (rounded-xl border bg-white shadow-lg overflow-hidden) */
|
||||
.profile-dropdown .profile-card {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(2, 6, 23, 0.10);
|
||||
border-radius: 14px; /* rounded-xl 느낌 */
|
||||
box-shadow: 0 12px 30px rgba(2, 6, 23, 0.12);
|
||||
overflow: hidden;
|
||||
min-width: 192px;
|
||||
}
|
||||
|
||||
/* top email line */
|
||||
.profile-dropdown .profile-meta {
|
||||
padding: 10px 14px;
|
||||
font-size: 12px;
|
||||
color: rgba(2, 6, 23, 0.55);
|
||||
background: rgba(2, 6, 23, 0.02);
|
||||
}
|
||||
|
||||
/* separators */
|
||||
.profile-dropdown .profile-sep {
|
||||
height: 1px;
|
||||
background: rgba(2, 6, 23, 0.08);
|
||||
}
|
||||
|
||||
/* menu links */
|
||||
.profile-dropdown a,
|
||||
.profile-dropdown button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
color: rgba(15, 23, 42, 0.90);
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.profile-dropdown a:hover,
|
||||
.profile-dropdown button:hover {
|
||||
background: rgba(2, 6, 23, 0.04);
|
||||
}
|
||||
|
||||
.profile-dropdown a:active,
|
||||
.profile-dropdown button:active {
|
||||
background: rgba(2, 6, 23, 0.07);
|
||||
}
|
||||
|
||||
/* logout button emphasis */
|
||||
.profile-dropdown .logout-btn {
|
||||
color: rgba(220, 38, 38, 0.92); /* red */
|
||||
}
|
||||
|
||||
/* accessibility: keyboard focus */
|
||||
.profile-dropdown a:focus,
|
||||
.profile-dropdown button:focus,
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.profile-dropdown a:focus-visible,
|
||||
.profile-dropdown button:focus-visible,
|
||||
.btn:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Optional: small arrow pointer */
|
||||
.profile-dropdown .profile-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: -7px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fff;
|
||||
border-left: 1px solid rgba(2, 6, 23, 0.10);
|
||||
border-top: 1px solid rgba(2, 6, 23, 0.10);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* ensure ::before positions correctly */
|
||||
.profile-dropdown {
|
||||
position: absolute;
|
||||
}
|
||||
.profile-dropdown .profile-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Mobile: open on tap is JS territory, but at least keep it usable */
|
||||
@media (max-width: 640px) {
|
||||
.header-profile .truncate { max-width: 90px; }
|
||||
.header-profile .profile-dropdown { width: 180px; }
|
||||
}
|
||||
|
||||
/* Mobile Drawer User Card (v2) */
|
||||
.m-usercard2{
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(255,255,255,.92);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
/* head */
|
||||
.m-usercard2__head{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.m-usercard2__badge{
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .06em;
|
||||
padding: 6px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(0,0,0,.03);
|
||||
}
|
||||
|
||||
.m-usercard2__title{
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.m-usercard2__sub{
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-muted, #6b7280);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-usercard2__icon{
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(0,0,0,.03);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* profile */
|
||||
.m-usercard2__profile{
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgba(255,255,255,.65);
|
||||
}
|
||||
|
||||
.m-usercard2__avatar{
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 900;
|
||||
background: rgba(0,0,0,.06);
|
||||
}
|
||||
|
||||
.m-usercard2__avatar--ghost{
|
||||
background: rgba(0,0,0,.03);
|
||||
}
|
||||
|
||||
.m-usercard2__info{
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m-usercard2__name{
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.m-usercard2__meta{
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-muted, #6b7280);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* actions */
|
||||
.m-usercard2__actions{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* button system (drawer-only) */
|
||||
.m-btn2{
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
background: var(--color-primary, #ff6a00);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.m-btn2--ghost{
|
||||
background: transparent;
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text, #111827);
|
||||
}
|
||||
|
||||
.m-btn2--text{
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--color-muted, #6b7280);
|
||||
}
|
||||
|
||||
/* 로그인 상태: 로그아웃은 2칸을 차지해서 아래로 명확히 */
|
||||
.m-usercard2.is-login .m-usercard2__logout{
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.m-usercard2__logout{ margin: 0; }
|
||||
.m-usercard2__logout button{ width: 100%; }
|
||||
|
||||
61
resources/views/web/auth/debug_payload.blade.php
Normal file
61
resources/views/web/auth/debug_payload.blade.php
Normal file
@ -0,0 +1,61 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '회원가입 Debug | PIN FOR YOU')
|
||||
|
||||
@section('auth_content')
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">회원가입 Debug</h2>
|
||||
|
||||
<div style="margin:12px 0;">
|
||||
<div style="font-weight:800;margin-bottom:6px;">Payload(session)</div>
|
||||
<pre style="white-space:pre-wrap; font-size:12px; background:rgba(0,0,0,.25); padding:12px; border-radius:12px;">
|
||||
{{ json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT) }}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap; margin: 14px 0;">
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.debug.run') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="mem_info">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">DRYRUN: mem_info</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.debug.run') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="mem_st_ring">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">DRYRUN: mem_st_ring</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.debug.run') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="mem_auth">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">DRYRUN: mem_auth</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.debug.run') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="mem_auth_log">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">DRYRUN: mem_auth_log</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if(!empty($last))
|
||||
<hr style="opacity:.2;margin:16px 0;">
|
||||
|
||||
<div style="font-weight:900;">마지막 실행 결과</div>
|
||||
<div style="margin:8px 0; opacity:.85;">
|
||||
step: <b>{{ $last['step'] ?? '-' }}</b> /
|
||||
mode: <b>{{ !empty($last['commit']) ? 'COMMIT' : 'DRY RUN(ROLLBACK)' }}</b> /
|
||||
ok: <b>{{ !empty($last['ok']) ? 'true' : 'false' }}</b>
|
||||
</div>
|
||||
<div style="white-space:pre-line; opacity:.85;">{{ $last['message'] ?? '' }}</div>
|
||||
|
||||
<div style="margin-top:12px;">
|
||||
<div style="font-weight:800;margin-bottom:6px;">Captured Queries</div>
|
||||
<pre style="white-space:pre-wrap; font-size:12px; background:rgba(0,0,0,.25); padding:12px; border-radius:12px;">
|
||||
{{ json_encode($last['queries'] ?? [], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT) }}
|
||||
</pre>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@ -10,8 +10,20 @@
|
||||
@section('subheadline', '로그인 후 구매/문의 내역을 빠르게 확인할 수 있어요.')
|
||||
@section('card_aria', '로그인 폼')
|
||||
|
||||
{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}}
|
||||
@push('recaptcha')
|
||||
<script>window.__recaptchaSiteKey = @json(config('services.recaptcha.site_key'));</script>
|
||||
<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.site_key') }}"></script>
|
||||
<script src="{{ asset('assets/js/recaptcha-v3.js') }}"></script>
|
||||
@endpush
|
||||
|
||||
@section('auth_content')
|
||||
<form class="auth-form" onsubmit="return false;">
|
||||
<form class="auth-form" method="post" action="{{ route('web.auth.login.prc') }}" id="loginForm">
|
||||
@csrf
|
||||
|
||||
<input type="hidden" name="return_url" value="{{ request('return_url', '/') }}">
|
||||
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">
|
||||
|
||||
<img
|
||||
class="reg-step0-hero__img"
|
||||
src="{{ asset('assets/images/web/member/login.webp') }}"
|
||||
@ -22,20 +34,21 @@
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="login_id">아이디(이메일)</label>
|
||||
<input class="auth-input" id="login_id" type="email" placeholder="example@domain.com" autocomplete="username">
|
||||
<input class="auth-input" id="login_id" name="mem_email" type="email"
|
||||
placeholder="example@domain.com" autocomplete="username"
|
||||
value="{{ old('mem_email') }}">
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="login_pw">비밀번호</label>
|
||||
<input class="auth-input" id="login_pw" type="password" placeholder="비밀번호" autocomplete="current-password">
|
||||
<input class="auth-input" id="login_pw" name="mem_pw" type="password"
|
||||
placeholder="비밀번호" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="auth-row">
|
||||
<label class="auth-check">
|
||||
<input type="checkbox">
|
||||
자동 로그인
|
||||
{{-- <input type="checkbox" name="auto_login" value="1">--}}
|
||||
{{-- 자동 로그인--}}
|
||||
</label>
|
||||
|
||||
<div class="auth-links-inline">
|
||||
<a class="auth-link" href="{{ route('web.auth.find_id') }}">아이디 찾기</a>
|
||||
<span class="auth-dot">·</span>
|
||||
@ -43,7 +56,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">로그인</button>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.register') }}">회원가입</a>
|
||||
@ -60,3 +72,94 @@
|
||||
<a class="auth-link" href="{{ route('web.cs.kakao.index') }}">카카오 상담</a>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function(){
|
||||
const form = document.getElementById('loginForm');
|
||||
if (!form) return;
|
||||
|
||||
const emailEl = document.getElementById('login_id');
|
||||
const pwEl = document.getElementById('login_pw');
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
|
||||
// showMsg / clearMsg 가 공통으로 있으면 그대로 활용
|
||||
async function alertMsg(msg) {
|
||||
if (typeof showMsg === 'function') {
|
||||
await showMsg(msg, { type: 'alert', title: '입력오류' });
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmail(v){
|
||||
// 너무 빡세게 잡지 말고 기본만
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (typeof clearMsg === 'function') clearMsg();
|
||||
|
||||
const email = (emailEl?.value || '').trim();
|
||||
const pw = (pwEl?.value || '');
|
||||
|
||||
if (!email) {
|
||||
await alertMsg('아이디(이메일)를 입력해 주세요.');
|
||||
emailEl?.focus();
|
||||
return;
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
await alertMsg('아이디는 이메일 형식이어야 합니다.');
|
||||
emailEl?.focus();
|
||||
return;
|
||||
}
|
||||
if (!pw) {
|
||||
await alertMsg('비밀번호를 입력해 주세요.');
|
||||
pwEl?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 버튼 잠금
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
try {
|
||||
// ✅ 운영에서만 recaptcha 토큰 넣기 (서버도 동일 정책)
|
||||
@if(app()->environment('production') && config('services.recaptcha.site_key'))
|
||||
const hidden = document.getElementById('g-recaptcha-response');
|
||||
try {
|
||||
// recaptcha-v3.js 에 recaptchaV3Exec(action) 있다고 가정
|
||||
const token = await window.recaptchaV3Exec('login');
|
||||
if (hidden) hidden.value = token || '';
|
||||
} catch (err) {
|
||||
if (hidden) hidden.value = '';
|
||||
}
|
||||
@endif
|
||||
|
||||
form.submit(); // 실제 전송
|
||||
} finally {
|
||||
// submit() 호출 후 페이지 이동하므로 보통 의미 없지만
|
||||
// 혹시 ajax로 바꾸면 필요함
|
||||
// if (btn) btn.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const msg = @json($errors->first('login'));
|
||||
if (msg) {
|
||||
if (typeof showMsg === 'function') {
|
||||
await showMsg(msg, { type: 'alert', title: '로그인 실패' });
|
||||
} else if (typeof showAlert === 'function') {
|
||||
await showAlert(msg, '로그인 실패');
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@endpush
|
||||
|
||||
@ -1,36 +1,562 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '회원정보 입력 | PIN FOR YOU')
|
||||
@section('meta_description', 'PIN FOR YOU 회원정보 입력 단계입니다.')
|
||||
@section('canonical', url('/auth/register/profile'))
|
||||
|
||||
@section('content')
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">회원정보 입력</h2>
|
||||
<p class="auth-desc">계정 생성을 위해 필요한 정보를 입력해 주세요.</p>
|
||||
@section('h1', '회원정보 입력')
|
||||
@section('desc', '본인인증 정보는 수정할 수 없으며, 아이디/비밀번호 등 가입 정보를 입력해 주세요.')
|
||||
@section('card_aria', '회원가입 Step2 - 가입정보 입력')
|
||||
@section('show_cs_links', true)
|
||||
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.submit') }}">
|
||||
@section('auth_content')
|
||||
<form class="auth-form" id="regProfileForm" method="POST" action="{{ route('web.auth.register.profile.submit') }}" novalidate>
|
||||
@csrf
|
||||
|
||||
{{-- 진행 단계 --}}
|
||||
<div class="terms-wrap">
|
||||
<div class="terms-steps" aria-label="진행 단계">
|
||||
<div class="terms-step">가입정보확인</div>
|
||||
<div class="terms-step">약관/인증 확인</div>
|
||||
<div class="terms-step is-active">가입정보입력</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- PASS 인증 정보(수정 불가) --}}
|
||||
<div class="auth-field">
|
||||
<label for="login_id">아이디</label>
|
||||
<input id="login_id" name="login_id" type="text" autocomplete="username" required>
|
||||
<div style="font-weight:800; margin-bottom:8px; opacity:.9;">본인인증 정보</div>
|
||||
|
||||
@php
|
||||
$pv = (array) session('register.pass_payload', []);
|
||||
|
||||
$pvName = $pv['NAME'] ?? '-';
|
||||
$pvBirth = $pv['DOB'] ?? null; // YYYYMMDD
|
||||
$pvSex = $pv['SEX'] ?? null; // 1/2 등
|
||||
$pvNation = $pv['FOREIGNER'] ?? null; // 0/1
|
||||
$pvTelco = $pv['CARRIER'] ?? (session('signup.carrier') ?? '-'); // SKT/KT/LGU+ 또는 코드
|
||||
$pvPhone = $pv['PHONE'] ?? (session('signup.phone') ?? '-');
|
||||
|
||||
// 표시용 변환(성별/내외국인)
|
||||
$pvSexText = match((string)$pvSex) {
|
||||
'1' => '남',
|
||||
'2' => '여',
|
||||
default => ($pvSex ?? '-'),
|
||||
};
|
||||
|
||||
$pvNationText = match((string)$pvNation) {
|
||||
'0' => '내국인',
|
||||
'1' => '외국인',
|
||||
default => ($pvNation ?? '-'),
|
||||
};
|
||||
|
||||
// 생년월일 포맷
|
||||
$pvBirthText = '-';
|
||||
if (is_string($pvBirth) && strlen($pvBirth) >= 8) {
|
||||
$pvBirthText = substr($pvBirth,0,4).'년 '.substr($pvBirth,4,2).'월 '.substr($pvBirth,6,2).'일';
|
||||
} elseif (!empty($pvBirth)) {
|
||||
$pvBirthText = (string)$pvBirth;
|
||||
}
|
||||
|
||||
// 휴대폰 포맷(표시용)
|
||||
$pvPhoneText = (string)$pvPhone;
|
||||
if (is_string($pvPhone) && preg_match('/^\d{10,11}$/', $pvPhone)) {
|
||||
$pvPhoneText = substr($pvPhone,0,3).'-'.substr($pvPhone,3,4).'-'.substr($pvPhone,7);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="profile-grid">
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">성명</div>
|
||||
<div class="profile-value">{{ $pvName}}</div>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">생년월일</div>
|
||||
<div class="profile-value">{{ $pvBirthText }}</div>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">성별</div>
|
||||
<div class="profile-value">{{ $pvSexText }}</div>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">내외국인</div>
|
||||
<div class="profile-value">{{ $pvNationText}}</div>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">통신사</div>
|
||||
<div class="profile-value">{{ $pvTelco }}</div>
|
||||
</div>
|
||||
<div class="profile-item">
|
||||
<div class="profile-label">휴대전화번호</div>
|
||||
<div class="profile-value">{{ $pvPhoneText}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-help" style="display:block;opacity:.7;margin-top:8px">
|
||||
위 정보가 다르면 인증을 중단하고 처음부터 다시 진행해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 아이디(이메일) --}}
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="login_id">아이디(이메일)</label>
|
||||
<input class="auth-input" id="login_id" name="login_id" type="email"
|
||||
placeholder="example@domain.com"
|
||||
autocomplete="username"
|
||||
maxlength="60"
|
||||
required>
|
||||
<div class="auth-help" id="login_id_help" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
{{-- 비밀번호(6자리 숫자) --}}
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="password">비밀번호</label>
|
||||
<input class="auth-input" id="password" name="password" type="password"
|
||||
placeholder="영문+숫자+특수문자 포함 8자 이상"
|
||||
minlength="8" maxlength="20"
|
||||
autocomplete="new-password"
|
||||
required>
|
||||
<div class="auth-help" id="password_help" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="password">비밀번호(6자리 숫자)</label>
|
||||
<input id="password" name="password" type="password" inputmode="numeric" pattern="\d{6}" maxlength="6" required>
|
||||
<label class="auth-label" for="password_confirmation">비밀번호 확인</label>
|
||||
<input class="auth-input" id="password_confirmation" name="password_confirmation" type="password"
|
||||
placeholder="비밀번호 확인"
|
||||
minlength="8" maxlength="20"
|
||||
autocomplete="new-password"
|
||||
required>
|
||||
<div class="auth-help" id="password_confirmation_help" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="password_confirmation">비밀번호 확인</label>
|
||||
<input id="password_confirmation" name="password_confirmation" type="password" inputmode="numeric" pattern="\d{6}" maxlength="6" required>
|
||||
|
||||
{{-- 2차 비밀번호(4자리 숫자) --}}
|
||||
<div class="auth-row" style="gap:10px">
|
||||
<div class="auth-field" style="flex:1">
|
||||
<label class="auth-label" for="pin2">2차 비밀번호(숫자 4자리)</label>
|
||||
<input class="auth-input" id="pin2" name="pin2" type="password"
|
||||
inputmode="numeric" pattern="\d{4}" maxlength="4"
|
||||
placeholder="숫자 4자리"
|
||||
required>
|
||||
<div class="auth-help" id="pin2_help" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="auth-field" style="flex:1">
|
||||
<label class="auth-label" for="pin2_confirmation">2차 비밀번호 확인</label>
|
||||
<input class="auth-input" id="pin2_confirmation" name="pin2_confirmation" type="password"
|
||||
inputmode="numeric" pattern="\d{4}" maxlength="4"
|
||||
placeholder="숫자 4자리 확인"
|
||||
required>
|
||||
<div class="auth-help" id="pin2_confirmation_help" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="email">이메일</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" id="profile_submit_btn" type="submit" >가입 완료</button>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.register') }}">처음으로</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-btn auth-btn--primary">가입 완료</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.terms-wrap{display:flex;flex-direction:column;gap:14px}
|
||||
.terms-steps{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
}
|
||||
|
||||
.terms-step{
|
||||
flex:1;
|
||||
text-align:center;
|
||||
padding:10px 12px;
|
||||
border-radius:999px; /* ✅ badge */
|
||||
font-size:12.5px;
|
||||
font-weight:800;
|
||||
letter-spacing:-0.2px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 50px;
|
||||
|
||||
/* 기본(비활성) */
|
||||
color: rgba(136, 105, 105, 0.78);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background:rgba(255,255,255,.06);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.10),
|
||||
0 10px 22px rgba(0,0,0,.14);
|
||||
|
||||
opacity:.78;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
/* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */
|
||||
.terms-step::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:linear-gradient(135deg,
|
||||
rgba(47,107,255,.18),
|
||||
rgba(74,163,255,.14),
|
||||
rgba(47,210,255,.12)
|
||||
);
|
||||
opacity:.55;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
/* ✅ 활성: 선명한 그라데이션 */
|
||||
.terms-step.is-active{
|
||||
opacity:1;
|
||||
color:#fff;
|
||||
border-color:rgba(120,180,255,.50);
|
||||
background:linear-gradient(135deg, #2f6bff 0%, #4aa3ff 55%, #2fd2ff 100%);
|
||||
box-shadow:
|
||||
0 18px 38px rgba(47,107,255,.28),
|
||||
0 10px 20px rgba(47,210,255,.14);
|
||||
}
|
||||
|
||||
/* shine 효과 (활성만) */
|
||||
.terms-step.is-active::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:-40%;
|
||||
left:-45%;
|
||||
width:55%;
|
||||
height:180%;
|
||||
transform:rotate(18deg);
|
||||
background:linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
||||
opacity:.55;
|
||||
animation: termsStepShine 2.8s ease-in-out infinite;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
@keyframes termsStepShine{
|
||||
0% { transform: translateX(-30%) rotate(18deg); opacity:.20; }
|
||||
45% { opacity:.70; }
|
||||
100% { transform: translateX(240%) rotate(18deg); opacity:.20; }
|
||||
}
|
||||
|
||||
/* 모바일에서 글자 길어서 깨질 때 대비 */
|
||||
@media (max-width: 420px){
|
||||
.terms-step{
|
||||
font-size:12px;
|
||||
padding:10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-grid{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap:10px;
|
||||
}
|
||||
@media (max-width: 420px){
|
||||
.profile-grid{ grid-template-columns: 1fr; }
|
||||
}
|
||||
.profile-item{
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background:rgba(255,255,255,.04);
|
||||
border-radius:14px;
|
||||
padding:10px 12px;
|
||||
}
|
||||
.profile-label{font-size:12px; opacity:.7; margin-bottom:6px; font-weight:700}
|
||||
.profile-value{font-size:13px; font-weight:900; letter-spacing:.2px}
|
||||
.auth-help.is-ok{display:block !important; color: rgba(120,255,180,.9); }
|
||||
.auth-help.is-bad{display:block !important; color: rgba(255,120,120,.95); }
|
||||
.auth-help { white-space: pre-line; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.getElementById('regProfileForm');
|
||||
const btn = document.getElementById('profile_submit_btn');
|
||||
|
||||
const loginId = document.getElementById('login_id');
|
||||
const pw1 = document.getElementById('password');
|
||||
const pw2 = document.getElementById('password_confirmation');
|
||||
const p2 = document.getElementById('pin2');
|
||||
const p2c = document.getElementById('pin2_confirmation');
|
||||
|
||||
const loginHelp = document.getElementById('login_id_help'); // ✅ 그대로 사용 (blur에서만 갱신)
|
||||
const pwRuleHelp = document.getElementById('password_help');
|
||||
const pwMatchHelp = document.getElementById('password_confirmation_help');
|
||||
const p2MatchHelp = document.getElementById('pin2_confirmation_help');
|
||||
|
||||
const PW_MIN = 8;
|
||||
const PW_MAX = 20;
|
||||
|
||||
const ALLOWED_SPECIALS_TEXT = `! @ # $ % ^ & * ( ) - _ = + [ ] { } ; : ' " , . < > ? | ~ \``;
|
||||
const ALLOWED_SPECIALS_REGEX = `!@#$%^&*()\\-_=+\\[\\]{};:'",.<>\\/\\?\\\\|\\\`~`;
|
||||
|
||||
let lastCheckedLogin = '';
|
||||
let duplicateOk = false;
|
||||
let isSubmitting = false;
|
||||
|
||||
function showAlert(message, title = '안내') {
|
||||
// ✅ 네가 쓰는 함수명에 맞춰 호출 (없으면 alert fallback)
|
||||
if (typeof window.showAlert === 'function') return window.showAlert(message, title);
|
||||
if (typeof window.showMsg === 'function') return window.showMsg(message, { type:'alert', title });
|
||||
alert(`${title}\n\n${message}`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function setHelp(el, msg, ok) {
|
||||
if (!el) return;
|
||||
el.classList.remove('is-ok', 'is-bad');
|
||||
el.style.display = msg ? 'block' : 'none';
|
||||
el.textContent = msg || '';
|
||||
if (msg) el.classList.add(ok ? 'is-ok' : 'is-bad');
|
||||
}
|
||||
|
||||
function emailFormatOk(v){
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v || '');
|
||||
}
|
||||
|
||||
function onlyDigitsInput(e, maxLen) {
|
||||
const allow = ['Backspace','Delete','ArrowLeft','ArrowRight','Tab','Home','End'];
|
||||
if (allow.includes(e.key)) return;
|
||||
if (e.ctrlKey || e.metaKey) return;
|
||||
if (!/^\d$/.test(e.key)) e.preventDefault();
|
||||
if (maxLen && e.target.value && e.target.value.length >= maxLen) e.preventDefault();
|
||||
}
|
||||
|
||||
[p2, p2c].forEach(el => {
|
||||
if (!el) return;
|
||||
el.addEventListener('keydown', (e) => onlyDigitsInput(e, 4));
|
||||
el.addEventListener('input', () => { el.value = (el.value||'').replace(/\D/g,'').slice(0,4); });
|
||||
});
|
||||
|
||||
function pwHasDigit(pw){ return /\d/.test(pw || ''); }
|
||||
function pwHasLetter(pw){ return /[A-Za-z]/.test(pw || ''); }
|
||||
function pwHasAllowedSpecial(pw){
|
||||
const re = new RegExp(`[${ALLOWED_SPECIALS_REGEX}]`);
|
||||
return re.test(pw || '');
|
||||
}
|
||||
function pwHasDisallowedChar(pw){
|
||||
const re = new RegExp(`^[A-Za-z0-9${ALLOWED_SPECIALS_REGEX}]*$`);
|
||||
return !re.test(pw || '');
|
||||
}
|
||||
|
||||
function passwordRuleMessage(pw){
|
||||
const s = pw || '';
|
||||
if (s.length < PW_MIN) return `비밀번호는 ${PW_MIN}자리 이상 입력해 주세요.`;
|
||||
if (s.length > PW_MAX) return `비밀번호는 ${PW_MAX}자리를 초과할 수 없습니다.`;
|
||||
if (!pwHasLetter(s)) return `비밀번호에 영문(A-Z, a-z)을 포함해 주세요.`;
|
||||
if (!pwHasDigit(s)) return `비밀번호에 숫자를 포함해 주세요.`;
|
||||
if (!pwHasAllowedSpecial(s)) {
|
||||
// ✅ 줄바꿈: CSS(pre-line) 적용돼 있으니 \n 사용
|
||||
return `특수문자를 입력해 주세요.\n(허용: ${ALLOWED_SPECIALS_TEXT})`;
|
||||
}
|
||||
if (pwHasDisallowedChar(s)) {
|
||||
return `허용되지 않은 문자가 포함되어 있습니다.\n(허용: 영문/숫자/지정 특수문자)`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function validatePasswordFields(showHelp = true){
|
||||
let ok = true;
|
||||
|
||||
const a = (pw1.value || '');
|
||||
const b = (pw2.value || '');
|
||||
|
||||
const msg = passwordRuleMessage(a);
|
||||
if (msg) {
|
||||
ok = false;
|
||||
if (showHelp) setHelp(pwRuleHelp, msg, false);
|
||||
} else {
|
||||
if (showHelp && a) setHelp(pwRuleHelp, '비밀번호 조건 충족', true);
|
||||
if (showHelp && !a) setHelp(pwRuleHelp, '', true);
|
||||
}
|
||||
|
||||
if (b) {
|
||||
if (a !== b) {
|
||||
ok = false;
|
||||
if (showHelp) setHelp(pwMatchHelp, '비밀번호가 일치하지 않습니다.', false);
|
||||
} else if (!msg) {
|
||||
if (showHelp) setHelp(pwMatchHelp, '비밀번호 확인 완료', true);
|
||||
} else {
|
||||
ok = false;
|
||||
if (showHelp) setHelp(pwMatchHelp, '', true);
|
||||
}
|
||||
} else {
|
||||
if (showHelp) setHelp(pwMatchHelp, '', true);
|
||||
if (a) ok = false; // 확인값 없으면 통과 불가
|
||||
}
|
||||
|
||||
const p2v = (p2.value || '');
|
||||
const p2cv = (p2c.value || '');
|
||||
|
||||
if (p2v.length !== 4) ok = false;
|
||||
if (p2cv.length !== 4) ok = false;
|
||||
|
||||
if (p2cv) {
|
||||
if (p2v !== p2cv) {
|
||||
ok = false;
|
||||
if (showHelp) setHelp(p2MatchHelp, '2차 비밀번호가 일치하지 않습니다.', false);
|
||||
} else if (p2v.length === 4) {
|
||||
if (showHelp) setHelp(p2MatchHelp, '2차 비밀번호 확인 완료', true);
|
||||
}
|
||||
} else {
|
||||
if (showHelp) setHelp(p2MatchHelp, '', true);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
async function postJson(url, payload){
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}",
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload || {})
|
||||
});
|
||||
|
||||
const raw = await res.text();
|
||||
let data = {};
|
||||
try { data = JSON.parse(raw || "{}"); } catch(e) {}
|
||||
return { res, data };
|
||||
}
|
||||
|
||||
async function ensureEmailDuplicateChecked(){
|
||||
const v = (loginId.value || '').trim();
|
||||
|
||||
if (!v) {
|
||||
await showAlert('이메일을 입력해 주세요.', '안내');
|
||||
loginId.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!emailFormatOk(v)) {
|
||||
await showAlert('이메일 형식으로 입력해 주세요.', '안내');
|
||||
loginId.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 이미 같은 값에 대해 통과한 중복체크가 있으면 skip
|
||||
if (duplicateOk && lastCheckedLogin === v) return true;
|
||||
|
||||
// ✅ 중복체크 시도
|
||||
try {
|
||||
// loginHelp는 “그대로 표시” 원칙이라 여기서 메시지 바꾸지 않음
|
||||
const { res, data } = await postJson("{{ route('web.auth.register.check_login_id') }}", { login_id: v });
|
||||
|
||||
if (res.ok && data && data.ok === true) {
|
||||
duplicateOk = true;
|
||||
lastCheckedLogin = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
await showAlert(data.message || '이미 사용 중인 이메일(아이디)입니다.', '안내');
|
||||
loginId.focus();
|
||||
return false;
|
||||
|
||||
} catch (e) {
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
await showAlert('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', '오류');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ loginHelp는 그대로: blur에서만 업데이트 (시각적 도움용)
|
||||
loginId.addEventListener('blur', async () => {
|
||||
const v = (loginId.value || '').trim();
|
||||
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
|
||||
if (!v) { setHelp(loginHelp, '아이디(이메일)를 입력해 주세요.', false); return; }
|
||||
if (!emailFormatOk(v)) { setHelp(loginHelp, '이메일 형식으로 입력해 주세요.', false); return; }
|
||||
|
||||
setHelp(loginHelp, '중복 확인 중...', true);
|
||||
|
||||
try {
|
||||
const { res, data } = await postJson("{{ route('web.auth.register.check_login_id') }}", { login_id: v });
|
||||
if (res.ok && data && data.ok === true) {
|
||||
duplicateOk = true;
|
||||
lastCheckedLogin = v;
|
||||
setHelp(loginHelp, '사용 가능한 아이디입니다.', true);
|
||||
} else {
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
setHelp(loginHelp, data.message || '이미 사용 중인 아이디입니다.', false);
|
||||
}
|
||||
} catch (e) {
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
setHelp(loginHelp, '네트워크 오류로 중복 확인 실패', false);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ 사용자가 이메일을 수정하면 중복체크 결과 무효화 (loginHelp는 굳이 건드리지 않음)
|
||||
loginId.addEventListener('input', () => {
|
||||
duplicateOk = false;
|
||||
lastCheckedLogin = '';
|
||||
});
|
||||
|
||||
// 비밀번호 입력 중 도움말은 즉시 표시
|
||||
[pw1, pw2, p2, p2c].forEach(el => {
|
||||
if (!el) return;
|
||||
el.addEventListener('input', () => validatePasswordFields(true));
|
||||
});
|
||||
|
||||
// ✅ 가입 버튼 눌렀을 때 “무조건 반응” + 단계별 안내
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (isSubmitting) return;
|
||||
|
||||
isSubmitting = true;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
// 1) 이메일 체크 + (필요시) 중복체크
|
||||
const emailOk = await ensureEmailDuplicateChecked();
|
||||
if (!emailOk) return;
|
||||
|
||||
// 2) 비밀번호/확인/2차비번 체크
|
||||
const pwOk = validatePasswordFields(true);
|
||||
if (!pwOk) {
|
||||
// 상황별로 더 직관적 메시지
|
||||
const pwMsg = passwordRuleMessage(pw1.value || '');
|
||||
if (!pw1.value) {
|
||||
await showAlert('비밀번호를 입력해 주세요.', '안내'); pw1.focus(); return;
|
||||
}
|
||||
if (pwMsg) {
|
||||
await showAlert(pwMsg, '안내'); pw1.focus(); return;
|
||||
}
|
||||
if ((pw1.value || '') !== (pw2.value || '')) {
|
||||
await showAlert('비밀번호 확인이 일치하지 않습니다.', '안내'); pw2.focus(); return;
|
||||
}
|
||||
if ((p2.value || '').length !== 4) {
|
||||
await showAlert('2차 비밀번호(숫자 4자리)를 입력해 주세요.', '안내'); p2.focus(); return;
|
||||
}
|
||||
if ((p2.value || '') !== (p2c.value || '')) {
|
||||
await showAlert('2차 비밀번호 확인이 일치하지 않습니다.', '안내'); p2c.focus(); return;
|
||||
}
|
||||
|
||||
await showAlert('입력값을 확인해 주세요.', '안내');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 최종 제출
|
||||
form.submit();
|
||||
|
||||
} catch (err) {
|
||||
await showAlert('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', '오류');
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
// submit 성공이면 페이지 이동, 실패 시 버튼 복구
|
||||
setTimeout(() => { btn.disabled = false; }, 200);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기 도움말 정리
|
||||
validatePasswordFields(false);
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@endsection
|
||||
|
||||
@ -12,15 +12,13 @@
|
||||
@section('auth_content')
|
||||
|
||||
{{-- Step0 Hero Image + 안내문구 --}}
|
||||
|
||||
<img
|
||||
class="reg-step0-hero__img"
|
||||
src="{{ asset('assets/images/web/member/register.webp') }}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none';"
|
||||
style="padding-bottom:15px"
|
||||
/>
|
||||
<div class="terms-wrap">
|
||||
<div class="terms-steps" aria-label="진행 단계">
|
||||
<div class="terms-step is-active">가입정보확인</div>
|
||||
<div class="terms-step">약관/인증 확인</div>
|
||||
<div class="terms-step">가입정보입력</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="auth-form" id="regStep0Form" onsubmit="return false;">
|
||||
@ -29,6 +27,25 @@
|
||||
{{-- ✅ hidden input만 생성(토큰은 JS에서 발급 후 payload에 포함) --}}
|
||||
<x-recaptcha-v3 />
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_carrier">
|
||||
통신사 <small>선택</small>
|
||||
</label>
|
||||
|
||||
{{-- 서버 전송용 hidden --}}
|
||||
<input type="hidden" id="reg_carrier" name="carrier" value="">
|
||||
|
||||
<div id="reg_carrier_group" style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<button type="button" class="auth-btn auth-btn--ghost" data-carrier="01">SKT</button>
|
||||
<button type="button" class="auth-btn auth-btn--ghost" data-carrier="02">KT</button>
|
||||
<button type="button" class="auth-btn auth-btn--ghost" data-carrier="03">LG U+</button>
|
||||
{{-- <button type="button" class="auth-btn auth-btn--ghost" data-carrier="04">SKT(알뜰폰)</button>--}}
|
||||
{{-- <button type="button" class="auth-btn auth-btn--ghost" data-carrier="05">KT(알뜰폰)</button>--}}
|
||||
{{-- <button type="button" class="auth-btn auth-btn--ghost" data-carrier="06">LGU+(알뜰폰)</button>--}}
|
||||
</div>
|
||||
|
||||
<div class="auth-help" id="reg_carrier_help" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_phone">
|
||||
휴대폰 번호 <small>가입 여부 확인</small>
|
||||
@ -49,6 +66,93 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.terms-steps{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
}
|
||||
|
||||
.terms-step{
|
||||
flex:1;
|
||||
text-align:center;
|
||||
padding:10px 12px;
|
||||
border-radius:999px; /* ✅ badge */
|
||||
font-size:12.5px;
|
||||
font-weight:800;
|
||||
letter-spacing:-0.2px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 50px;
|
||||
|
||||
/* 기본(비활성) */
|
||||
color: rgba(136, 105, 105, 0.78);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background:rgba(255,255,255,.06);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.10),
|
||||
0 10px 22px rgba(0,0,0,.14);
|
||||
|
||||
opacity:.78;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
/* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */
|
||||
.terms-step::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:linear-gradient(135deg,
|
||||
rgba(47,107,255,.18),
|
||||
rgba(74,163,255,.14),
|
||||
rgba(47,210,255,.12)
|
||||
);
|
||||
opacity:.55;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
/* ✅ 활성: 선명한 그라데이션 */
|
||||
.terms-step.is-active{
|
||||
opacity:1;
|
||||
color:#fff;
|
||||
border-color:rgba(120,180,255,.50);
|
||||
background:linear-gradient(135deg, #2f6bff 0%, #4aa3ff 55%, #2fd2ff 100%);
|
||||
box-shadow:
|
||||
0 18px 38px rgba(47,107,255,.28),
|
||||
0 10px 20px rgba(47,210,255,.14);
|
||||
}
|
||||
|
||||
/* shine 효과 (활성만) */
|
||||
.terms-step.is-active::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:-40%;
|
||||
left:-45%;
|
||||
width:55%;
|
||||
height:180%;
|
||||
transform:rotate(18deg);
|
||||
background:linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
||||
opacity:.55;
|
||||
animation: termsStepShine 2.8s ease-in-out infinite;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
@keyframes termsStepShine{
|
||||
0% { transform: translateX(-30%) rotate(18deg); opacity:.20; }
|
||||
45% { opacity:.70; }
|
||||
100% { transform: translateX(240%) rotate(18deg); opacity:.20; }
|
||||
}
|
||||
|
||||
/* 모바일에서 글자 길어서 깨질 때 대비 */
|
||||
@media (max-width: 420px){
|
||||
.terms-step{
|
||||
font-size:12px;
|
||||
padding:10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}}
|
||||
@push('recaptcha')
|
||||
<script>window.__recaptchaSiteKey = @json(config('services.recaptcha.site_key'));</script>
|
||||
@ -63,6 +167,12 @@
|
||||
const help = document.getElementById('reg_phone_help');
|
||||
const btn = document.getElementById('reg_next_btn');
|
||||
|
||||
// ✅ 통신사
|
||||
const carrierHidden = document.getElementById('reg_carrier');
|
||||
const carrierGroup = document.getElementById('reg_carrier_group');
|
||||
|
||||
let selectedCarrier = '';
|
||||
|
||||
// 숫자만 남기고 010-0000-0000 형태로 포맷
|
||||
function formatPhone(value) {
|
||||
const digits = (value || '').replace(/\D/g, '').slice(0, 11);
|
||||
@ -76,6 +186,50 @@
|
||||
return (value || '').replace(/\D/g, '');
|
||||
}
|
||||
|
||||
// ✅ 통신사 버튼 레이아웃/동일 크기(기존 CSS 크게 안 건드리고 JS로 스타일만 주입)
|
||||
function applyCarrierButtonLayout() {
|
||||
if (!carrierGroup) return;
|
||||
|
||||
carrierGroup.style.display = 'grid';
|
||||
carrierGroup.style.gridTemplateColumns = 'repeat(3, 1fr)';
|
||||
carrierGroup.style.gap = '8px';
|
||||
carrierGroup.style.width = '100%';
|
||||
carrierGroup.style.justifyItems = 'stretch';
|
||||
carrierGroup.style.alignItems = 'stretch';
|
||||
|
||||
carrierGroup.querySelectorAll('button[data-carrier]').forEach(b => {
|
||||
b.style.width = '100%';
|
||||
b.style.height = '44px'; // 동일 세로
|
||||
b.style.padding = '0'; // 높이 고정에 방해되는 padding 제거
|
||||
b.style.display = 'inline-flex';
|
||||
b.style.alignItems = 'center';
|
||||
b.style.justifyContent = 'center';
|
||||
b.style.borderRadius = '12px'; // 기존 톤에 맞게(원하면 삭제)
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 통신사 선택 UI
|
||||
function bindCarrierButtons() {
|
||||
if (!carrierGroup || !carrierHidden) return;
|
||||
|
||||
carrierGroup.addEventListener('click', function (e) {
|
||||
const btnEl = e.target.closest('button[data-carrier]');
|
||||
if (!btnEl) return;
|
||||
|
||||
selectedCarrier = btnEl.getAttribute('data-carrier') || '';
|
||||
carrierHidden.value = selectedCarrier;
|
||||
|
||||
// 선택=primary, 나머지=ghost
|
||||
carrierGroup.querySelectorAll('button[data-carrier]').forEach(b => {
|
||||
b.classList.remove('auth-btn--primary');
|
||||
b.classList.add('auth-btn--ghost');
|
||||
});
|
||||
btnEl.classList.remove('auth-btn--ghost');
|
||||
btnEl.classList.add('auth-btn--primary');
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 휴대폰 입력 UX
|
||||
input.addEventListener('input', function () {
|
||||
const formatted = formatPhone(input.value);
|
||||
if (input.value !== formatted) input.value = formatted;
|
||||
@ -95,10 +249,23 @@
|
||||
input.value = formatPhone(digits);
|
||||
});
|
||||
|
||||
// 초기 적용
|
||||
applyCarrierButtonLayout();
|
||||
bindCarrierButtons();
|
||||
|
||||
// ✅ submit
|
||||
form.addEventListener('submit', async function () {
|
||||
clearMsg();
|
||||
// clearMsg()가 기존에 전역으로 있다면 유지
|
||||
if (typeof clearMsg === 'function') clearMsg();
|
||||
|
||||
// 통신사 선택 체크
|
||||
if (!selectedCarrier) {
|
||||
await showMsg('통신사를 선택하세요.', { type: 'alert', title: '입력오류' });
|
||||
return;
|
||||
}
|
||||
|
||||
const phoneDigits = toDigits(input.value);
|
||||
|
||||
if (!phoneDigits) {
|
||||
await showMsg('휴대폰 번호를 입력해 주세요.', { type:'alert', title:'입력오류' });
|
||||
input.focus();
|
||||
@ -125,19 +292,17 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
phone: phoneDigits,
|
||||
carrier: selectedCarrier,
|
||||
"g-recaptcha-response": token
|
||||
})
|
||||
});
|
||||
|
||||
// 422/403에서도 JSON 잘 파싱되게 안전 처리
|
||||
const raw = await res.text();
|
||||
let data = {};
|
||||
try { data = JSON.parse(raw || "{}"); } catch (e) { data = {}; }
|
||||
|
||||
// 공통 메시지
|
||||
const msg = data.message || '처리에 실패했습니다.';
|
||||
|
||||
// 1) 이미 가입(=confirm)
|
||||
if (data.reason === 'already_member') {
|
||||
await showMsg(msg, {
|
||||
type: 'confirm',
|
||||
@ -149,8 +314,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 성공(=confirm)
|
||||
if (data.ok === true) {
|
||||
|
||||
await showMsg(msg || '회원가입 가능한 전화번호 입니다.\n\n인증페이지로 이동합니다.', {
|
||||
type: 'confirm',
|
||||
title: '안내',
|
||||
@ -161,11 +326,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 나머지 전부 실패(=alert)
|
||||
await showMsg(msg, {
|
||||
type: 'alert',
|
||||
title: '안내',
|
||||
});
|
||||
await showMsg(msg, { type: 'alert', title: '안내' });
|
||||
|
||||
|
||||
|
||||
} catch (e) {
|
||||
await showMsg("네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", {
|
||||
@ -178,6 +341,7 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
||||
@section('auth_bottom')
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
<div class="terms-wrap">
|
||||
{{-- 진행 단계 --}}
|
||||
<div class="terms-steps" aria-label="진행 단계">
|
||||
<div class="terms-step is-active">약관 동의</div>
|
||||
<div class="terms-step">본인인증</div>
|
||||
<div class="terms-step">가입정보</div>
|
||||
<div class="terms-step">가입정보확인</div>
|
||||
<div class="terms-step is-active">약관/인증 확인</div>
|
||||
<div class="terms-step">가입정보입력</div>
|
||||
</div>
|
||||
|
||||
{{-- <form id="regTermsForm" class="terms-form" method="POST" action="{{ route('web.auth.register.terms.submit') }}" novalidate>--}}
|
||||
@ -453,11 +453,90 @@
|
||||
{{-- 페이지 전용 스타일(이쁘게) --}}
|
||||
<style>
|
||||
.terms-wrap{display:flex;flex-direction:column;gap:14px}
|
||||
.terms-steps{display:flex;gap:8px;align-items:center}
|
||||
.terms-step{flex:1;text-align:center;padding:10px 8px;border-radius:12px;
|
||||
border:1px solid rgba(255,255,255,.10);opacity:.65;font-size:13px}
|
||||
.terms-step.is-active{opacity:1;border-color:rgba(255,255,255,.18);
|
||||
background:rgba(255,255,255,.06);font-weight:700}
|
||||
.terms-steps{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
}
|
||||
|
||||
.terms-step{
|
||||
flex:1;
|
||||
text-align:center;
|
||||
padding:10px 12px;
|
||||
border-radius:999px; /* ✅ badge */
|
||||
font-size:12.5px;
|
||||
font-weight:800;
|
||||
letter-spacing:-0.2px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 50px;
|
||||
|
||||
/* 기본(비활성) */
|
||||
color: rgba(136, 105, 105, 0.78);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background:rgba(255,255,255,.06);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,.10),
|
||||
0 10px 22px rgba(0,0,0,.14);
|
||||
|
||||
opacity:.78;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
/* ✅ 비활성도 살짝 그라데이션 느낌(은은하게) */
|
||||
.terms-step::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background:linear-gradient(135deg,
|
||||
rgba(47,107,255,.18),
|
||||
rgba(74,163,255,.14),
|
||||
rgba(47,210,255,.12)
|
||||
);
|
||||
opacity:.55;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
/* ✅ 활성: 선명한 그라데이션 */
|
||||
.terms-step.is-active{
|
||||
opacity:1;
|
||||
color:#fff;
|
||||
border-color:rgba(120,180,255,.50);
|
||||
background:linear-gradient(135deg, #2f6bff 0%, #4aa3ff 55%, #2fd2ff 100%);
|
||||
box-shadow:
|
||||
0 18px 38px rgba(47,107,255,.28),
|
||||
0 10px 20px rgba(47,210,255,.14);
|
||||
}
|
||||
|
||||
/* shine 효과 (활성만) */
|
||||
.terms-step.is-active::after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:-40%;
|
||||
left:-45%;
|
||||
width:55%;
|
||||
height:180%;
|
||||
transform:rotate(18deg);
|
||||
background:linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
||||
opacity:.55;
|
||||
animation: termsStepShine 2.8s ease-in-out infinite;
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
@keyframes termsStepShine{
|
||||
0% { transform: translateX(-30%) rotate(18deg); opacity:.20; }
|
||||
45% { opacity:.70; }
|
||||
100% { transform: translateX(240%) rotate(18deg); opacity:.20; }
|
||||
}
|
||||
|
||||
/* 모바일에서 글자 길어서 깨질 때 대비 */
|
||||
@media (max-width: 420px){
|
||||
.terms-step{
|
||||
font-size:12px;
|
||||
padding:10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.terms-form{display:flex;flex-direction:column;gap:12px}
|
||||
|
||||
@ -644,16 +723,76 @@
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.id = popupName;
|
||||
|
||||
// ✅ step0(전화번호 입력 페이지) 이동 URL
|
||||
const backUrl = @json(route('web.auth.register'));
|
||||
|
||||
wrap.innerHTML = `
|
||||
<div style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:200000;background:#000;opacity:.55"></div>
|
||||
<div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:${w}px;height:${h}px;background:#fff;border-radius:10px;z-index:200001;overflow:hidden;">
|
||||
<iframe id="${popupName}_iframe" name="${popupName}_iframe" style="width:100%;height:100%;border:none"></iframe>
|
||||
</div>`;
|
||||
<div class="danal-modal-dim" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:200000;background:#000;opacity:.55"></div>
|
||||
|
||||
<div class="danal-modal-box" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:${w}px;height:${h}px;background:#fff;border-radius:10px;z-index:200001;overflow:hidden;">
|
||||
|
||||
<!-- ✅ 헤더 + 닫기버튼 -->
|
||||
<div style="height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;background:rgba(0,0,0,.06);border-bottom:1px solid rgba(0,0,0,.08);">
|
||||
<div style="font-weight:800;font-size:13px;color:#111;">PASS 본인인증</div>
|
||||
<button type="button" id="${popupName}_close"
|
||||
aria-label="인증창 닫기"
|
||||
style="width:34px;height:34px;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:#fff;cursor:pointer;font-size:18px;line-height:1;color:#111;">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<iframe id="${popupName}_iframe" name="${popupName}_iframe"
|
||||
style="width:100%;height:calc(100% - 44px);border:none"></iframe>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(wrap);
|
||||
|
||||
window.closeIframe = function () {
|
||||
// 실제 닫기(제거)만 하는 함수
|
||||
function removeModal() {
|
||||
const el = document.getElementById(popupName);
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
async function askCancelAndGo() {
|
||||
const msg = "인증을 중단합니다.\n처음부터 다시 진행 하시겠습니까?";
|
||||
|
||||
// ✅ 시스템 confirm (레이어/모달 z-index 영향을 안 받음)
|
||||
if (window.confirm(msg)) {
|
||||
// iframe 강제 종료(선택)
|
||||
const ifr = document.getElementById(popupName + '_iframe');
|
||||
if (ifr) ifr.src = 'about:blank';
|
||||
|
||||
// 모달 제거
|
||||
const el = document.getElementById(popupName);
|
||||
if (el) el.remove();
|
||||
|
||||
// Step0로 이동
|
||||
window.location.href = @json(route('web.auth.register'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 닫기 버튼 핸들러
|
||||
const closeBtn = wrap.querySelector('#' + popupName + '_close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', askCancelAndGo);
|
||||
}
|
||||
|
||||
// ✅ 바깥 클릭으로 닫히지 않게 유지(기존 정책)
|
||||
// wrap.querySelector('.danal-modal-dim').addEventListener('click', (e)=>e.preventDefault());
|
||||
|
||||
// ✅ ESC 키로도 “중단” 처리 (선택)
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') askCancelAndGo();
|
||||
};
|
||||
window.addEventListener('keydown', escHandler);
|
||||
|
||||
// 외부에서 호출 가능하게 유지
|
||||
window.closeIframe = function () {
|
||||
window.removeEventListener('keydown', escHandler);
|
||||
removeModal();
|
||||
};
|
||||
|
||||
return popupName + '_iframe';
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
@php
|
||||
$sess = session('_sess', []);
|
||||
$isLogin = !empty($sess['_login_']);
|
||||
$name = $sess['_mname'] ?? '';
|
||||
$email = $sess['_mid'] ?? '';
|
||||
$display = $name !== '' ? $name : ($email !== '' ? $email : '내 계정');
|
||||
@endphp
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container" style="height: 100%; display: flex; align-items: center; justify-content: space-between;">
|
||||
|
||||
@ -33,8 +41,55 @@
|
||||
|
||||
<!-- Right: Auth Buttons -->
|
||||
<div class="auth-buttons flex items-center gap-2">
|
||||
@if($isLogin)
|
||||
|
||||
{{-- CTA: 핵심 행동 1개 --}}
|
||||
<a href="/shop" class="btn btn-primary" style="padding: 8px 18px;">
|
||||
상품권 구매
|
||||
</a>
|
||||
|
||||
{{-- 알림(지금은 뱃지 0으로만) --}}
|
||||
<a href="{{ route('web.mypage.qna.index') }}" class="btn btn-ghost" aria-label="알림">
|
||||
🔔
|
||||
{{-- <span class="badge">3</span> --}}
|
||||
</a>
|
||||
|
||||
{{-- 프로필 드롭다운 --}}
|
||||
<div class="relative group header-profile">
|
||||
<button type="button" class="btn btn-ghost">
|
||||
<span class="truncate max-w-[120px]">{{ $display }}</span>
|
||||
<span class="ml-1">▾</span>
|
||||
</button>
|
||||
|
||||
<div class="absolute right-0 mt-2 w-48 hidden group-hover:block profile-dropdown">
|
||||
<div class="rounded-xl border bg-white shadow-lg overflow-hidden profile-card">
|
||||
<div class="px-4 py-3 text-xs text-gray-500 profile-meta">
|
||||
{{ $email }}
|
||||
</div>
|
||||
<div class="border-t profile-sep"></div>
|
||||
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.mypage.info.index') }}">내 정보</a>
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.mypage.usage.index') }}">이용내역</a>
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.cs.qna.index') }}">1:1 문의</a>
|
||||
|
||||
<div class="border-t profile-sep"></div>
|
||||
|
||||
{{-- 로그아웃: POST 권장(라우트는 네가 만들거나 기존거 사용) --}}
|
||||
<form action="{{ route('web.auth.logout') }}" method="post">
|
||||
@csrf
|
||||
<button type="submit" class="w-full text-left px-4 py-2 hover:bg-gray-50 logout-btn">
|
||||
로그아웃
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<a href="/login" class="btn btn-ghost">로그인</a>
|
||||
<a href="/register" class="btn btn-primary" style="padding: 8px 20px;">회원가입</a>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
<div class="support-banner-cta">바로가기 →</div>
|
||||
</a>
|
||||
|
||||
<a href="/guide" class="support-banner support-banner--guide" aria-label="이용안내">
|
||||
<a href="/cs/guide" class="support-banner support-banner--guide" aria-label="이용안내">
|
||||
<div class="support-banner-title">이용안내</div>
|
||||
<div class="support-banner-desc">구매 절차 · 결제 · 발송 흐름 한눈에</div>
|
||||
<div class="support-banner-cta">바로가기 →</div>
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
$sections = config('web.drawer_sections', []);
|
||||
$isLoggedIn = auth()->check();
|
||||
$user = auth()->user();
|
||||
|
||||
$sess = session('_sess', []);
|
||||
$isLogin = !empty($sess['_login_']);
|
||||
$name = $sess['_mname'] ?? '';
|
||||
$email = $sess['_mid'] ?? '';
|
||||
$display = $name !== '' ? $name : ($email !== '' ? $email : '내 계정');
|
||||
@endphp
|
||||
|
||||
{{-- Overlay --}}
|
||||
@ -15,34 +21,66 @@
|
||||
</div>
|
||||
|
||||
{{-- ✅ 트렌디한 “프로필 카드”: 로그인 전/후 UI 분기 --}}
|
||||
<div class="m-usercard">
|
||||
@if($isLoggedIn)
|
||||
<div class="m-usercard__row">
|
||||
<div class="m-avatar" aria-hidden="true">{{ mb_substr($user->name ?? 'U', 0, 1) }}</div>
|
||||
<div class="m-usercard__info">
|
||||
<div class="m-usercard__name">{{ $user->name ?? '회원' }}</div>
|
||||
<div class="m-usercard__meta">{{ $user->email ?? '' }}</div>
|
||||
<div class="m-usercard2 {{ $isLogin ? 'is-login' : 'is-guest' }}">
|
||||
{{-- <div class="m-usercard2__head">--}}
|
||||
{{-- <div class="m-usercard2__badge">--}}
|
||||
{{-- {{ $isLogin ? 'MY' : 'GUEST' }}--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="m-usercard2__title">--}}
|
||||
{{-- {{ $isLogin ? '내 계정' : '로그인이 필요해요' }}--}}
|
||||
{{-- <div class="m-usercard2__sub">--}}
|
||||
{{-- {{ $isLogin ? '내역/설정은 여기서 바로' : '내역 확인 · 1:1문의는 로그인 후 이용 가능' }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($isLogin)--}}
|
||||
{{-- <a class="m-usercard2__icon" href="{{ route('web.mypage.info.index') }}" aria-label="내 정보">--}}
|
||||
{{-- ⚙️--}}
|
||||
{{-- </a>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
<div class="m-usercard2__body">
|
||||
@if($isLogin)
|
||||
<div class="m-usercard2__profile">
|
||||
<div class="m-usercard2__avatar" aria-hidden="true">
|
||||
{{ mb_substr($user->name ?? 'U', 0, 1) }}
|
||||
</div>
|
||||
|
||||
<div class="m-usercard2__info">
|
||||
<div class="m-usercard2__name">{{ $name }}</div>
|
||||
<div class="m-usercard2__meta">{{ $email }}</div>
|
||||
</div>
|
||||
<div class="m-usercard__actions">
|
||||
<a class="m-pill" href="{{ route('web.mypage.info.index') }}">내 정보</a>
|
||||
{{-- <a class="m-pill m-pill--ghost" href="{{ route('web.auth.logout') }}">로그아웃</a> --}}
|
||||
</div>
|
||||
@else
|
||||
<div class="m-usercard__row">
|
||||
<div class="m-avatar m-avatar--ghost" aria-hidden="true">🔒</div>
|
||||
<div class="m-usercard__info">
|
||||
<div class="m-usercard__name">로그인이 필요해요</div>
|
||||
<div class="m-usercard__meta">내역 확인/1:1문의는 로그인 후 이용 가능</div>
|
||||
<div class="m-usercard2__profile">
|
||||
<div class="m-usercard2__avatar m-usercard2__avatar--ghost" aria-hidden="true">🔒</div>
|
||||
|
||||
<div class="m-usercard2__info">
|
||||
<div class="m-usercard2__name">비회원</div>
|
||||
<div class="m-usercard2__meta">로그인하면 할인구매/거래내역을 바로 확인할 수 있어요.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-usercard__actions">
|
||||
<a class="m-pill" href="{{ route('web.auth.login') }}">로그인</a>
|
||||
<a class="m-pill m-pill--ghost" href="{{ route('web.auth.register') }}">회원가입</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="m-usercard2__actions">
|
||||
@if($isLogin)
|
||||
<a class="m-btn2" href="{{ route('web.mypage.info.index') }}">내 정보</a>
|
||||
<a class="m-btn2 m-btn2--ghost" href="{{ route('web.mypage.usage.index') }}">이용내역</a>
|
||||
|
||||
<form action="{{ route('web.auth.logout') }}" method="post" class="m-usercard2__logout">
|
||||
@csrf
|
||||
<button type="submit" class="m-btn2 m-btn2--text">로그아웃</button>
|
||||
</form>
|
||||
@else
|
||||
<a class="m-btn2" href="{{ route('web.auth.login') }}">로그인</a>
|
||||
<a class="m-btn2 m-btn2--ghost" href="{{ route('web.auth.register') }}">회원가입</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ✅ 메뉴 섹션(주메뉴/CS/정책/마이페이지) --}}
|
||||
<div class="mobile-drawer__body">
|
||||
@foreach($sections as $secKey => $sec)
|
||||
@ -64,21 +102,6 @@
|
||||
})->values()->all();
|
||||
@endphp
|
||||
|
||||
{{-- 마이페이지는 “로그인 전”에 섹션 자체를 살짝 줄여 보여도 됨 --}}
|
||||
@if($secKey === 'mypage' && !$isLoggedIn)
|
||||
{{-- 로그인 전에는 마이페이지 섹션은 보여주되, 안내 문구를 둠 --}}
|
||||
<section class="m-navsec">
|
||||
<div class="m-navsec__title">{{ $title }}</div>
|
||||
<div class="m-navsec__hint">로그인 후 이용내역/교환내역을 확인할 수 있어요.</div>
|
||||
|
||||
<div class="m-grid">
|
||||
<a class="m-pill" href="{{ route('web.auth.login') }}">로그인</a>
|
||||
<a class="m-pill m-pill--ghost" href="{{ route('web.auth.register') }}">회원가입</a>
|
||||
</div>
|
||||
</section>
|
||||
@continue
|
||||
@endif
|
||||
|
||||
<section class="m-navsec">
|
||||
<div class="m-navsec__title">{{ $title }}</div>
|
||||
<div class="m-grid">
|
||||
|
||||
@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Web\Auth\FindIdController;
|
||||
use App\Http\Controllers\Web\Auth\FindPasswordController;
|
||||
use App\Http\Controllers\Web\Auth\RegisterController;
|
||||
use App\Http\Controllers\Web\Auth\LoginController;
|
||||
use App\Http\Controllers\Web\Cs\NoticeController;
|
||||
|
||||
|
||||
@ -20,7 +21,7 @@ Route::prefix('cs')->name('web.cs.')->group(function () {
|
||||
Route::get('/notice/{seq}', [NoticeController::class, 'show'])->whereNumber('seq')->name('notice.show');
|
||||
Route::view('faq', 'web.cs.faq.index')->name('faq.index');
|
||||
Route::view('kakao', 'web.cs.kakao.index')->name('kakao.index');
|
||||
Route::view('qna', 'web.cs.qna.index')->name('qna.index');
|
||||
Route::view('qna', 'web.cs.qna.index')->name('qna.index')->middleware('legacy.auth');
|
||||
Route::view('guide', 'web.cs.guide.index')->name('guide.index');
|
||||
});
|
||||
|
||||
@ -29,18 +30,22 @@ Route::prefix('cs')->name('web.cs.')->group(function () {
|
||||
| My Page
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('mypage')->name('web.mypage.')->group(function () {
|
||||
// 전체 로그인필요
|
||||
Route::prefix('mypage')->name('web.mypage.')
|
||||
->middleware('legacy.auth')
|
||||
->group(function () {
|
||||
Route::view('info', 'web.mypage.info.index')->name('info.index');
|
||||
Route::view('usage', 'web.mypage.usage.index')->name('usage.index');
|
||||
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||
Route::view('qna', 'web.mypage.qna.index')->name('qna.index');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Policy
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
//로그인 후 접근하면 안됨
|
||||
Route::prefix('policy')->name('web.policy.')->group(function () {
|
||||
Route::view('/', 'web.policy.index')->name('index');
|
||||
Route::view('privacy', 'web.policy.privacy.index')->name('privacy.index');
|
||||
@ -53,42 +58,55 @@ Route::prefix('policy')->name('web.policy.')->group(function () {
|
||||
| Auth
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
Route::prefix('auth')->name('web.auth.')->group(function () {
|
||||
|
||||
// 정적 페이지
|
||||
Route::view('login', 'web.auth.login')->name('login');
|
||||
// 게스트만 접근 (로그인한 사람은 못 들어옴)
|
||||
Route::middleware('legacy.guest')->group(function () {
|
||||
// 로그인
|
||||
Route::get('login', [LoginController::class, 'show'])->name('login');
|
||||
Route::post('login', [LoginController::class, 'prc'])->name('login.prc');
|
||||
Route::get('login/dormancy-prc', [LoginController::class, 'dormancyPrc'])->name('login.dormancy_prc');
|
||||
|
||||
// 회원가입 Step0
|
||||
Route::get('register', [RegisterController::class, 'showStep0'])->name('register');
|
||||
Route::post('register/phone-check', [RegisterController::class, 'postPhoneCheck'])->name('register.phone_check');
|
||||
|
||||
// Step1(약관) - 지금은 임시 화면
|
||||
// Step1(약관)
|
||||
Route::get('register/terms', [RegisterController::class, 'showTerms'])->name('register.terms');
|
||||
Route::post('register/terms', [RegisterController::class, 'termsSubmit'])->name('register.terms.submit');
|
||||
|
||||
// Step2 다날 인증 시작 화면
|
||||
Route::post('register/danal/start', [RegisterController::class, 'danalStart'])->name('register.danal.start'); // iframe에서 로드될 페이지
|
||||
Route::post('register/danal/result', [RegisterController::class, 'danalResult'])->name('register.danal.result'); // 다날 pass 결과 리턴
|
||||
// Step2 다날 인증
|
||||
Route::post('register/danal/start', [RegisterController::class, 'danalStart'])->name('register.danal.start');
|
||||
Route::post('register/danal/result', [RegisterController::class, 'danalResult'])->name('register.danal.result');
|
||||
|
||||
// Step3 회원정보 입력
|
||||
Route::get('register/profile', [RegisterController::class, 'showProfileForm'])->name('register.profile');
|
||||
Route::post('register/profile', [RegisterController::class, 'submitProfile'])->name('register.profile.submit');
|
||||
Route::post('register/check-login-id', [RegisterController::class, 'checkLoginId'])->name('register.check_login_id');
|
||||
|
||||
// 회원가입 디버그용
|
||||
Route::get('register/profile/debug', [RegisterController::class, 'debugProfile'])->name('register.profile.debug');
|
||||
Route::post('register/profile/debug/run', [RegisterController::class, 'debugProfileRun'])->name('register.profile.debug.run');
|
||||
|
||||
// 아이디 찾기 (컨트롤러)
|
||||
// 아이디 찾기
|
||||
Route::get('find-id', [FindIdController::class, 'show'])->name('find_id');
|
||||
Route::post('find-id/send-code',[FindIdController::class, 'sendCode'])->name('find_id.send_code');
|
||||
Route::post('find-id/verify', [FindIdController::class, 'verify'])->name('find_id.verify');
|
||||
Route::post('find-id/reset', [FindIdController::class, 'reset'])->name('find_id.reset');
|
||||
|
||||
// 비밀번호 찾기 (컨트롤러)
|
||||
// 비밀번호 찾기
|
||||
Route::get('find-password', [FindPasswordController::class, 'show'])->name('find_password');
|
||||
Route::post('find-password/send-code', [FindPasswordController::class, 'sendCode'])->name('find_password.send_code');
|
||||
Route::post('find-password/verify', [FindPasswordController::class, 'verify'])->name('find_password.verify');
|
||||
Route::post('find-password/reset', [FindPasswordController::class, 'reset'])->name('find_password.reset');
|
||||
Route::post('find-password/reset-session', [FindPasswordController::class, 'resetSession'])->name('find_password.reset_session');
|
||||
});
|
||||
});
|
||||
|
||||
Route::middleware('legacy.auth')->group(function () {
|
||||
Route::post('logout', [LoginController::class, 'logout'])->name('logout');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
@ -99,7 +117,17 @@ Route::prefix('auth')->name('web.auth.')->group(function () {
|
||||
Route::get('/login', fn() => redirect()->route('web.auth.login'))->name('web.login');
|
||||
Route::get('/register', fn() => redirect()->route('web.auth.register'))->name('web.signup');
|
||||
|
||||
// Dev Lab (로컬에서만 + 파일 존재할 때만 라우트 등록)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Dev Lab (로컬에서만 + 파일 존재할 때만 라우트 등록)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
if (app()->environment(['local', 'development', 'testing'])
|
||||
&& class_exists(\App\Http\Controllers\Dev\DevLabController::class)) {
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user