회원가입 작업 중
This commit is contained in:
parent
cd9c2bb1f7
commit
28ec93ac1f
@ -4,9 +4,11 @@ namespace App\Http\Controllers\Web\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Repositories\Member\MemberAuthRepository;
|
||||
use App\Services\Danal\DanalAuthtelService;
|
||||
use App\Rules\RecaptchaV3Rule;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
@ -43,30 +45,254 @@ class RegisterController extends Controller
|
||||
$ip4 = $request->ip() ?: '';
|
||||
$result = $repo->step0PhoneCheck((string)$request->input('phone'), $ip4);
|
||||
|
||||
if (!$result['ok']) {
|
||||
$status = ($result['reason'] ?? '') === 'blocked' ? 403 : 422;
|
||||
return response()->json(['ok' => false, 'message' => $result['message'] ?? '처리 실패'], $status);
|
||||
}
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$reason = $result['reason'] ?? 'error';
|
||||
|
||||
if (($result['reason'] ?? '') === 'already_member') {
|
||||
// blocked류만 403, 그 외는 422(원하면 already_member는 200으로 바꿔도 됨)
|
||||
$status = in_array($reason, ['blocked', 'blocked_ip'], true) ? 403 : 422;
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reason' => 'already_member',
|
||||
'redirect' => route('web.auth.find_id'),
|
||||
]);
|
||||
'ok' => false,
|
||||
'reason' => $reason,
|
||||
'message' => $result['message'] ?? '처리에 실패했습니다.',
|
||||
'redirect' => $result['redirect'] ?? null,
|
||||
], $status);
|
||||
}
|
||||
|
||||
$request->session()->put('signup.phone', $result['phone']);
|
||||
$request->session()->put('signup.step', 1);
|
||||
$request->session()->put('signup.ip4', $ip4);
|
||||
$request->session()->put('signup.ip4_c', $repo->ipToCClass($ip4));
|
||||
$request->session()->put('signup.checked_at', now()->toDateTimeString());
|
||||
$request->session()->put('signup.phone', $result['phone']); // 필요하면
|
||||
$request->session()->save();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reason' => 'ok',
|
||||
'redirect' => route('web.auth.register.terms'),
|
||||
'reason' => $result['reason'] ?? 'ok',
|
||||
'message' => $result['message'] ?? '',
|
||||
'redirect' => $result['redirect'] ?? null,
|
||||
'phone' => $result['phone'] ?? null,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function termsSubmit(Request $request)
|
||||
{
|
||||
$v = Validator::make($request->all(), [
|
||||
'agree_terms' => ['required', 'in:1'],
|
||||
'agree_privacy' => ['required', 'in:1'],
|
||||
'agree_age' => ['required', 'in:1'],
|
||||
'agree_marketing' => ['nullable', 'in:1'],
|
||||
], [
|
||||
'agree_terms.required' => '이용약관에 동의해 주세요.',
|
||||
'agree_privacy.required' => '개인정보 수집·이용에 동의해 주세요.',
|
||||
'agree_age.required' => '만 14세 이상만 가입할 수 있습니다.',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
// AJAX면 JSON으로
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $v->errors()->first(),
|
||||
'errors' => $v->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 일반 submit이면 기존처럼
|
||||
return back()->withErrors($v)->withInput();
|
||||
}
|
||||
|
||||
// 약관 동의값 저장(세션)
|
||||
$request->session()->put('register.terms', [
|
||||
'agree_terms' => true,
|
||||
'agree_privacy' => true,
|
||||
'agree_age' => true,
|
||||
'agree_marketing' => (bool)$request->input('agree_marketing'),
|
||||
'agreed_at' => now()->toDateTimeString(),
|
||||
'ip' => $request->ip(),
|
||||
'ua' => substr((string)$request->userAgent(), 0, 500),
|
||||
]);
|
||||
$request->session()->save();
|
||||
|
||||
// AJAX면: 다날 준비값 생성 후 popup 정보 반환
|
||||
if ($request->expectsJson()) {
|
||||
$danal = app(\App\Services\Danal\DanalAuthtelService::class)->prepare([
|
||||
'targetUrl' => route('web.auth.register.danal.result'),
|
||||
'backUrl' => route('web.auth.register.terms'),
|
||||
'cpTitle' => request()->getHost(),
|
||||
]);
|
||||
|
||||
if (!($danal['ok'] ?? false)) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $danal['message'] ?? '본인인증 준비에 실패했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
], 500);
|
||||
}
|
||||
|
||||
// 필요하면 트랜잭션 정보 세션 저장
|
||||
$request->session()->put('register.danal', [
|
||||
'txid' => $danal['txid'] ?? null,
|
||||
'created_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
$request->session()->save();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'reason' => 'danal_ready',
|
||||
'popup' => [
|
||||
'url' => route('web.auth.register.danal.start'),
|
||||
'fields' => $danal['fields'], // Start.php로 보낼 hidden inputs
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('web.auth.register.terms')
|
||||
->with('ui_dialog', [
|
||||
'type' => 'alert',
|
||||
'title' => '완료',
|
||||
'message' => "약관 동의가 저장되었습니다.\n\n다음 단계(본인인증)를 진행합니다.",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function danalStart(Request $request)
|
||||
{
|
||||
// fields는 JSON으로 받을 것
|
||||
$fieldsJson = (string)$request->input('fields', '');
|
||||
$fields = json_decode($fieldsJson, true);
|
||||
|
||||
if (!is_array($fields) || empty($fields)) {
|
||||
abort(400, 'Invalid Danal fields');
|
||||
}
|
||||
|
||||
return view('web.auth.danal_autosubmit', [
|
||||
'action' => 'https://wauth.teledit.com/Danal/WebAuth/Web/Start.php',
|
||||
'fields' => $fields,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function danalResult(Request $request, DanalAuthtelService $danal)
|
||||
{
|
||||
$payload = $request->all();
|
||||
|
||||
if (config('app.debug')) {
|
||||
Log::info('[DANAL][RESULT] payload keys', [
|
||||
'method' => $request->method(),
|
||||
'url' => $request->fullUrl(),
|
||||
'keys' => array_keys($payload),
|
||||
]);
|
||||
Log::info('[DANAL][RESULT] payload', $this->maskDanalPayloadForLog($payload));
|
||||
}
|
||||
|
||||
$tid = (string)($payload['TID'] ?? '');
|
||||
if ($tid === '') {
|
||||
return response()->view('web.auth.danal_finish_top', [
|
||||
'ok' => false,
|
||||
'message' => 'TID가 없습니다.',
|
||||
'redirect' => route('web.auth.register.terms'),
|
||||
]);
|
||||
}
|
||||
|
||||
// CI와 동일: TID로 CONFIRM 호출해서 RETURNCODE를 받는다 (dndata 사용 안함)
|
||||
$res = $danal->confirm($tid, 0, 1);
|
||||
|
||||
if (config('app.debug')) {
|
||||
Log::info('[DANAL][CONFIRM] keys', ['keys' => array_keys($res)]);
|
||||
Log::info('[DANAL][CONFIRM] res', $this->maskDanalPayloadForLog($res));
|
||||
}
|
||||
|
||||
$ok = (($res['RETURNCODE'] ?? '') === '0000');
|
||||
|
||||
if (!$ok) {
|
||||
return response()->view('web.auth.danal_finish_top', [
|
||||
'ok' => false,
|
||||
'message' => ($res['RETURNMSG'] ?? '본인인증에 실패했습니다.') . ' (' . ($res['RETURNCODE'] ?? 'NO_CODE') . ')',
|
||||
'redirect' => route('web.auth.register.terms'),
|
||||
]);
|
||||
}
|
||||
|
||||
/* 다날 통신 성공 후 체크*/
|
||||
$normPhone = function (?string $p) {
|
||||
$p = preg_replace('/\D+/', '', (string)$p);
|
||||
return $p;
|
||||
};
|
||||
|
||||
$signupPhone = $normPhone(data_get($request->session()->get('signup', []), 'phone')); //처음 입력 전화번호
|
||||
$passPhone = $normPhone($res['PHONE'] ?? null); //인증받은 전화번호
|
||||
|
||||
// signup.phone vs PASS PHONE 비교
|
||||
if ($signupPhone === '' || $passPhone === '' || $signupPhone !== $passPhone) {
|
||||
$request->session()->flush();
|
||||
$request->session()->save();
|
||||
|
||||
return response()->view('web.auth.danal_finish_top', [
|
||||
'ok' => false,
|
||||
'message' => '인증에 사용한 휴대폰 번호가 가입 진행 번호와 일치하지 않습니다. 다시 진행해 주세요.',
|
||||
'redirect' => route('web.auth.register'),
|
||||
]);
|
||||
}
|
||||
|
||||
// 통신사 제한
|
||||
$carrier = (string)($res['CARRIER'] ?? '');
|
||||
$blockedCarriers = ['SKT_MVNO', 'KT_MVNO', 'LGT_MVNO']; // 알뜰폰
|
||||
if (in_array($carrier, $blockedCarriers, true)) {
|
||||
$request->session()->flush();
|
||||
$request->session()->save();
|
||||
|
||||
return response()->view('web.auth.danal_finish_top', [
|
||||
'ok' => false,
|
||||
'message' => '죄송합니다. 알뜰폰(또는 일부 통신사) 휴대전화는 가입할 수 없습니다.',
|
||||
'redirect' => route('web.auth.register'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 성공: 다음 단계에서 쓸 데이터 세션 저장
|
||||
$request->session()->put('register.pass_verified', true);
|
||||
$request->session()->put('register.pass_payload', $res);
|
||||
$request->session()->save();
|
||||
|
||||
return response()->view('web.auth.danal_finish_top', [
|
||||
'ok' => true,
|
||||
'message' => '본인인증이 완료되었습니다.',
|
||||
'redirect' => route('web.auth.register.profile'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function showProfileForm(Request $request)
|
||||
{
|
||||
if (!$request->session()->get('register.pass_verified')) {
|
||||
return redirect()->route('web.auth.register.terms')
|
||||
->with('ui_dialog', [
|
||||
'type'=>'alert',
|
||||
'title'=>'안내',
|
||||
'message'=>'본인인증 후 진행해 주세요.',
|
||||
]);
|
||||
}
|
||||
|
||||
return view('web.auth.profile');
|
||||
}
|
||||
|
||||
|
||||
private function maskDanalPayloadForLog(array $payload): array
|
||||
{
|
||||
$masked = $payload;
|
||||
|
||||
$sensitiveKeys = [
|
||||
'CI','DI','NAME','BIRTH','SEX','GENDER','TEL','PHONE','MOBILE',
|
||||
'TID','TXID','TOKEN','ENC','CERT','SSN'
|
||||
];
|
||||
|
||||
foreach ($masked as $k => $v) {
|
||||
$keyUpper = strtoupper((string)$k);
|
||||
foreach ($sensitiveKeys as $sk) {
|
||||
if (str_contains($keyUpper, $sk)) {
|
||||
$masked[$k] = '***';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $masked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ use App\Models\Member\MemAuthLog;
|
||||
use App\Models\Member\MemInfo;
|
||||
use App\Models\Member\MemJoinFilter;
|
||||
use App\Models\Member\MemJoinLog;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@ -107,39 +108,18 @@ class MemberAuthRepository
|
||||
* Step0: phone check + join_filter + join_log
|
||||
* ========================================================= */
|
||||
|
||||
public function normalizeKoreanPhone(string $raw): ?string
|
||||
private function normalizeKoreanPhone(string $raw): ?string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $raw ?? '');
|
||||
if (!$digits) return null;
|
||||
|
||||
// 82 국제형 → 0 시작으로 변환
|
||||
if (str_starts_with($digits, '82')) {
|
||||
$digits = '0' . substr($digits, 2);
|
||||
if (strlen($digits) !== 11) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 010/011/016/017/018/019 + 10~11자리
|
||||
if (!preg_match('/^01[016789]\d{7,8}$/', $digits)) {
|
||||
if (substr($digits, 0, 3) !== '010') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
public function ipToCClass(string $ip): string
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return '';
|
||||
}
|
||||
$p = explode('.', $ip);
|
||||
return count($p) === 4 ? ($p[0] . '.' . $p[1] . '.' . $p[2] . '.0') : '';
|
||||
}
|
||||
|
||||
public function isAlreadyMemberByPhone(string $phone): bool
|
||||
{
|
||||
return MemInfo::query()
|
||||
->where('cell_phone', $phone)
|
||||
->where('dt_out', '0000-00-00 00:00:00')
|
||||
->exists();
|
||||
return $digits; // ✅ 무조건 11자리 숫자만 리턴
|
||||
}
|
||||
|
||||
/**
|
||||
@ -196,62 +176,169 @@ class MemberAuthRepository
|
||||
*/
|
||||
public function step0PhoneCheck(string $rawPhone, string $ip4 = ''): array
|
||||
{
|
||||
$base = [
|
||||
'ok' => false,
|
||||
'reason' => '',
|
||||
'message' => '',
|
||||
'redirect' => null,
|
||||
'phone' => null,
|
||||
'filter' => null,
|
||||
'admin_phones' => [],
|
||||
];
|
||||
|
||||
// 0) IP 필터 먼저
|
||||
$ip4c = $this->ipToCClass($ip4);
|
||||
$ipHit = $this->checkJoinFilterByIp($ip4, $ip4c);
|
||||
|
||||
if ($ipHit) {
|
||||
$base['filter'] = $ipHit;
|
||||
$base['admin_phones'] = $ipHit['admin_phones'] ?? [];
|
||||
}
|
||||
|
||||
if ($ipHit && ($ipHit['join_block'] ?? '') === 'A') {
|
||||
return array_merge($base, [
|
||||
'ok' => false,
|
||||
'reason' => 'blocked_ip',
|
||||
'message' => '현재 가입이 제한된 정보입니다. 고객센터로 문의해 주세요.',
|
||||
]);
|
||||
}
|
||||
|
||||
// 1) 전화번호 검증
|
||||
$phone = $this->normalizeKoreanPhone($rawPhone);
|
||||
if (!$phone) {
|
||||
return [
|
||||
return array_merge($base, [
|
||||
'ok' => false,
|
||||
'reason' => 'invalid_phone',
|
||||
'message' => '휴대폰 번호 형식이 올바르지 않습니다.',
|
||||
];
|
||||
}
|
||||
|
||||
$ip4c = $this->ipToCClass($ip4);
|
||||
|
||||
// already member
|
||||
if ($this->isAlreadyMemberByPhone($phone)) {
|
||||
$this->writeJoinLog([
|
||||
'gubun' => 'already_member',
|
||||
'mem_no' => 0,
|
||||
'cell_phone' => $phone,
|
||||
'ip4' => $ip4,
|
||||
'ip4_c' => $ip4c,
|
||||
'error_code' => 'J2',
|
||||
]);
|
||||
}
|
||||
$base['phone'] = $phone;
|
||||
|
||||
return ['ok' => true, 'reason' => 'already_member', '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, // 필요하면 유지
|
||||
]);
|
||||
}
|
||||
|
||||
// join filter
|
||||
// 3) 기존 phone+ip 필터
|
||||
$filter = $this->checkJoinFilter($phone, $ip4, $ip4c);
|
||||
if ($filter && ($filter['block'] ?? false) === true) {
|
||||
$this->writeJoinLog([
|
||||
'gubun' => $filter['gubun'] ?? 'filter_block',
|
||||
'mem_no' => 0,
|
||||
'cell_phone' => $phone,
|
||||
'ip4' => $ip4,
|
||||
'ip4_c' => $ip4c,
|
||||
'error_code' => 'J1',
|
||||
]);
|
||||
|
||||
return [
|
||||
return array_merge($base, [
|
||||
'ok' => false,
|
||||
'reason' => 'blocked',
|
||||
'phone' => $phone,
|
||||
'filter' => $filter,
|
||||
'message' => '현재 가입이 제한된 정보입니다. 고객센터로 문의해 주세요.',
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
// pass
|
||||
$this->writeJoinLog([
|
||||
'gubun' => $filter['gubun'] ?? 'ok',
|
||||
'mem_no' => 0,
|
||||
'cell_phone' => $phone,
|
||||
'ip4' => $ip4,
|
||||
'ip4_c' => $ip4c,
|
||||
'error_code' => 'J0',
|
||||
// 4) 성공(가입 가능)
|
||||
return array_merge($base, [
|
||||
'ok' => true,
|
||||
'reason' => 'ok',
|
||||
'filter' => $filter ?: $ipHit,
|
||||
'message' => "회원가입 가능한 전화번호 입니다.\n\n약관동의 페이지로 이동합니다.",
|
||||
'redirect' => route('web.auth.register.terms'),
|
||||
]);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'reason' => 'ok', 'phone' => $phone, 'filter' => $filter];
|
||||
|
||||
|
||||
private function findMemberByPhone(string $phone): ?object
|
||||
{
|
||||
/** @var CiSeedCrypto $seed */
|
||||
$seed = app(CiSeedCrypto::class);
|
||||
$phoneEnc = $seed->encrypt($phone);
|
||||
|
||||
// 실제로 매칭되는 회원 1건을 가져옴
|
||||
return DB::table('mem_info')
|
||||
->select('mem_no', 'cell_phone')
|
||||
->where('cell_phone', $phoneEnc)
|
||||
->limit(1)
|
||||
->first();
|
||||
}
|
||||
|
||||
|
||||
public function ipToCClass(string $ip): string
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return '';
|
||||
}
|
||||
$p = explode('.', $ip);
|
||||
return count($p) === 4 ? ($p[0] . '.' . $p[1] . '.' . $p[2] . '.0') : '';
|
||||
}
|
||||
|
||||
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'])
|
||||
->where(function ($q) use ($ip4, $ip4c) {
|
||||
// ✅ C class (보통 gubun_code='01', filter='162.168.0.0')
|
||||
$q->where(function ($q2) use ($ip4c) {
|
||||
$q2->where('gubun_code', '01')
|
||||
->where('filter', $ip4c);
|
||||
});
|
||||
|
||||
// ✅ D class (보통 gubun_code='02', filter='162.168.0.2')
|
||||
$q->orWhere(function ($q2) use ($ip4) {
|
||||
$q2->where('gubun_code', '02')
|
||||
->where('filter', $ip4);
|
||||
});
|
||||
|
||||
// ✅ (레거시) gubun_code가 01인데도 filter에 단일 IP가 들어있는 케이스 방어
|
||||
$q->orWhere(function ($q2) use ($ip4) {
|
||||
$q2->where('gubun_code', '01')
|
||||
->where('filter', $ip4);
|
||||
});
|
||||
})
|
||||
->orderByRaw("CASE join_block WHEN 'A' THEN 0 WHEN 'S' THEN 1 ELSE 9 END")
|
||||
->limit(1)
|
||||
->first();
|
||||
|
||||
if (!$row) return null;
|
||||
|
||||
$adminPhones = $this->parseAdminPhones($row->admin_phone ?? '');
|
||||
|
||||
return [
|
||||
'gubun_code' => $row->gubun_code ?? null,
|
||||
'join_block' => $row->join_block ?? null,
|
||||
'filter' => $row->filter ?? null,
|
||||
'admin_phones' => $adminPhones,
|
||||
'ip' => $ip4,
|
||||
'ip4c' => $ip4c,
|
||||
];
|
||||
}
|
||||
|
||||
private function parseAdminPhones($value): array
|
||||
{
|
||||
// admin_phone: ["010-3682-8958", ...] 형태(=JSON 배열)라고 했으니 JSON 우선
|
||||
if (is_array($value)) return $value;
|
||||
|
||||
$s = trim((string)$value);
|
||||
if ($s === '') return [];
|
||||
|
||||
$json = json_decode($s, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
|
||||
return array_values(array_filter(array_map('trim', $json)));
|
||||
}
|
||||
|
||||
// 혹시 "010...,010..." 같은 문자열이면 콤마 분리 fallback
|
||||
$parts = array_map('trim', explode(',', $s));
|
||||
return array_values(array_filter($parts));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
222
app/Services/Danal/DanalAuthtelService.php
Normal file
222
app/Services/Danal/DanalAuthtelService.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Danal;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Danal PASS(Authtel) 준비 서비스
|
||||
*
|
||||
* - prepare(): 다날 Start 페이지로 POST할 hidden fields를 구성
|
||||
* - callTrans / makeFormFields / getRandom 등은 별도 Client로 분리하는 게 이상적이지만,
|
||||
* 지금은 "작동 우선"으로 이 파일 하나로 정리할 수 있게 구성합니다.
|
||||
*/
|
||||
final class DanalAuthtelService
|
||||
{
|
||||
private string $serviceUrl;
|
||||
private string $cpid;
|
||||
private string $cppwd;
|
||||
private string $charset;
|
||||
private int $connectTimeout;
|
||||
private int $timeout;
|
||||
private bool $debug;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$cfg = (array) config('danal.authtel', []);
|
||||
|
||||
$this->serviceUrl = (string) Arr::get($cfg, 'service_url', '');
|
||||
$this->cpid = (string) Arr::get($cfg, 'cpid', '');
|
||||
$this->cppwd = (string) Arr::get($cfg, 'cppwd', '');
|
||||
$this->charset = (string) Arr::get($cfg, 'charset', 'UTF-8');
|
||||
$this->connectTimeout = (int) Arr::get($cfg, 'connect_timeout', 5);
|
||||
$this->timeout = (int) Arr::get($cfg, 'timeout', 30);
|
||||
$this->debug = (bool) Arr::get($cfg, 'debug', false);
|
||||
}
|
||||
|
||||
public function prepare(array $opt): array
|
||||
{
|
||||
// 필수 설정 체크
|
||||
if ($this->cpid === '' || $this->cppwd === '' || $this->serviceUrl === '') {
|
||||
return ['ok' => false, 'message' => 'DANAL 설정(CPID/CPPWD/SERVICE_URL)이 없습니다.'];
|
||||
}
|
||||
|
||||
$targetUrl = (string) ($opt['targetUrl'] ?? '');
|
||||
$backUrl = (string) ($opt['backUrl'] ?? '');
|
||||
|
||||
if ($targetUrl === '') return ['ok' => false, 'message' => 'targetUrl 누락'];
|
||||
if ($backUrl === '') return ['ok' => false, 'message' => 'backUrl 누락'];
|
||||
|
||||
// 기본 타이틀
|
||||
$cpTitle = (string) ($opt['cpTitle'] ?? request()->getHost());
|
||||
|
||||
// TransR
|
||||
$trans = [
|
||||
'TXTYPE' => 'ITEMSEND',
|
||||
'SERVICE' => 'UAS',
|
||||
'AUTHTYPE' => '36',
|
||||
'CPID' => $this->cpid,
|
||||
'CPPWD' => $this->cppwd,
|
||||
'TARGETURL' => $targetUrl,
|
||||
'CPTITLE' => $cpTitle,
|
||||
];
|
||||
|
||||
// 다날 서버 통신
|
||||
$res = $this->callTrans($trans);
|
||||
|
||||
if (($res['RETURNCODE'] ?? '') !== '0000') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $this->formatReturnMsg($res),
|
||||
];
|
||||
}
|
||||
|
||||
// CI3의 ByPassValue
|
||||
$byPass = [
|
||||
// CI의 GetBgColor(0~10 => "00"~"10") 느낌 유지
|
||||
'BgColor' => $this->getBgColor($this->getRandom(0, 10)),
|
||||
'BackURL' => $backUrl,
|
||||
'IsCharSet' => $this->charset,
|
||||
'ByBuffer' => 'This value bypass to CPCGI Page',
|
||||
'ByAnyName' => 'AnyValue',
|
||||
];
|
||||
|
||||
// Start.php로 보낼 hidden fields 구성
|
||||
// - 여기서는 절대 e()/htmlspecialchars 같은 escape를 하지 마세요.
|
||||
// - 프론트에서 input.value로 넣으면 브라우저가 처리합니다.
|
||||
$fields = array_merge(
|
||||
$this->makeFormFields($res, ['RETURNCODE', 'RETURNMSG']),
|
||||
$this->makeFormFields($byPass)
|
||||
);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'txid' => $res['TID'] ?? $res['TXID'] ?? null,
|
||||
'fields' => $fields,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 다날 서버 통신
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function callTrans(array $reqData): array
|
||||
{
|
||||
// x-www-form-urlencoded
|
||||
$body = http_build_query($reqData, '', '&', PHP_QUERY_RFC3986);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
|
||||
// SSL 검증은 끄지 마세요 (운영 보안)
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
|
||||
curl_setopt($ch, CURLOPT_URL, $this->serviceUrl);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded; charset=' . $this->charset,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
|
||||
$resStr = curl_exec($ch);
|
||||
|
||||
if (($errno = curl_errno($ch)) !== 0) {
|
||||
$err = curl_error($ch) ?: 'curl error';
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'RETURNCODE' => '-1',
|
||||
'RETURNMSG' => 'NETWORK ERROR(' . $errno . ':' . $err . ')',
|
||||
];
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
// 다날 응답 파싱
|
||||
return $this->str2data((string) $resStr);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function str2data(string $str): array
|
||||
{
|
||||
$data = [];
|
||||
foreach (explode('&', $str) as $line) {
|
||||
$kv = explode('=', $line, 2);
|
||||
if (count($kv) === 2) {
|
||||
$data[$kv[0]] = $kv[1];
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* hidden fields 만들기 (escape 하지 않음)
|
||||
* @param array<string, mixed> $arr
|
||||
* @param array<int, string> $excludeKeys
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function makeFormFields(array $arr, array $excludeKeys = [], string $prefix = ''): array
|
||||
{
|
||||
$out = [];
|
||||
$preLen = strlen(trim($prefix));
|
||||
|
||||
foreach ($arr as $key => $value) {
|
||||
$key = (string) $key;
|
||||
if ($key === '' || trim($key) === '') continue;
|
||||
|
||||
if (in_array($key, $excludeKeys, true)) continue;
|
||||
|
||||
if ($preLen > 0 && substr($key, 0, $preLen) !== $prefix) continue;
|
||||
|
||||
// 다날은 value에 urlencoded가 들어갈 수 있음
|
||||
// 그대로 보내는 게 원칙이지만, 필요 시 여기서 정책적으로 urldecode/encode 조절 가능
|
||||
$out[$key] = is_scalar($value) || $value === null ? (string) $value : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function getBgColor($bgColor): string
|
||||
{
|
||||
$color = 0;
|
||||
$i = (int) $bgColor;
|
||||
if ($i > 0 && $i < 11) $color = $i;
|
||||
return sprintf('%02d', $color);
|
||||
}
|
||||
|
||||
private function getRandom(int $min, int $max): int
|
||||
{
|
||||
return random_int($min, $max);
|
||||
}
|
||||
|
||||
/** @param array<string,string> $res */
|
||||
private function formatReturnMsg(array $res): string
|
||||
{
|
||||
$msg = (string)($res['RETURNMSG'] ?? 'DANAL ERROR');
|
||||
$code = (string)($res['RETURNCODE'] ?? 'NO_CODE');
|
||||
return $msg . ' (' . $code . ')';
|
||||
}
|
||||
|
||||
public function confirm(string $tid, int $confirmOption = 0, int $idenOption = 1): array
|
||||
{
|
||||
$req = [
|
||||
'TXTYPE' => 'CONFIRM',
|
||||
'TID' => $tid,
|
||||
'CONFIRMOPTION' => $confirmOption,
|
||||
'IDENOPTION' => $idenOption,
|
||||
];
|
||||
|
||||
// CI 주석: CONFIRMOPTION=1이면 CPID/ORDERID 필수
|
||||
if ($confirmOption === 1) {
|
||||
$req['CPID'] = $this->cpid;
|
||||
}
|
||||
|
||||
return $this->callTrans($req);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -22,7 +22,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
},
|
||||
)
|
||||
|
||||
// (B) Middleware: Reverse Proxy/Host 신뢰 정책
|
||||
// (B) Middleware: Reverse Proxy/Host 신뢰 정책 + CSRF 예외
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: [
|
||||
'192.168.100.0/24',
|
||||
@ -35,6 +35,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'four.syye.net',
|
||||
'shot.syye.net',
|
||||
]);
|
||||
|
||||
// CSRF 예외 처리
|
||||
// - 도메인 제외, path만
|
||||
// - 네 라우트 정의 기준: POST register/danal/result
|
||||
$middleware->validateCsrfTokens(except: [
|
||||
'auth/register/danal/result', //다날 PASS 콜백 (외부 서버가 호출)
|
||||
]);
|
||||
})
|
||||
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
15
config/danal.php
Normal file
15
config/danal.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'authtel' => [
|
||||
// CI3 CallTrans가 호출하던 다날 "CPCGI" URL
|
||||
'service_url' => env('DANAL_AUTHTEL_SERVICE_URL', ''),
|
||||
'charset' => env('DANAL_AUTHTEL_CHARSET', 'UTF-8'),
|
||||
'connect_timeout' => env('DANAL_AUTHTEL_CONNECT_TIMEOUT', 5),
|
||||
'timeout' => env('DANAL_AUTHTEL_TIMEOUT', 30),
|
||||
'debug' => env('DANAL_AUTHTEL_DEBUG', false),
|
||||
|
||||
'cpid' => env('DANAL_AUTHTEL_CPID', ''),
|
||||
'cppwd' => env('DANAL_AUTHTEL_CPPWD', ''),
|
||||
],
|
||||
];
|
||||
BIN
public/assets/images/web/member/idpwfind.png
Normal file
BIN
public/assets/images/web/member/idpwfind.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
BIN
public/assets/images/web/member/idpwfind.webp
Normal file
BIN
public/assets/images/web/member/idpwfind.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
public/assets/images/web/member/login.png
Normal file
BIN
public/assets/images/web/member/login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 MiB |
BIN
public/assets/images/web/member/login.webp
Normal file
BIN
public/assets/images/web/member/login.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
BIN
public/assets/images/web/member/register.png
Normal file
BIN
public/assets/images/web/member/register.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 MiB |
BIN
public/assets/images/web/member/register.webp
Normal file
BIN
public/assets/images/web/member/register.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
53
resources/css/ui-dialog.css
Normal file
53
resources/css/ui-dialog.css
Normal file
@ -0,0 +1,53 @@
|
||||
.ui-dialog { position: fixed; inset: 0; z-index: 9999; display: none; }
|
||||
.ui-dialog.is-open { display: block; }
|
||||
|
||||
.ui-dialog__backdrop {
|
||||
position: absolute; inset: 0;
|
||||
background: rgba(0,0,0,.55);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.ui-dialog__panel {
|
||||
position: relative;
|
||||
width: min(520px, calc(100% - 32px));
|
||||
margin: 12vh auto 0;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.55);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-dialog { position: fixed; inset: 0; z-index: 200000; display: none; }
|
||||
.ui-dialog__header { display:flex; align-items:center; justify-content:space-between; padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.10); }
|
||||
.ui-dialog__title { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -0.2px; }
|
||||
.ui-dialog__x {
|
||||
width: 36px; height: 36px; border: 0; background: transparent; color: rgba(255,255,255,.8);
|
||||
font-size: 22px; cursor: pointer; border-radius: 10px;
|
||||
}
|
||||
.ui-dialog__x:hover { background: rgba(255,255,255,.08); }
|
||||
|
||||
.ui-dialog__body { padding: 16px; }
|
||||
.ui-dialog__message { margin: 0; white-space: pre-line; line-height: 1.6; color: rgba(255,255,255,.92); }
|
||||
|
||||
.ui-dialog__footer { display:flex; gap: 10px; justify-content:flex-end; padding: 14px 16px; border-top: 1px solid rgba(255,255,255,.10); }
|
||||
|
||||
.ui-dialog__btn {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.16);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ui-dialog__btn:hover { background: rgba(255,255,255,.10); }
|
||||
|
||||
.ui-dialog__btn--ok { background: rgba(99,102,241,.22); border-color: rgba(99,102,241,.45); }
|
||||
.ui-dialog__btn--ok:hover { background: rgba(99,102,241,.30); }
|
||||
|
||||
.ui-dialog__btn--danger { background: rgba(239,68,68,.20); border-color: rgba(239,68,68,.45); }
|
||||
.ui-dialog__btn--danger:hover { background: rgba(239,68,68,.28); }
|
||||
|
||||
.ui-dialog__btn[disabled] { opacity: .6; cursor: not-allowed; }
|
||||
|
||||
243
resources/js/ui/dialog.js
Normal file
243
resources/js/ui/dialog.js
Normal file
@ -0,0 +1,243 @@
|
||||
class UiDialog {
|
||||
constructor(rootId = "uiDialog") {
|
||||
this.root = document.getElementById(rootId);
|
||||
if (!this.root) return;
|
||||
|
||||
this.titleEl = this.root.querySelector("#uiDialogTitle");
|
||||
this.msgEl = this.root.querySelector("#uiDialogMessage");
|
||||
this.okBtn = this.root.querySelector("#uiDialogOk");
|
||||
this.cancelBtn = this.root.querySelector("#uiDialogCancel");
|
||||
|
||||
this._resolver = null;
|
||||
this._type = "alert";
|
||||
this._lastFocus = null;
|
||||
|
||||
// close triggers
|
||||
this.root.addEventListener("click", (e) => {
|
||||
if (!e.target?.dataset?.uidialogClose) return;
|
||||
|
||||
// 어떤 close인지 구분 (backdrop / x)
|
||||
const isBackdrop = !!e.target.closest(".ui-dialog__backdrop");
|
||||
const isX = !!e.target.closest(".ui-dialog__x");
|
||||
|
||||
// 정책에 따라 차단 (기본: 모두 차단)
|
||||
if (isBackdrop && !this._closePolicy?.backdrop) return;
|
||||
if (isX && !this._closePolicy?.x) return;
|
||||
|
||||
this._resolve(false);
|
||||
});
|
||||
|
||||
|
||||
// keyboard
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (!this.isOpen()) return;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
if (this._closePolicy?.esc) this._resolve(false);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
// Enter는 OK로 처리 (기본 true)
|
||||
if (this._closePolicy?.enter !== false) this._resolve(true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
this.okBtn.addEventListener("click", () => this._resolve(true));
|
||||
this.cancelBtn.addEventListener("click", () => this._resolve(false));
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return !!this.root && this.root.classList.contains("is-open");
|
||||
}
|
||||
|
||||
alert(message, options = {}) {
|
||||
return this._open("alert", message, options);
|
||||
}
|
||||
|
||||
confirm(message, options = {}) {
|
||||
return this._open("confirm", message, options);
|
||||
}
|
||||
|
||||
_open(type, message, options) {
|
||||
if (!this.root) return Promise.resolve(false);
|
||||
|
||||
const {
|
||||
title = type === "confirm" ? "확인" : "알림",
|
||||
okText = "확인",
|
||||
cancelText = "취소",
|
||||
dangerous = false,
|
||||
|
||||
// ✅ 기본: 밖 클릭/닫기(X)/ESC로 닫기 금지
|
||||
closeOnBackdrop = false,
|
||||
closeOnX = false,
|
||||
closeOnEsc = false,
|
||||
closeOnEnter = true,
|
||||
} = options;
|
||||
|
||||
this._closePolicy = {
|
||||
backdrop: !!closeOnBackdrop,
|
||||
x: !!closeOnX,
|
||||
esc: !!closeOnEsc,
|
||||
enter: closeOnEnter !== false,
|
||||
};
|
||||
|
||||
this._type = type;
|
||||
this._lastFocus = document.activeElement;
|
||||
|
||||
this.titleEl.textContent = title;
|
||||
this.msgEl.textContent = message ?? "";
|
||||
|
||||
this.okBtn.textContent = okText;
|
||||
this.cancelBtn.textContent = cancelText;
|
||||
|
||||
// alert면 cancel 숨김
|
||||
if (type === "alert") {
|
||||
this.cancelBtn.style.display = "none";
|
||||
} else {
|
||||
this.cancelBtn.style.display = "";
|
||||
}
|
||||
|
||||
// danger 스타일
|
||||
this.okBtn.classList.toggle("ui-dialog__btn--danger", !!dangerous);
|
||||
|
||||
// open
|
||||
this.root.classList.add("is-open");
|
||||
this.root.setAttribute("aria-hidden", "false");
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
|
||||
// focus
|
||||
setTimeout(() => this.okBtn.focus(), 0);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
_resolve(ok) {
|
||||
if (!this.isOpen()) return;
|
||||
|
||||
// close
|
||||
this.root.classList.remove("is-open");
|
||||
this.root.setAttribute("aria-hidden", "true");
|
||||
document.documentElement.style.overflow = "";
|
||||
|
||||
const r = this._resolver;
|
||||
this._resolver = null;
|
||||
|
||||
// focus restore
|
||||
try { this._lastFocus?.focus?.(); } catch (e) {}
|
||||
|
||||
if (typeof r === "function") r(!!ok);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출 (모든 페이지에서 바로 호출)
|
||||
window.uiDialog = new UiDialog();
|
||||
|
||||
|
||||
// ======================================================
|
||||
// ✅ Global showMsg / clearMsg (공통 사용)
|
||||
// - 페이지에서는 showMsg/clearMsg만 호출
|
||||
// - await showMsg(...) 하면 버튼 클릭 전까지 다음 코드 진행이 멈춤
|
||||
// ======================================================
|
||||
(function () {
|
||||
let cachedHelpEl = null;
|
||||
|
||||
function getHelpEl(opt = {}) {
|
||||
// opt.helpId를 주면 그걸 우선 사용
|
||||
if (opt.helpId) {
|
||||
const el = document.getElementById(opt.helpId);
|
||||
if (el) return el;
|
||||
}
|
||||
// 기본 fallback id (너가 쓰는 reg_phone_help)
|
||||
if (cachedHelpEl && document.contains(cachedHelpEl)) return cachedHelpEl;
|
||||
cachedHelpEl = document.getElementById("reg_phone_help");
|
||||
return cachedHelpEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* showMsg(msg, opt)
|
||||
* opt: {
|
||||
* type:'alert'|'confirm',
|
||||
* title, okText, cancelText, dangerous,
|
||||
* redirect, helpId
|
||||
* }
|
||||
* return: Promise<boolean> (confirm: true/false, alert: true)
|
||||
*/
|
||||
window.showMsg = async function (msg, opt = {}) {
|
||||
const d = Object.assign({
|
||||
type: "alert",
|
||||
title: "알림",
|
||||
okText: "확인",
|
||||
cancelText: "취소",
|
||||
dangerous: false,
|
||||
redirect: "",
|
||||
helpId: "",
|
||||
}, opt || {});
|
||||
|
||||
// ✅ 모달이 있으면 모달로 (여기서 await로 멈춤)
|
||||
if (window.uiDialog && typeof window.uiDialog[d.type] === "function") {
|
||||
const ok = await window.uiDialog[d.type](msg || "", d);
|
||||
|
||||
if (d.redirect) {
|
||||
// alert: OK(true)일 때만 / confirm: OK(true)일 때만
|
||||
if (ok) {
|
||||
window.location.href = d.redirect;
|
||||
}
|
||||
}
|
||||
|
||||
return d.type === "confirm" ? !!ok : true;
|
||||
}
|
||||
|
||||
// ✅ fallback (모달이 없으면 help 영역에 표시)
|
||||
const helpEl = getHelpEl(d);
|
||||
if (helpEl) {
|
||||
helpEl.style.display = "block";
|
||||
helpEl.textContent = msg || "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// 마지막 fallback
|
||||
alert(msg || "");
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* clearMsg(helpId?)
|
||||
*/
|
||||
window.clearMsg = function (helpId = "") {
|
||||
const el = helpId ? document.getElementById(helpId) : getHelpEl({});
|
||||
if (!el) return;
|
||||
el.style.display = "none";
|
||||
el.textContent = "";
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
// ======================================================
|
||||
// 서버 flash 자동 실행 (기존 유지)
|
||||
// ======================================================
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const flashEl = document.getElementById("uiDialogFlash");
|
||||
if (!flashEl || !window.uiDialog) return;
|
||||
|
||||
let payload = null;
|
||||
try { payload = JSON.parse(flashEl.textContent || "{}"); } catch (e) {}
|
||||
|
||||
if (!payload) return;
|
||||
|
||||
/**
|
||||
* payload 예시:
|
||||
* { type:'alert'|'confirm', message:'...', title:'...', okText:'...', cancelText:'...', redirect:'/...' }
|
||||
*/
|
||||
const type = payload.type || "alert";
|
||||
const ok = await window.uiDialog[type](payload.message || "", payload);
|
||||
|
||||
// confirm이고 OK일 때만 redirect 같은 후속 처리
|
||||
if (type === "confirm") {
|
||||
if (ok && payload.redirect) window.location.href = payload.redirect;
|
||||
} else {
|
||||
if (payload.redirect) window.location.href = payload.redirect;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import "../css/ui-dialog.css";
|
||||
import "./ui/dialog";
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// --- Hero Carousel Logic ---
|
||||
|
||||
28
resources/views/web/auth/danal_autosubmit.blade.php
Normal file
28
resources/views/web/auth/danal_autosubmit.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
{{-- resources/views/web/auth/danal_autosubmit.blade.php --}}
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>본인인증 이동</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="padding:16px;font-family:sans-serif;">
|
||||
본인인증 페이지로 이동 중입니다. 잠시만 기다려 주세요…
|
||||
</div>
|
||||
|
||||
<form id="danalAutoSubmitForm" method="post" action="{{ $action }}">
|
||||
@foreach(($fields ?? []) as $k => $v)
|
||||
<input type="hidden" name="{{ $k }}" value="{{ $v }}">
|
||||
@endforeach
|
||||
|
||||
<noscript>
|
||||
<button type="submit">계속</button>
|
||||
</noscript>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('danalAutoSubmitForm').submit();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
32
resources/views/web/auth/danal_finish_top.blade.php
Normal file
32
resources/views/web/auth/danal_finish_top.blade.php
Normal file
@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>처리 결과</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
var ok = @json($ok ?? false);
|
||||
var message = @json($message ?? '');
|
||||
var redirect = @json($redirect ?? '/');
|
||||
|
||||
if (message) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.top) window.top.location.href = redirect;
|
||||
else window.location.href = redirect;
|
||||
} catch (e) {
|
||||
window.location.href = redirect;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<a href="{{ $redirect }}">계속</a>
|
||||
</noscript>
|
||||
</body>
|
||||
</html>
|
||||
@ -12,6 +12,13 @@
|
||||
@section('auth_content')
|
||||
<form class="auth-form" id="findIdForm" onsubmit="return false;">
|
||||
{{-- STEP 1 --}}
|
||||
<img
|
||||
class="reg-step0-hero__img"
|
||||
src="{{ asset('assets/images/web/member/idpwfind.webp') }}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none';"
|
||||
/>
|
||||
<div class="auth-panel is-active" data-step="1">
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="fi_phone">휴대폰 번호</label>
|
||||
|
||||
@ -12,6 +12,14 @@
|
||||
|
||||
@section('auth_content')
|
||||
<form class="auth-form" onsubmit="return false;">
|
||||
<img
|
||||
class="reg-step0-hero__img"
|
||||
src="{{ asset('assets/images/web/member/login.webp') }}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none';"
|
||||
/>
|
||||
|
||||
<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">
|
||||
|
||||
25
resources/views/web/auth/pass_redirect.blade.php
Normal file
25
resources/views/web/auth/pass_redirect.blade.php
Normal file
@ -0,0 +1,25 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '본인인증 이동 | PIN FOR YOU')
|
||||
|
||||
@section('content')
|
||||
<div class="auth-card">
|
||||
<h2>본인인증으로 이동 중입니다…</h2>
|
||||
<p>잠시만 기다려 주세요.</p>
|
||||
|
||||
<form id="danalPassForm" method="POST" action="{{ $actionUrl }}">
|
||||
{{-- 다날로 POST할 때 CSRF는 "우리 사이트용"이라 필요 없음 --}}
|
||||
@foreach($inputs as $k => $v)
|
||||
<input type="hidden" name="{{ $k }}" value="{{ $v }}">
|
||||
@endforeach
|
||||
|
||||
<noscript>
|
||||
<button type="submit">계속</button>
|
||||
</noscript>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('danalPassForm').submit();
|
||||
</script>
|
||||
@endsection
|
||||
36
resources/views/web/auth/profile.blade.php
Normal file
36
resources/views/web/auth/profile.blade.php
Normal file
@ -0,0 +1,36 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '회원정보 입력 | PIN FOR YOU')
|
||||
|
||||
@section('content')
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">회원정보 입력</h2>
|
||||
<p class="auth-desc">계정 생성을 위해 필요한 정보를 입력해 주세요.</p>
|
||||
|
||||
<form method="POST" action="{{ route('web.auth.register.profile.submit') }}">
|
||||
@csrf
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="login_id">아이디</label>
|
||||
<input id="login_id" name="login_id" type="text" autocomplete="username" required>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="email">이메일</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-btn auth-btn--primary">가입 완료</button>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@ -5,51 +5,23 @@
|
||||
@section('canonical', url('/auth/register'))
|
||||
|
||||
@section('h1', '회원가입')
|
||||
@section('desc', '휴대폰 번호 확인 후 본인인증을 진행합니다.')
|
||||
@section('desc', '휴대폰 번호로 가입 여부를 먼저 확인한 뒤, PASS 본인인증을 진행합니다.')
|
||||
@section('card_aria', '회원가입 Step0 - 휴대폰 확인')
|
||||
@section('show_cs_links', true)
|
||||
|
||||
@section('auth_content')
|
||||
|
||||
{{-- Step0 Hero Image + 안내문구 --}}
|
||||
<div class="reg-step0-hero" aria-hidden="true">
|
||||
<img
|
||||
class="reg-step0-hero__img"
|
||||
src="{{ asset('assets/images/web/member/register_step0.webp') }}"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none';"
|
||||
/>
|
||||
<div class="reg-step0-hero__text" aria-hidden="true">
|
||||
<div class="reg-step0-hero__line">휴대폰 번호로 가입 여부를 먼저 확인한 뒤,</div>
|
||||
<div class="reg-step0-hero__line">PASS 본인인증을 진행합니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.reg-step0-hero{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
margin: 6px 0 14px;
|
||||
}
|
||||
.reg-step0-hero__img{
|
||||
width:100%;
|
||||
max-width: 300px;
|
||||
height:auto;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,.08);
|
||||
background: #fff;
|
||||
}
|
||||
.reg-step0-hero__text{
|
||||
text-align:center;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: rgba(0,0,0,.65);
|
||||
}
|
||||
.reg-step0-hero__line{ margin: 0; }
|
||||
</style>
|
||||
<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"
|
||||
/>
|
||||
|
||||
|
||||
<form class="auth-form" id="regStep0Form" onsubmit="return false;">
|
||||
@csrf
|
||||
@ -91,15 +63,6 @@
|
||||
const help = document.getElementById('reg_phone_help');
|
||||
const btn = document.getElementById('reg_next_btn');
|
||||
|
||||
function showMsg(msg) {
|
||||
help.style.display = 'block';
|
||||
help.textContent = msg;
|
||||
}
|
||||
function clearMsg() {
|
||||
help.style.display = 'none';
|
||||
help.textContent = '';
|
||||
}
|
||||
|
||||
// 숫자만 남기고 010-0000-0000 형태로 포맷
|
||||
function formatPhone(value) {
|
||||
const digits = (value || '').replace(/\D/g, '').slice(0, 11);
|
||||
@ -137,12 +100,12 @@
|
||||
|
||||
const phoneDigits = toDigits(input.value);
|
||||
if (!phoneDigits) {
|
||||
showMsg('휴대폰 번호를 입력해 주세요.');
|
||||
await showMsg('휴대폰 번호를 입력해 주세요.', { type:'alert', title:'입력오류' });
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
if (phoneDigits.length < 10) {
|
||||
showMsg('휴대폰 번호를 끝까지 입력해 주세요.');
|
||||
await showMsg('휴대폰 번호를 끝까지 입력해 주세요.', { type:'alert', title:'입력오류' });
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
@ -166,21 +129,49 @@
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
// 422/403에서도 JSON 잘 파싱되게 안전 처리
|
||||
const raw = await res.text();
|
||||
let data = {};
|
||||
try { data = JSON.parse(raw || "{}"); } catch (e) { data = {}; }
|
||||
|
||||
if (!res.ok || data.ok === false) {
|
||||
showMsg(data.message || '처리에 실패했습니다.');
|
||||
// 공통 메시지
|
||||
const msg = data.message || '처리에 실패했습니다.';
|
||||
|
||||
// 1) 이미 가입(=confirm)
|
||||
if (data.reason === 'already_member') {
|
||||
await showMsg(msg, {
|
||||
type: 'confirm',
|
||||
title: '안내',
|
||||
okText: '이동',
|
||||
cancelText: '취소',
|
||||
redirect: data.redirect,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
// 2) 성공(=confirm)
|
||||
if (data.ok === true) {
|
||||
await showMsg(msg || '회원가입 가능한 전화번호 입니다.\n\n인증페이지로 이동합니다.', {
|
||||
type: 'confirm',
|
||||
title: '안내',
|
||||
okText: '이동',
|
||||
cancelText: '취소',
|
||||
redirect: data.redirect,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showMsg('처리에 실패했습니다.');
|
||||
// 3) 나머지 전부 실패(=alert)
|
||||
await showMsg(msg, {
|
||||
type: 'alert',
|
||||
title: '안내',
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
showMsg('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
|
||||
await showMsg("네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", {
|
||||
type:'alert',
|
||||
title:'네트워크오류'
|
||||
});
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
@ -5,18 +5,796 @@
|
||||
@section('canonical', url('/auth/register/terms'))
|
||||
|
||||
@section('h1', '약관 동의')
|
||||
@section('desc', '다음 단계에서 본인인증을 진행합니다.')
|
||||
@section('desc', '필수 약관에 동의하시면 다음 단계(본인인증)로 진행합니다.')
|
||||
@section('card_aria', '회원가입 Step1 - 약관 동의')
|
||||
@section('show_cs_links', true)
|
||||
|
||||
@section('auth_content')
|
||||
<div class="auth-divider">임시 페이지</div>
|
||||
<p style="opacity:.8; line-height:1.6;">
|
||||
Step0(휴대폰 확인) 통과했습니다.<br>
|
||||
다음 단계에서 약관 UI와 저장 로직을 붙일 예정입니다.
|
||||
</p>
|
||||
<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 class="auth-actions">
|
||||
<a class="auth-btn auth-btn--primary" href="{{ route('web.auth.register') }}">처음으로</a>
|
||||
{{-- <form id="regTermsForm" class="terms-form" method="POST" action="{{ route('web.auth.register.terms.submit') }}" novalidate>--}}
|
||||
<form id="regTermsForm" class="terms-form" method="POST" action="" novalidate>
|
||||
@csrf
|
||||
|
||||
{{-- 전체 동의 --}}
|
||||
<div class="terms-card terms-card--all">
|
||||
<label class="chk">
|
||||
<input type="checkbox" id="agree_all">
|
||||
<span class="chk-ui" aria-hidden="true"></span>
|
||||
<span class="chk-text">
|
||||
<span class="chk-title">전체 동의</span>
|
||||
<span class="chk-sub">필수 및 선택 항목을 모두 포함합니다.</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- 약관 리스트 --}}
|
||||
<div class="terms-card">
|
||||
{{-- [필수] 이용약관 --}}
|
||||
<div class="terms-row">
|
||||
<label class="chk">
|
||||
<input type="checkbox" name="agree_terms" id="agree_terms" value="1" required>
|
||||
<span class="chk-ui" aria-hidden="true"></span>
|
||||
<span class="chk-text">
|
||||
<span class="badge badge--req">필수</span>
|
||||
<span class="terms-label">이용약관 동의</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button type="button" class="terms-view" data-toggle="t_terms">내용보기</button>
|
||||
</div>
|
||||
<div class="terms-body" id="t_terms" hidden>
|
||||
<div class="terms-scroll">
|
||||
<h4>이용약관</h4>
|
||||
|
||||
<div class="terms-legal">
|
||||
<h5>제 1 조 (목적)</h5>
|
||||
<p>이 약관은 ㈜플러스메이커(이하 "회사")가 제공하는 핀포유(이하 “사이트”) 관련 제반 서비스 (이하 "서비스")의 이용조건 및 절차에 관한 회사와 회원간의 권리 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.</p>
|
||||
|
||||
<h5>제 2 조 (약관의 효력 및 변경)</h5>
|
||||
<p>① 이 약관은 서비스 화면이나 기타의 방법으로 이용고객에게 공지함으로써 효력을 발생합니다.</p>
|
||||
<p>② 사이트는 이 약관의 내용을 변경할 수 있으며, 변경된 약관은 제①항과 같은 방법으로 공지 또는 통지함으로써 효력을 발생합니다.</p>
|
||||
|
||||
<h5>제 3 조 (용어의 정의)</h5>
|
||||
<p>이 약관에서 사용하는 용어의 정의는 다음과 같습니다.</p>
|
||||
<p>① 회원 : 사이트와 서비스 이용계약을 체결하거나 이용자 아이디(ID)를 부여받은 개인을 말합니다.</p>
|
||||
<p>② 아이디(ID) : 회원의 식별과 서비스 이용을 위하여 회원이 정하고 사이트가 승인하는 이메일 형식을 말합니다</p>
|
||||
<p>③ 비밀번호 : 회원이 부여 받은 아이디(ID)와 일치된 회원임을 확인하고, 회원 자신의 비밀을 보호하기 위하여 회원이 정한 영문자와 숫자, 특수문자의 조합을 말합니다.</p>
|
||||
<p>⑤ 회원탈퇴 : 사이트 또는 회원이 서비스 이용계약을 취소하는 것을 말합니다.</p>
|
||||
|
||||
<h5>제 4 조 (회원가입)</h5>
|
||||
<p>① 사이트에서 제공하는 절차에 맞춰 이용약관 동의 후 본인인증 절차를 완료한 후, 가입 양식에 따라 회원정보를 기입함으로서 회원가입을 신청합니다.</p>
|
||||
<p>② 사이트는 신청자에 대하여 제3항, 제4항의 경우를 예외로 하여 회원 등록합니다..</p>
|
||||
<p>③ 사이트는 다음에 해당하는 경우에 그 회원 등록을 제한사유가 해소될 때까지 유보할 수 있습니다.</p>
|
||||
<ul>
|
||||
<li>가. 서비스 관련 설비에 여유가 없는 경우</li>
|
||||
<li>나. 기술상 지장이 있는 경우</li>
|
||||
<li>다. 기타 사이트가 필요하다고 인정되는 경우</li>
|
||||
</ul>
|
||||
<p>④ 사이트는 신청자가 다음에 해당하는 경우에는 회원 등록을 거부할 수 있습니다.</p>
|
||||
<ul>
|
||||
<li>가. 다른 개인(사이트)의 명의를 사용하여 신청한 경우</li>
|
||||
<li>나. 이용자 정보를 허위로 기재하여 신청한 경우</li>
|
||||
<li>다. 사회의 안녕질서 또는 미풍양속을 저해할 목적으로 신청한 경우</li>
|
||||
<li>라. 기타 사이트 소정의 이용신청요건을 충족하지 못하는 경우</li>
|
||||
</ul>
|
||||
|
||||
<h5>제 5 조 (회원 탈퇴 및 이용정지)</h5>
|
||||
<p>① 회원은 사이트에 언제든지 탈퇴를 요청할 수 있으며 사이트는 요청사항을 명확히 확인한 즉시 회원탈퇴를 처리합니다</p>
|
||||
<p>② 회원이 다음 각 호의 사유에 해당하는 경우, 사이트는 회원자격을 제한 및 정지시킬 수 있습니다.</p>
|
||||
<ol>
|
||||
<li>회원 가입 시 허위 정보를 기재한 경우</li>
|
||||
<li>타인 명의 또는 전자결제 수단을 도용 또는 의심되는 경우</li>
|
||||
<li>사이트가 제공하는 관련 서비스 프로그램에 대한 허가되지 않은 접근 시도 및 변경을 시도하는 경우</li>
|
||||
<li>기타 사이트가 필요하다고 인정되는 경우</li>
|
||||
</ol>
|
||||
<p>③ 회원 자격 제한 및 정지가 이루어진 경우 회원은 고객센터 상담을 통해 사유 및 해결 방안에 대해 확인할 수 있습니다.</p>
|
||||
|
||||
<h5>제 6 조 (이용자정보의 변경)</h5>
|
||||
<p>회원은 회원가입 시 기재했던 회원정보가 변경되었을 경우에는, 사이트 개인정보 확인 페이지를 통해 으로 수정하여야 하며 변경하지 않음으로 인하여 발생되는 모든 문제의 책임은 회원에게 있습니다.</p>
|
||||
|
||||
<h5>제 7 조 (사이트의 의무)</h5>
|
||||
<p>① 사이트는 법령과 이 약관이 금지하거나 공서양속에 반하는 행위를 하지 않으며 이 약관이 정하는 바에 따라 지속적이고, 안정적으로 재화․용역을 제공하는데 최선을 다하여야 합니다.</p>
|
||||
<p>② 사이트는 서비스 제공과 관련하여 취득한 회원의 개인정보를 회원의 동의없이 타인에게 누설, 공개 또는 배포할 수 없으며, 서비스관련 업무 이외의 상업적 목적으로 사용할 수 없습니다. 단, 다음 각 호의 1에 해당하는 경우는 예외입니다.</p>
|
||||
<ul>
|
||||
<li>가. 전기통신기본법 등 법률의 규정에 의해 국가기관의 요구가 있는 경우</li>
|
||||
<li>나. 범죄에 대한 수사상의 목적이 있거나 정보통신윤리 위원회의 요청이 있는 경우</li>
|
||||
<li>다. 기타 관계법령에서 정한 정차에 따른 요청이 있는 경우</li>
|
||||
</ul>
|
||||
<p>③ 사이트는 이 약관에서 정한 바에 따라 지속적, 안정적으로 서비스를 제공할 의무가 있습니다.</p>
|
||||
|
||||
<h5>제 8 조 (회원의 의무)</h5>
|
||||
<p>① 회원은 서비스 이용 시 다음 각 호의 행위를 하지 않아야 합니다.</p>
|
||||
<ul>
|
||||
<li>가. 다른 회원의 ID를 부정하게 사용하는 행위</li>
|
||||
<li>나. 서비스에서 얻은 정보를 사이트의 사전승낙 없이 회원의 이용 이외의 목적으로 복제하거나 이를 변경, 출판 및 방송 등에 사용하거나 타인에게 제공하는 행위</li>
|
||||
<li>다. 사이트의 저작권, 타인의 저작권 등 기타 권리를 침해하는 행위</li>
|
||||
<li>라. 공공질서 및 미풍양속에 위반되는 내용의 정보, 문장, 도형 등을 타인에게 유포하는 행위</li>
|
||||
<li>마. 범죄와 결부된다고 객관적으로 판단되는 행위</li>
|
||||
<li>바. 기타 관계법령에 위배되는 행위</li>
|
||||
</ul>
|
||||
<p>② 회원은 관계법령, 이 약관에서 규정하는 사항, 서비스 이용 안내 및 주의 사항을 준수하여야 합니다.</p>
|
||||
<p>③ 회원은 내용별로 사이트가 서비스 공지사항에 게시하거나 별도로 공지한 이용 제한 사항을 준수하여야 합니다.</p>
|
||||
|
||||
<h5>제 9 조 (회원 아이디(ID)와 비밀번호 관리에 대한 회원의 의무)</h5>
|
||||
<p>① 아이디(ID)와 비밀번호에 대한 모든 관리는 회원에게 책임이 있습니다. 회원에게 부여된 아이디(ID)와 비밀번호의 관리소홀, 부정사용에 의하여 발생하는 모든 결과에 대한 전적인 책임은 회원에게 있습니다.</p>
|
||||
<p>② 자신의 아이디(ID)가 부정하게 사용된 경우 또는 기타 보안 위반에 대하여, 회원은 반드시 사이트에 그 사실을 통보해야 합니다.</p>
|
||||
|
||||
<h5>제 10 조 (서비스 이용료)</h5>
|
||||
<p>① 사이트를 통해 상품 구매 시 휴대폰소액결제 서비스를 이용해 결제하는 경우 회사가 정한 규정에 따라 서비스 이용료가 부과됩니다.</p>
|
||||
|
||||
<h5>제 11 조 (서비스 제한 및 정지)</h5>
|
||||
<p>① 사이트는 전시, 사변, 천재지변 또는 이에 준하는 국가비상사태가 발생하거나 발생할 우려가 있는 경우와 전기통신사업법에 의한 기간통신 사업자가 전기통신서비스를 중지하는 등 기타 불가항력적 사유가 있는 경우에는 서비스의 전부 또는 일부를 제한하거나 정지할 수 있습니다.</p>
|
||||
<p>② 사이트는 제1항의 규정에 의하여 서비스의 이용을 제한하거나 정지할 때에는 그 사유 및 제한기간 등을 지체없이 회원에게 알려야 합니다.</p>
|
||||
|
||||
<h5>제 12 조 (정보의 변경)</h5>
|
||||
<p>회원이 주소, 비밀번호 등 고객정보를 변경하고자 하는 경우에는 홈페이지의 회원정보 변경 서비스를 이용하여 변경할 수 있습니다.</p>
|
||||
|
||||
<h5>제 13 조 (면책조항)</h5>
|
||||
<p>① 회사는 다음 각 호에 해당하는 경우에는 책임을 지지 않습니다.</p>
|
||||
<ol>
|
||||
<li>전시, 사변, 천재지변 또는 이에 준하는 국가 비상 사태 등 불가항력적인 경우</li>
|
||||
<li>회원의 고의 또는 과실로 인하여 손해가 발생한 경우</li>
|
||||
<li>전기통신사업법에 의한 타 기간 통신사업자가 제공하는 전기통신서비스 장애로 인한 경우</li>
|
||||
</ol>
|
||||
<p>② 회사는 회원의 귀책 사유로 인한 서비스 이용의 장애에 대하여 책임을 지지 아니합니다.</p>
|
||||
|
||||
<h5>제 14 조 (관할법원)</h5>
|
||||
<p>① 회사의 요금체계 등 서비스 이용과 관련하여 분쟁이 발생될 경우, 회사의 본사 소재지를 관할하는 법원을 관할 법원으로 하여 이를 해결합니다.</p>
|
||||
<p>② 서비스 이용과 관련하여 회사와 회원 간의 소송에는 대한민국 법을 적용합니다.</p>
|
||||
|
||||
<h5>[부칙]</h5>
|
||||
<p>(시행일) 이 약관은 2019년 01월부터 시행합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="terms-divider"></div>
|
||||
|
||||
{{-- [필수] 개인정보 --}}
|
||||
<div class="terms-row">
|
||||
<label class="chk">
|
||||
<input type="checkbox" name="agree_privacy" id="agree_privacy" value="1" required>
|
||||
<span class="chk-ui" aria-hidden="true"></span>
|
||||
<span class="chk-text">
|
||||
<span class="badge badge--req">필수</span>
|
||||
<span class="terms-label">개인정보 수집·이용 동의</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button type="button" class="terms-view" data-toggle="t_privacy">내용보기</button>
|
||||
</div>
|
||||
<div class="terms-body" id="t_privacy" hidden>
|
||||
<div class="terms-scroll">
|
||||
<h4>개인정보 수집·이용 동의</h4>
|
||||
|
||||
<div class="terms-legal">
|
||||
<h5>1. 개인정보의 처리 목적</h5>
|
||||
<p>
|
||||
주식회사 플러스메이커(이하 “회사”라 함)가 운영하는 “핀포유”는 개인정보를 다음의 목적을 위해 처리합니다.
|
||||
처리한 개인정보는 다음의 목적 이외의 용도로는 사용되지 않으며 이용 목적이 변경될 시에는 사전동의를 구할 예정입니다.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>(1) 홈페이지 회원가입 및 관리</strong> : 회원 가입의사 확인, 회원자격 유지·관리, 서비스 부정이용 방지, 각종 고지·통지, 고충처리, 분쟁 조정을 위한 기록 보존</li>
|
||||
<li><strong>(2) 민원사무 처리</strong> : 민원인의 신원 확인, 민원사항 확인, 사실조사를 위한 연락·통지, 처리결과 통보</li>
|
||||
</ul>
|
||||
|
||||
<h5>2. 개인정보의 항목 및 수집방법</h5>
|
||||
<p><strong>(1) 개인정보 수집 항목</strong></p>
|
||||
<p>① 회사는 회원가입, 상담, 기타 부가 서비스 제공 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.</p>
|
||||
<ul>
|
||||
<li>이름, 생년월일, 성별, 전화번호, 이메일, 아이디, 비밀번호, 개인식별값(CI, DI)</li>
|
||||
</ul>
|
||||
<p>② 또한 서비스 이용과정이나 사업 처리 과정에서 아래와 같은 정보들이 생성되어 수집될 수 있습니다.</p>
|
||||
<ul>
|
||||
<li>접속 IP 정보, 쿠키, 서비스 이용 및 접속기록</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>(2) 개인정보 수집방법</strong></p>
|
||||
<ul>
|
||||
<li>홈페이지(회원가입, 문의상담), 서면양식</li>
|
||||
<li>제휴사로부터의 제공</li>
|
||||
</ul>
|
||||
|
||||
<h5>3. 개인정보의 처리 및 보유 기간</h5>
|
||||
<p>
|
||||
회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의 받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.
|
||||
또한, 정보통신망 이용촉진 및 정보보호 등에 관한 법률에 따라 1년간 “핀포유”를 이용하지 아니한 회원의 개인정보는 파기하거나 별도 분리되어 보관됩니다.
|
||||
그럼에도 회원이 그 보유기간을 별도로 지정하거나, 법률에 근거하여 보존할 필요가 있는 경우는 기존과 같이 안전 관리를 다하여 보관합니다.
|
||||
</p>
|
||||
|
||||
<p><strong>(1) 홈페이지 회원가입 및 관리, 민원사무 처리, 재화 또는 서비스 제공</strong></p>
|
||||
<p>
|
||||
마케팅 및 광고의 활용과 관련한 개인정보는 수집·이용에 관한 동의일로부터 지체 없이 파기까지 위 이용목적을 위하여 보유·이용됩니다.
|
||||
</p>
|
||||
|
||||
<p><strong>(2) 관계법령에 따른 보관</strong></p>
|
||||
<p>
|
||||
상법, 전자상거래 등에서의 소비자보호에 관한 법률 등 관계법령의 규정에 의하여 보존할 필요가 있는 경우 회사는 관계법령에서 정한 일정한 기간 동안 회원정보를 보관합니다.
|
||||
이 경우 회사는 보관하는 정보를 그 보관의 목적으로만 이용합니다.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>통신비밀보호법</strong> : 로그기록, IP 등 (수사기관 요청 시 제공) / <strong>3개월</strong></li>
|
||||
<li><strong>전자상거래 등에서의 소비자보호에 관한 법률</strong> : 회원의 불만 또는 분쟁처리에 관한 기록(회원 식별정보, 분쟁처리 기록 등) / <strong>3년</strong></li>
|
||||
<li><strong>전자상거래 등에서의 소비자보호에 관한 법률</strong> : 대금결제 및 재화 등의 공급에 관한 기록(회원 식별정보, 계약/청약철회 기록 등) / <strong>5년</strong></li>
|
||||
<li><strong>부가가치세법</strong> : 장부, 세금계산서, 수입세금계산서, 영수증 등(과세표준과 세액 신고자료) / <strong>5년</strong></li>
|
||||
<li><strong>전자금융거래법</strong> : 전자금융거래에 관한 기록, 상대방에 관한 정보 등 / <strong>5년</strong></li>
|
||||
</ul>
|
||||
|
||||
<h5>4. 개인정보의 제3자 제공에 관한 사항</h5>
|
||||
<p>
|
||||
① 회사는 회원의 동의가 있거나 법령의 규정에 의한 경우를 제외하고는 어떠한 경우에도 [개인정보 처리방침]에서 고지한 범위를 넘어 회원의 개인정보를 이용하거나
|
||||
타인 또는 타기업/기관에 제공하지 않습니다.
|
||||
</p>
|
||||
<p>
|
||||
② 회원의 개인정보를 제공하는 경우에는 개인정보를 제공받는 자, 개인정보 이용 목적, 제공 항목, 보유 및 이용기간을 개별적으로 고지한 후 사전 동의를 구합니다.
|
||||
</p>
|
||||
<p>다만, 다음의 경우에는 회원의 동의 없이 개인정보를 제공할 수 있습니다.</p>
|
||||
<ol>
|
||||
<li>서비스 제공에 따른 요금 정산을 위하여 필요한 경우</li>
|
||||
<li>통계작성, 학술연구 또는 시장조사를 위하여 개인을 식별할 수 없는 형태로 제공하는 경우</li>
|
||||
<li>법령에 특별한 규정이 있는 경우</li>
|
||||
</ol>
|
||||
|
||||
<p>③ 회사는 아래와 같은 경우 이용자의 동의 하에 개인정보를 제3자에게 제공할 수 있습니다.</p>
|
||||
<ol>
|
||||
<li>물품구매, 유료컨텐츠 이용 등의 배송 및 정산을 위해 이용자의 이름, 전화번호 등이 해당 유료컨텐츠 제공자, 제휴 업자에게 제공될 수 있습니다.</li>
|
||||
<li>각종 이벤트 행사에 참여한 회원의 개인정보가 해당 이벤트의 주최자에게 제공될 수 있습니다.</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
④ 회사는 회원에게 제공되는 서비스의 질을 향상시키기 위해 여러 분야의 전문 컨텐츠 사업자 혹은 비즈니스 사업자와 함께 제휴를 맺을 수 있습니다.
|
||||
</p>
|
||||
|
||||
<h5>5. 개인정보처리 위탁</h5>
|
||||
<p>
|
||||
1. 회사는 서비스 향상을 위해서 아래와 같이 개인정보를 위탁하고 있으며, 관계 법령에 따라 위탁계약 시 개인정보가 안전하게 관리될 수 있도록 필요한 사항을 규정하고 있습니다.
|
||||
</p>
|
||||
<p><strong>회사의 개인정보 위탁처리 기관 및 위탁업무 내용(요약)</strong></p>
|
||||
<ul>
|
||||
<li><strong>㈜LG U+</strong> : 신용카드 결제, 가상계좌 결제, SMS 발송 (회원탈퇴 또는 위탁계약 종료 시까지 / 법령 기준)</li>
|
||||
<li><strong>㈜다날</strong> : 결제(신용카드/가상계좌/휴대폰/계좌이체/상품권 결제), 상품권 발급 (회원탈퇴 또는 위탁계약 종료 시까지 / 법령 기준)</li>
|
||||
<li><strong>㈜더즌</strong> : 가상계좌, 계좌출금, 계좌성명조회 (회원탈퇴 또는 위탁계약 종료 시까지 / 법령 기준)</li>
|
||||
<li><strong>로움아이티</strong> : 로그인톡 인증 서비스 (별도 저장하지 않음 / 본인인증 업체 보유 정보)</li>
|
||||
<li><strong>(주)케이티엠하우스</strong> : 휴대폰번호, 상품권명 (회원 탈퇴 또는 위탁계약 종료 시)</li>
|
||||
</ul>
|
||||
|
||||
<h5>6. 정보주체의 권리·의무 및 그 행사방법</h5>
|
||||
<p>이용자는 개인정보주체로서 다음과 같은 권리를 행사할 수 있습니다.</p>
|
||||
<p><strong>(1)</strong> 정보주체는 회사에 대해 언제든지 다음 각 호의 개인정보 보호 관련 권리를 행사할 수 있습니다.</p>
|
||||
<ol>
|
||||
<li>개인정보 열람요구</li>
|
||||
<li>오류 등이 있을 경우 정정 요구</li>
|
||||
<li>삭제요구</li>
|
||||
<li>처리정지 요구</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
<strong>(2)</strong> 제1항에 따른 권리 행사는 회사에 대해 개인정보 보호법 시행령 제41조제1항 서식에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며
|
||||
회사는 이에 대해 지체 없이 조치하겠습니다.
|
||||
</p>
|
||||
|
||||
<p><strong>(3)</strong> 정보주체가 정정 또는 삭제를 요구한 경우에는 회사는 완료할 때까지 당해 개인정보를 이용하거나 제공하지 않습니다.</p>
|
||||
<p><strong>(4)</strong> 대리인을 통하여 권리 행사를 하실 수 있으며, 이 경우 위임장을 제출하셔야 합니다.</p>
|
||||
<p><strong>(5)</strong> 개인정보 열람 및 처리정지 요구는 법령에 의해 제한될 수 있습니다.</p>
|
||||
<p><strong>(6)</strong> 다른 법령에서 수집 대상으로 명시된 개인정보는 삭제를 요구할 수 없습니다.</p>
|
||||
<p><strong>(8)</strong> 회사는 정보주체 권리 행사 시 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.</p>
|
||||
|
||||
<h5>7. 개인정보 자동수집 장치의 설치, 운영 및 그 거부에 관한 사항</h5>
|
||||
<p>
|
||||
회사는 ‘쿠키(cookie)’ 등을 운용합니다. 쿠키란 웹사이트 서버가 이용자의 브라우저에 보내는 작은 텍스트 파일로서 이용자의 컴퓨터 하드디스크에 저장됩니다.
|
||||
회사는 다음과 같은 목적을 위해 암호화된 쿠키를 사용합니다.
|
||||
</p>
|
||||
|
||||
<p><strong>(1) 쿠키 등 사용 목적</strong></p>
|
||||
<p>
|
||||
회원과 비회원의 접속 빈도나 방문 시간 등을 분석, 이용자의 취향과 관심분야를 파악 및 자취 추적,
|
||||
각종 이벤트 참여 정도 및 방문 회수 파악 등을 통한 타겟 마케팅 및 개인 맞춤 서비스 제공.
|
||||
</p>
|
||||
|
||||
<p><strong>(2) 쿠키 설정 거부 방법</strong></p>
|
||||
<p>
|
||||
웹 브라우저 옵션을 통해 쿠키 허용/확인/거부를 선택할 수 있습니다.
|
||||
(예: 인터넷 익스플로어) 도구 > 인터넷 옵션 > 개인정보
|
||||
(단, 쿠키 설치를 거부하였을 경우 서비스 제공에 어려움이 있을 수 있습니다.)
|
||||
</p>
|
||||
|
||||
<h5>8. 개인정보 파기</h5>
|
||||
<p>회사는 원칙적으로 개인정보 수집 및 이용목적이 달성된 후에는 해당 정보를 지체없이 파기합니다.</p>
|
||||
<p><strong>(1) 파기절차</strong></p>
|
||||
<p>
|
||||
회원이 입력한 정보는 목적 달성 후 별도 DB로 옮겨져 내부 방침 및 관련 법령에 의한 정보보호 사유에 따라 일정 기간 저장된 후 파기됩니다.
|
||||
법률에 의한 경우가 아니고서는 다른 목적으로 이용되지 않습니다.
|
||||
</p>
|
||||
<p><strong>(2) 파기방법</strong></p>
|
||||
<ul>
|
||||
<li>전자적 파일형태로 저장된 개인정보는 기록을 재생할 수 없는 기술적 방법을 사용하여 삭제합니다.</li>
|
||||
</ul>
|
||||
|
||||
<h5>9. 개인정보 보호책임자</h5>
|
||||
<p>
|
||||
① 회사는 귀하가 좋은 정보를 안전하게 이용할 수 있도록 최선을 다하고 있습니다.
|
||||
개인정보를 보호하는데 있어 귀하께 고지한 사항들에 반하는 사고가 발생할 경우 개인정보보호책임자가 책임을 집니다.
|
||||
</p>
|
||||
<p>
|
||||
② 이용자 비밀번호의 보안유지책임은 이용자 자신에게 있습니다.
|
||||
회사는 비밀번호에 대해 어떠한 방법으로도 이용자에게 직접적으로 질문하는 경우는 없으므로 타인에게 비밀번호가 유출되지 않도록 주의하시기 바랍니다.
|
||||
</p>
|
||||
<p>
|
||||
③ 회사가 기술적인 보완조치를 했음에도 해킹 등 네트워크 위험성으로 인해 발생하는 예기치 못한 사고로 인한 정보의 훼손 및 분쟁에 관해서는 책임이 없습니다.
|
||||
</p>
|
||||
<p>④ 개인정보 보호책임자 및 담당자 연락처는 아래와 같습니다.</p>
|
||||
<ul>
|
||||
<li><strong>소속</strong> : 대표이사</li>
|
||||
<li><strong>전화번호</strong> : 1833-4856</li>
|
||||
<li><strong>성명</strong> : 송병수</li>
|
||||
<li><strong>이메일</strong> : Bestplusmakerr@gmail.com</li>
|
||||
</ul>
|
||||
|
||||
<h5>10. 정보주체의 권익침해에 대한 구제방법</h5>
|
||||
<p>아래의 기관은 당사와 별개의 기관입니다. 도움이 필요하시면 문의하여 주시기 바랍니다.</p>
|
||||
<ul>
|
||||
<li><strong>개인정보 침해신고센터(한국인터넷진흥원)</strong> : privacy.kisa.or.kr / 118 / 서울시 송파구 중대로 135</li>
|
||||
<li><strong>개인정보 분쟁조정위원회</strong> : www.kopico.go.kr / 1833-6972 / 정부서울청사 4층</li>
|
||||
<li><strong>대검찰청 사이버수사과</strong> : 1301 / www.spo.go.kr</li>
|
||||
<li><strong>경찰청 사이버안전국</strong> : 182 / http://cyberbureau.police.go.kr</li>
|
||||
</ul>
|
||||
|
||||
<h5>11. 개인정보 처리방침 고지의 의무</h5>
|
||||
<p>
|
||||
이 개인정보처리방침은 시행일로부터 적용되며, 법령 및 방침에 따른 변경내용의 추가, 삭제 및 정정이 있는 경우에는
|
||||
변경사항의 시행 7일 전부터 공지사항을 통하여 고지할 것입니다.
|
||||
</p>
|
||||
<p>본 방침은 2019년 1월 21일부터 시행됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="terms-divider"></div>
|
||||
|
||||
{{-- [필수] 14세 --}}
|
||||
<div class="terms-row">
|
||||
<label class="chk">
|
||||
<input type="checkbox" name="agree_age" id="agree_age" value="1" required>
|
||||
<span class="chk-ui" aria-hidden="true"></span>
|
||||
<span class="chk-text">
|
||||
<span class="badge badge--req">필수</span>
|
||||
<span class="terms-label">만 14세 이상입니다</span>
|
||||
<span class="terms-sub">만 14세 미만은 회원가입이 제한됩니다.</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="terms-divider"></div>
|
||||
|
||||
{{-- [선택] 마케팅 --}}
|
||||
<div class="terms-row">
|
||||
<label class="chk">
|
||||
<input type="checkbox" name="agree_marketing" id="agree_marketing" value="1">
|
||||
<span class="chk-ui" aria-hidden="true"></span>
|
||||
<span class="chk-text">
|
||||
<span class="badge badge--opt">선택</span>
|
||||
<span class="terms-label">마케팅 정보 수신 동의</span>
|
||||
<span class="terms-sub">할인/이벤트 등 혜택 안내를 받을 수 있어요.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button type="button" class="terms-view" data-toggle="t_marketing">내용보기</button>
|
||||
</div>
|
||||
<div class="terms-body" id="t_marketing" hidden>
|
||||
<div class="terms-scroll">
|
||||
<h4>마케팅 정보 수신 동의 (선택)</h4>
|
||||
|
||||
<div class="terms-legal">
|
||||
<p>
|
||||
회사는 이벤트/혜택 안내 등 마케팅 정보 제공을 위해 아래와 같이 개인정보를 이용하고,
|
||||
문자(SMS) 등으로 안내를 발송할 수 있습니다. 본 동의는 선택 사항이며, 동의하지 않아도 서비스 이용이 가능합니다.
|
||||
</p>
|
||||
|
||||
<h5>1. 수신 내용</h5>
|
||||
<ul>
|
||||
<li>이벤트/프로모션, 할인 혜택, 쿠폰, 신규 서비스 / 상품안내, 맞춤형 혜택 안내(이용 이력 기반 추천 포함 가능)</li>
|
||||
</ul>
|
||||
|
||||
<h5>2. 수신 채널</h5>
|
||||
<ul>
|
||||
<li>SMS(문자)</li>
|
||||
<li>이메일</li>
|
||||
</ul>
|
||||
|
||||
<h5>3. 이용 항목</h5>
|
||||
<ul>
|
||||
<li>휴대폰번호, 이메일, 서비스 이용 정보(혜택 제공 목적 내 최소한)</li>
|
||||
</ul>
|
||||
|
||||
<h5>4. 보유 및 이용기간</h5>
|
||||
<p>
|
||||
동의일로부터 <strong>회원 탈퇴 또는 동의 철회 시까지</strong> 보유·이용합니다.
|
||||
단, 관계 법령에 따라 보존할 필요가 있는 경우 해당 기간 동안 보관될 수 있습니다.
|
||||
</p>
|
||||
|
||||
<h5>5. 동의 철회(수신 거부)</h5>
|
||||
<p>
|
||||
마케팅 정보 수신은 언제든지 철회할 수 있습니다.
|
||||
(예: 마이페이지 > 설정/수신동의 변경, 고객센터 요청 등)
|
||||
철회 후에도 서비스 관련 필수 안내(가입/인증/결제/보안/공지 등)는 발송될 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms-help" id="terms_help" hidden>
|
||||
필수 약관에 동의해 주세요.
|
||||
</div>
|
||||
|
||||
<div class="terms-actions">
|
||||
<button type="submit" class="terms-btn terms-btn--primary" id="terms_next_btn" disabled>
|
||||
동의하고 다음
|
||||
</button>
|
||||
<a class="terms-btn" href="{{ route('web.auth.register') }}">이전</a>
|
||||
</div>
|
||||
|
||||
<p class="terms-note">
|
||||
다음 단계에서 PASS 본인인증을 진행합니다.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- 페이지 전용 스타일(이쁘게) --}}
|
||||
<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-form{display:flex;flex-direction:column;gap:12px}
|
||||
|
||||
.terms-card{border:1px solid rgba(255,255,255,.12);border-radius:16px;
|
||||
background:rgba(255,255,255,.04);box-shadow:0 12px 30px rgba(0,0,0,.18);
|
||||
overflow:hidden}
|
||||
.terms-card--all{padding:14px 14px}
|
||||
|
||||
.terms-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;
|
||||
padding:14px 14px}
|
||||
.terms-divider{height:1px;background:rgba(255,255,255,.10);margin:0 14px}
|
||||
|
||||
.terms-legal h5 { margin:14px 0 6px; font-size:12.5px; opacity:.95; }
|
||||
.terms-legal p { margin:0 0 8px; }
|
||||
.terms-legal ul, .terms-legal ol { margin:6px 0 10px; padding-left:18px; }
|
||||
.terms-legal li { margin:2px 0; opacity:.85; line-height:1.65; }
|
||||
|
||||
/* custom checkbox */
|
||||
.chk{display:flex;gap:10px;align-items:flex-start;cursor:pointer;user-select:none}
|
||||
.chk input{position:absolute;opacity:0;pointer-events:none}
|
||||
.chk-ui{width:20px;height:20px;border-radius:6px;flex:0 0 20px;margin-top:2px;
|
||||
border:1px solid rgba(255,255,255,.22);
|
||||
background:rgba(0,0,0,.15);
|
||||
box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)}
|
||||
.chk input:checked + .chk-ui{
|
||||
border-color:rgba(255,255,255,.35);
|
||||
background:rgba(255,255,255,.92);
|
||||
}
|
||||
.chk input:checked + .chk-ui::after{
|
||||
content:"";display:block;width:10px;height:6px;border-left:2px solid #111;border-bottom:2px solid #111;
|
||||
transform:rotate(-45deg);margin:5px 0 0 4px
|
||||
}
|
||||
|
||||
.chk-text{display:flex;flex-direction:column;gap:4px;line-height:1.3}
|
||||
.chk-title{font-weight:800}
|
||||
.chk-sub{opacity:.72;font-size:12px}
|
||||
.terms-label{font-weight:800}
|
||||
.terms-sub{opacity:.72;font-size:12px}
|
||||
|
||||
.badge{display:inline-flex;align-items:center;justify-content:center;
|
||||
padding:2px 8px;border-radius:999px;font-size:11px;font-weight:800;width:max-content}
|
||||
.badge--req{background:rgba(255,90,90,.18);border:1px solid rgba(255,90,90,.35)}
|
||||
.badge--opt{background:rgba(120,180,255,.16);border:1px solid rgba(120,180,255,.30);opacity:.9}
|
||||
|
||||
/* "내용보기" 버튼 */
|
||||
.terms-view{border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);
|
||||
color:inherit;border-radius:10px;padding:8px 10px;font-size:12px;cursor:pointer;opacity:.92;
|
||||
transition:transform .08s ease, opacity .08s ease, background .08s ease}
|
||||
.terms-view:hover{opacity:1;background:rgba(255,255,255,.10)}
|
||||
.terms-view:active{transform:scale(.98)}
|
||||
|
||||
/* 내용 영역: div + 스크롤 */
|
||||
.terms-body{padding:0 14px 14px}
|
||||
.terms-scroll{border:1px solid rgba(255,255,255,.10);background:rgba(0,0,0,.16);
|
||||
border-radius:12px;padding:12px;max-height:180px;overflow:auto}
|
||||
.terms-scroll h4{margin:0 0 10px;font-size:13px}
|
||||
.terms-scroll p,.terms-scroll li{opacity:.85;line-height:1.65;font-size:12.5px}
|
||||
.terms-scroll ul{margin:8px 0 0;padding-left:18px}
|
||||
|
||||
/* 안내/버튼 */
|
||||
.terms-help{padding:10px 12px;border-radius:12px;background:rgba(255,90,90,.12);
|
||||
border:1px solid rgba(255,90,90,.25);opacity:.95;font-size:12.5px}
|
||||
.terms-actions{display:flex;gap:10px;margin-top:4px}
|
||||
.terms-btn{flex:1;text-align:center;border-radius:14px;padding:12px 12px;
|
||||
border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:inherit;
|
||||
text-decoration:none;cursor:pointer;font-weight:800}
|
||||
.terms-btn--primary{background:rgba(255,255,255,.92);color:#111;border-color:rgba(255,255,255,.55)}
|
||||
.terms-btn[disabled]{opacity:.45;cursor:not-allowed}
|
||||
.terms-note{margin:0;opacity:.7;font-size:12.5px;line-height:1.5}
|
||||
|
||||
/* Primary 버튼: 기본(비활성) = 밋밋 / 활성 = 블루톤 튀게 */
|
||||
#terms_next_btn.terms-btn--primary{
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0);
|
||||
transition: transform .12s ease, box-shadow .12s ease, filter .12s ease, opacity .12s ease;
|
||||
}
|
||||
|
||||
/* 비활성(disabled)이어도 "버튼처럼" 보이게 */
|
||||
#terms_next_btn.terms-btn--primary:disabled{
|
||||
opacity: 1 !important;
|
||||
cursor: not-allowed;
|
||||
|
||||
background: rgba(255,255,255,.88) !important; /* 밝은 버튼 */
|
||||
border-color: rgba(0,0,0,.10) !important;
|
||||
|
||||
color: #7a7a7a !important; /* 검은 글자 */
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.25); /* 살짝 또렷 */
|
||||
box-shadow: 0 10px 22px rgba(0,0,0,.10) !important;
|
||||
}
|
||||
|
||||
/* disabled일 때 shine 제거 */
|
||||
#terms_next_btn.terms-btn--primary:disabled::before{
|
||||
content: none !important;
|
||||
}
|
||||
/* 활성 상태 */
|
||||
#terms_next_btn.terms-btn--primary:not(:disabled){
|
||||
background: linear-gradient(135deg, #2f6bff 0%, #4aa3ff 55%, #2fd2ff 100%);
|
||||
color: #fff;
|
||||
border-color: rgba(120,180,255,.55);
|
||||
box-shadow:
|
||||
0 14px 30px rgba(47,107,255,.28),
|
||||
0 6px 14px rgba(74,163,255,.18);
|
||||
}
|
||||
|
||||
/* shine 효과 */
|
||||
#terms_next_btn.terms-btn--primary:not(:disabled)::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
top:-30%;
|
||||
left:-40%;
|
||||
width:60%;
|
||||
height:160%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
||||
transform: rotate(18deg);
|
||||
opacity:.55;
|
||||
animation: termsShine 2.6s ease-in-out infinite;
|
||||
pointer-events:none;
|
||||
}
|
||||
@keyframes termsShine{
|
||||
0% { transform: translateX(-30%) rotate(18deg); opacity:.25; }
|
||||
45% { opacity:.65; }
|
||||
100% { transform: translateX(220%) rotate(18deg); opacity:.25; }
|
||||
}
|
||||
|
||||
/* hover/active */
|
||||
#terms_next_btn.terms-btn--primary:not(:disabled):hover{
|
||||
transform: translateY(-1px);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(47,107,255,.34),
|
||||
0 8px 18px rgba(74,163,255,.22);
|
||||
filter: saturate(1.08);
|
||||
}
|
||||
#terms_next_btn.terms-btn--primary:not(:disabled):active{
|
||||
transform: translateY(0);
|
||||
filter: saturate(1.0);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{{-- 페이지 전용 JS --}}
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('regTermsForm');
|
||||
const btn = document.getElementById('terms_next_btn');
|
||||
const help = document.getElementById('terms_help');
|
||||
|
||||
const all = document.getElementById('agree_all');
|
||||
const reqIds = ['agree_terms', 'agree_privacy', 'agree_age'];
|
||||
const optIds = ['agree_marketing'];
|
||||
|
||||
const reqEls = reqIds.map(id => document.getElementById(id));
|
||||
const optEls = optIds.map(id => document.getElementById(id));
|
||||
const allEls = [...reqEls, ...optEls];
|
||||
|
||||
function requiredOk() {
|
||||
return reqEls.every(el => el && el.checked);
|
||||
}
|
||||
|
||||
function syncAllState() {
|
||||
const checked = allEls.filter(el => el && el.checked).length;
|
||||
all.checked = (checked === allEls.length);
|
||||
all.indeterminate = (checked > 0 && checked < allEls.length);
|
||||
}
|
||||
|
||||
function syncButton() {
|
||||
const ok = requiredOk();
|
||||
btn.disabled = !ok;
|
||||
if (help) help.hidden = ok;
|
||||
}
|
||||
|
||||
// 공통 메시지(모달 우선)
|
||||
async function showAlert(msg, title='안내') {
|
||||
if (window.showMsg) return await window.showMsg(msg, { type:'alert', title });
|
||||
alert(msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
// iframe 모달 오픈/닫기 (외부 클릭으로 닫히지 않게)
|
||||
function openIframeModal(popupName = 'danal_authtel_popup', w = 420, h = 750) {
|
||||
// 이미 떠 있으면 제거 후 다시
|
||||
const old = document.getElementById(popupName);
|
||||
if (old) old.remove();
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.id = popupName;
|
||||
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>`;
|
||||
document.body.appendChild(wrap);
|
||||
|
||||
window.closeIframe = function () {
|
||||
const el = document.getElementById(popupName);
|
||||
if (el) el.remove();
|
||||
};
|
||||
|
||||
return popupName + '_iframe';
|
||||
}
|
||||
|
||||
function postToIframe(url, targetName, fieldsObj) {
|
||||
const temp = document.createElement('form');
|
||||
temp.method = 'POST';
|
||||
temp.action = url;
|
||||
temp.target = targetName;
|
||||
|
||||
// 라라벨 CSRF
|
||||
const csrf = document.createElement('input');
|
||||
csrf.type = 'hidden';
|
||||
csrf.name = '_token';
|
||||
csrf.value = "{{ csrf_token() }}";
|
||||
temp.appendChild(csrf);
|
||||
|
||||
// fields(JSON 문자열로)
|
||||
const fields = document.createElement('input');
|
||||
fields.type = 'hidden';
|
||||
fields.name = 'fields';
|
||||
fields.value = JSON.stringify(fieldsObj || {});
|
||||
temp.appendChild(fields);
|
||||
|
||||
document.body.appendChild(temp);
|
||||
temp.submit();
|
||||
temp.remove();
|
||||
}
|
||||
|
||||
// 다날 결과 메시지 수신 (iframe -> parent)
|
||||
window.addEventListener('message', async (ev) => {
|
||||
const d = ev.data || {};
|
||||
if (d.type !== 'danal_result') return;
|
||||
|
||||
// 모달 닫기
|
||||
if (typeof window.closeIframe === 'function') window.closeIframe();
|
||||
|
||||
await showAlert(d.message || (d.ok ? '본인인증이 완료되었습니다.' : '본인인증에 실패했습니다.'), d.ok ? '인증 완료' : '인증 실패');
|
||||
|
||||
if (d.redirect) {
|
||||
window.location.href = d.redirect;
|
||||
}
|
||||
});
|
||||
|
||||
// 전체 동의
|
||||
all.addEventListener('change', function () {
|
||||
allEls.forEach(el => { if (el) el.checked = all.checked; });
|
||||
syncButton();
|
||||
syncAllState();
|
||||
});
|
||||
|
||||
// 개별 변경
|
||||
allEls.forEach(el => {
|
||||
if (!el) return;
|
||||
el.addEventListener('change', function () {
|
||||
syncButton();
|
||||
syncAllState();
|
||||
});
|
||||
});
|
||||
|
||||
// 내용 보기 토글(스크롤 div)
|
||||
document.querySelectorAll('[data-toggle]').forEach(btn2 => {
|
||||
btn2.addEventListener('click', function () {
|
||||
const id = btn2.getAttribute('data-toggle');
|
||||
const panel = document.getElementById(id);
|
||||
if (!panel) return;
|
||||
|
||||
const willOpen = panel.hidden === true;
|
||||
panel.hidden = !willOpen;
|
||||
btn2.textContent = willOpen ? '닫기' : '내용보기';
|
||||
});
|
||||
});
|
||||
|
||||
// 초기
|
||||
syncButton();
|
||||
syncAllState();
|
||||
|
||||
// 제출: (기존 기능 유지) 약관 저장 + 다날 시작으로 변경
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!requiredOk()) {
|
||||
await showAlert('필수 약관에 동의해 주세요.', '안내');
|
||||
syncButton();
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
// AJAX로 약관 저장 + 다날 준비값 받기
|
||||
const body = {
|
||||
agree_terms: document.getElementById('agree_terms')?.checked ? '1' : '',
|
||||
agree_privacy: document.getElementById('agree_privacy')?.checked ? '1' : '',
|
||||
agree_age: document.getElementById('agree_age')?.checked ? '1' : '',
|
||||
agree_marketing: document.getElementById('agree_marketing')?.checked ? '1' : '',
|
||||
};
|
||||
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}",
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
if (!res.ok || data.ok === false) {
|
||||
await showAlert(data.message || '처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요ff.', '오류');
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버가 danal_ready를 내려주면 iframe 모달 실행
|
||||
if (data.reason === 'danal_ready' && data.popup && data.popup.url) {
|
||||
const targetName = openIframeModal('danal_authtel_popup', 420, 750);
|
||||
postToIframe(data.popup.url, targetName, data.popup.fields || {});
|
||||
return;
|
||||
}
|
||||
|
||||
// (혹시) 준비중/리다이렉트 방식일 때 fallback
|
||||
if (data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
await showAlert('약관 동의가 저장되었습니다.', '완료');
|
||||
|
||||
} catch (err) {
|
||||
await showAlert('네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', '오류');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
||||
|
||||
@ -69,5 +69,9 @@
|
||||
{{-- 페이지별 스크립트 추가용 --}}
|
||||
@stack('recaptcha')
|
||||
@stack('scripts')
|
||||
@include('web.partials.ui-dialog')
|
||||
|
||||
{{--@php 개발모드에서만 세션표시--}}
|
||||
@include('web.partials.dev_session_overlay')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
resources/views/web/partials/auth/danal_autosubmit.blade.php
Normal file
12
resources/views/web/partials/auth/danal_autosubmit.blade.php
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<form id="danalForm" method="post" action="{{ $action }}">
|
||||
@foreach($fields as $k => $v)
|
||||
<input type="hidden" name="{{ $k }}" value="{{ $v }}">
|
||||
@endforeach
|
||||
</form>
|
||||
<script>document.getElementById('danalForm').submit();</script>
|
||||
</body>
|
||||
</html>
|
||||
16
resources/views/web/partials/auth/danal_finish.blade.php
Normal file
16
resources/views/web/partials/auth/danal_finish.blade.php
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body>
|
||||
<script>
|
||||
try {
|
||||
window.parent && window.parent.postMessage({
|
||||
type: 'danal_result',
|
||||
ok: {{ $ok ? 'true' : 'false' }},
|
||||
message: @json($message),
|
||||
redirect: @json($redirect),
|
||||
}, '*');
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
218
resources/views/web/partials/dev_session_overlay.blade.php
Normal file
218
resources/views/web/partials/dev_session_overlay.blade.php
Normal file
@ -0,0 +1,218 @@
|
||||
{{-- resources/views/web/partials/dev_session_overlay.blade.php --}}
|
||||
@php
|
||||
// ✅ 개발 모드에서만 노출
|
||||
$show = config('app.debug') || app()->environment('local');
|
||||
|
||||
// ✅ 이 overlay 자체가 세션을 수정하는 "dev action" 처리(컨트롤러/라우트 없이)
|
||||
if ($show && request()->isMethod('post') && request()->has('_dev_sess_action')) {
|
||||
// CSRF는 web 미들웨어에 걸려있으니 토큰 포함된 요청만 처리됨.
|
||||
$action = request()->input('_dev_sess_action');
|
||||
|
||||
if ($action === 'flush') {
|
||||
session()->flush();
|
||||
session()->save();
|
||||
}
|
||||
|
||||
if ($action === 'put') {
|
||||
$k = (string) request()->input('_dev_sess_key', '');
|
||||
$v = (string) request()->input('_dev_sess_value', '');
|
||||
|
||||
// 빈 키 방지
|
||||
if ($k !== '') {
|
||||
// "a.b.c" 점 표기 지원
|
||||
data_set(session()->all(), $k, $v);
|
||||
|
||||
// data_set은 배열에만 반영되므로 세션에 직접 put
|
||||
session()->put($k, $v);
|
||||
session()->save();
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ POST 재전송 방지 + 현재 페이지로 되돌리기
|
||||
$redir = url()->current();
|
||||
$qs = request()->query();
|
||||
if (!empty($qs)) $redir .= '?' . http_build_query($qs);
|
||||
header('Location: ' . $redir, true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ✅ 세션 전체
|
||||
$sess = session()->all();
|
||||
|
||||
// ✅ 민감값 마스킹
|
||||
$maskKeys = [];
|
||||
// $maskKeys = [
|
||||
// 'password', 'passwd', 'pw', 'token', 'access_token', 'refresh_token',
|
||||
// 'api_key', 'secret', 'authorization', 'csrf', '_token',
|
||||
// 'g-recaptcha-response', 'recaptcha', 'otp',
|
||||
// 'ci', 'di', 'phone', 'mobile', 'email',
|
||||
// ];
|
||||
|
||||
$mask = function ($key, $val) use ($maskKeys) {
|
||||
$k = strtolower((string)$key);
|
||||
foreach ($maskKeys as $mk) {
|
||||
if (str_contains($k, $mk)) return '***';
|
||||
}
|
||||
return $val;
|
||||
};
|
||||
|
||||
// ✅ key:value 라인 생성(재귀)
|
||||
$lines = [];
|
||||
$dump = function ($data, $prefix = '') use (&$dump, &$lines, $mask) {
|
||||
foreach ((array)$data as $k => $v) {
|
||||
$key = $prefix . (string)$k;
|
||||
|
||||
if (is_array($v)) {
|
||||
$lines[] = $key . ' : [';
|
||||
$dump($v, $prefix . ' ');
|
||||
$lines[] = $prefix . ']';
|
||||
} else {
|
||||
if (is_bool($v)) $v = $v ? 'true' : 'false';
|
||||
if ($v === null) $v = 'null';
|
||||
|
||||
$vv = is_string($v) ? (mb_strlen($v) > 260 ? mb_substr($v, 0, 260) . '…' : $v) : (string)$v;
|
||||
$vv = $mask($k, $vv);
|
||||
|
||||
$lines[] = $key . ' : ' . $vv;
|
||||
}
|
||||
}
|
||||
};
|
||||
$dump($sess);
|
||||
$text = implode("\n", $lines);
|
||||
@endphp
|
||||
|
||||
@if($show)
|
||||
<div id="dev-session-overlay" style="
|
||||
position: fixed; left: 12px; bottom: 12px; z-index: 999999;
|
||||
width: 620px; max-width: calc(100vw - 24px);
|
||||
background: rgba(10,10,10,.92); color: #eaeaea;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
||||
font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
|
||||
">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 12px; gap:8px;">
|
||||
<div style="font-weight:800;">
|
||||
SESSION
|
||||
<span style="opacity:.65; font-weight:500;">({{ count($sess) }} keys)</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px;">
|
||||
<button type="button" id="devSessCopy" style="
|
||||
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
|
||||
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
|
||||
">Copy</button>
|
||||
<button type="button" id="devSessToggle" style="
|
||||
border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.06);
|
||||
color:#fff; padding:5px 10px; border-radius:10px; cursor:pointer;
|
||||
">Hide</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="devSessBody" style="padding:0 12px 12px 12px;">
|
||||
<div style="opacity:.7; margin-bottom:8px;">
|
||||
{{ request()->method() }} {{ request()->path() }}
|
||||
</div>
|
||||
|
||||
{{-- ✅ Controls --}}
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px;">
|
||||
<form method="POST" action="{{ route('dev.session') }}" style="display:flex; gap:6px; align-items:center; margin:0;">
|
||||
@csrf
|
||||
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
||||
<input type="hidden" name="_dev_sess_action" value="flush">
|
||||
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
||||
<button type="submit" style="
|
||||
border: 1px solid rgba(255,90,90,.35);
|
||||
background: rgba(255,90,90,.14);
|
||||
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
|
||||
" onclick="return confirm('세션을 초기화할까요? (dev 전용)');">세션 초기화</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('dev.session') }}" style="display:flex; gap:6px; align-items:center; margin:0; flex:1;">
|
||||
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
||||
@csrf
|
||||
<input type="hidden" name="_dev_sess_action" value="put">
|
||||
<input type="hidden" name="_dev_return" value="{{ url()->full() }}">
|
||||
<input name="_dev_sess_key" placeholder="key (예: register.pass_verified)" style="
|
||||
flex: 0 0 240px; max-width: 45%;
|
||||
border: 1px solid rgba(255,255,255,.16);
|
||||
background: rgba(255,255,255,.06);
|
||||
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
|
||||
">
|
||||
<input name="_dev_sess_value" placeholder="value (문자열로 저장)" style="
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid rgba(255,255,255,.16);
|
||||
background: rgba(255,255,255,.06);
|
||||
color:#fff; padding:6px 8px; border-radius:10px; outline:none;
|
||||
">
|
||||
<button type="submit" style="
|
||||
border: 1px solid rgba(90,180,255,.35);
|
||||
background: rgba(90,180,255,.14);
|
||||
color:#fff; padding:6px 10px; border-radius:10px; cursor:pointer;
|
||||
">등록</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="
|
||||
padding:10px; border-radius:12px;
|
||||
background: rgba(255,255,255,.05);
|
||||
max-height: 360px; overflow:auto; white-space: pre;
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
">
|
||||
<pre id="devSessPre" style="margin:0;">{!! e($text) !!}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const box = document.getElementById('dev-session-overlay');
|
||||
const body = document.getElementById('devSessBody');
|
||||
const toggle = document.getElementById('devSessToggle');
|
||||
const copyBtn = document.getElementById('devSessCopy');
|
||||
const pre = document.getElementById('devSessPre');
|
||||
|
||||
// 상태 기억
|
||||
const key = 'devSessOverlayCollapsed';
|
||||
const collapsed = localStorage.getItem(key) === '1';
|
||||
if (collapsed) { body.style.display='none'; toggle.textContent='Show'; }
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? '' : 'none';
|
||||
toggle.textContent = isHidden ? 'Hide' : 'Show';
|
||||
localStorage.setItem(key, isHidden ? '0' : '1');
|
||||
});
|
||||
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(pre.innerText);
|
||||
copyBtn.textContent = 'Copied';
|
||||
setTimeout(()=>copyBtn.textContent='Copy', 800);
|
||||
} catch(e) {
|
||||
alert('복사 실패(브라우저 권한 확인)');
|
||||
}
|
||||
});
|
||||
|
||||
// 드래그 이동
|
||||
let dragging=false, sx=0, sy=0, ox=0, oy=0;
|
||||
box.firstElementChild.style.cursor = 'move';
|
||||
box.firstElementChild.addEventListener('mousedown', (e)=>{
|
||||
dragging=true; sx=e.clientX; sy=e.clientY;
|
||||
const r = box.getBoundingClientRect();
|
||||
ox=r.left; oy=r.top;
|
||||
box.style.right='auto'; box.style.bottom='auto';
|
||||
document.body.style.userSelect='none';
|
||||
});
|
||||
window.addEventListener('mousemove', (e)=>{
|
||||
if(!dragging) return;
|
||||
const nx = ox + (e.clientX - sx);
|
||||
const ny = oy + (e.clientY - sy);
|
||||
box.style.left = Math.max(0, nx) + 'px';
|
||||
box.style.top = Math.max(0, ny) + 'px';
|
||||
});
|
||||
window.addEventListener('mouseup', ()=>{
|
||||
dragging=false;
|
||||
document.body.style.userSelect='';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
28
resources/views/web/partials/ui-dialog.blade.php
Normal file
28
resources/views/web/partials/ui-dialog.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
{{-- 공통 Alert/Confirm Dialog (no bootstrap dependency) --}}
|
||||
<div id="uiDialog" class="ui-dialog" aria-hidden="true">
|
||||
<div class="ui-dialog__backdrop" data-uidialog-close="1"></div>
|
||||
|
||||
<div class="ui-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="uiDialogTitle">
|
||||
<div class="ui-dialog__header">
|
||||
<h3 id="uiDialogTitle" class="ui-dialog__title">알림</h3>
|
||||
<button type="button" class="ui-dialog__x" aria-label="닫기" data-uidialog-close="1">×</button>
|
||||
</div>
|
||||
|
||||
<div class="ui-dialog__body">
|
||||
<p class="ui-dialog__message" id="uiDialogMessage"></p>
|
||||
</div>
|
||||
|
||||
<div class="ui-dialog__footer">
|
||||
<button type="button" class="ui-dialog__btn ui-dialog__btn--cancel" id="uiDialogCancel">취소</button>
|
||||
<button type="button" class="ui-dialog__btn ui-dialog__btn--ok" id="uiDialogOk">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 서버 flash로 띄우고 싶을 때: session('ui_dialog') 사용 --}}
|
||||
@if (session()->has('ui_dialog'))
|
||||
<script type="application/json" id="uiDialogFlash">
|
||||
{!! json_encode(session('ui_dialog'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) !!}
|
||||
</script>
|
||||
@endif
|
||||
|
||||
@ -58,12 +58,21 @@ Route::prefix('auth')->name('web.auth.')->group(function () {
|
||||
// 정적 페이지
|
||||
Route::view('login', 'web.auth.login')->name('login');
|
||||
|
||||
// ✅ 회원가입 Step0
|
||||
// 회원가입 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 결과 리턴
|
||||
|
||||
// Step3 회원정보 입력
|
||||
Route::get('register/profile', [RegisterController::class, 'showProfileForm'])->name('register.profile');
|
||||
Route::post('register/profile', [RegisterController::class, 'submitProfile'])->name('register.profile.submit');
|
||||
|
||||
|
||||
// 아이디 찾기 (컨트롤러)
|
||||
@ -109,4 +118,67 @@ if (app()->environment(['local', 'development', 'testing'])
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* 개발용 페이지 세션 보기*/
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
Route::post('_dev/session', function (Request $request) {
|
||||
abort_unless(config('app.debug') || app()->environment('local'), 404);
|
||||
|
||||
$action = (string) $request->input('_dev_sess_action', '');
|
||||
|
||||
// ✅ 자동 타입 변환(익명 함수라 재선언 문제 없음)
|
||||
$parse = function (string $raw) {
|
||||
$s = trim($raw);
|
||||
$lower = strtolower($s);
|
||||
|
||||
if ($lower === 'true') return true;
|
||||
if ($lower === 'false') return false;
|
||||
if ($lower === 'null') return null;
|
||||
|
||||
// 정수
|
||||
if (preg_match('/^-?\d+$/', $s)) {
|
||||
// 앞자리 0이 있는 값(예: 00123)은 문자열 유지하고 싶으면 아래 조건 추가
|
||||
// if (strlen($s) > 1 && $s[0] === '0') return $raw;
|
||||
|
||||
$int = (int) $s;
|
||||
if ((string)$int === $s) return $int;
|
||||
}
|
||||
|
||||
// 실수
|
||||
if (preg_match('/^-?\d+\.\d+$/', $s)) {
|
||||
return (float) $s;
|
||||
}
|
||||
|
||||
// JSON
|
||||
if ($s !== '' && (str_starts_with($s, '{') || str_starts_with($s, '['))) {
|
||||
$j = json_decode($s, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) return $j;
|
||||
}
|
||||
|
||||
return $raw;
|
||||
};
|
||||
|
||||
if ($action === 'flush') {
|
||||
session()->flush();
|
||||
session()->save();
|
||||
} elseif ($action === 'put') {
|
||||
$k = trim((string) $request->input('_dev_sess_key', ''));
|
||||
$raw = (string) $request->input('_dev_sess_value', '');
|
||||
|
||||
if ($k !== '') {
|
||||
session()->put($k, $parse($raw));
|
||||
session()->save();
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to((string) $request->input('_dev_return', '/'));
|
||||
})->name('dev.session');
|
||||
|
||||
/* 개발용 페이지 세션 보기*/
|
||||
|
||||
Route::fallback(fn () => abort(404));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user