diff --git a/app/Http/Controllers/Web/Auth/EmailVerificationController.php b/app/Http/Controllers/Web/Auth/EmailVerificationController.php
new file mode 100644
index 0000000..5265e9a
--- /dev/null
+++ b/app/Http/Controllers/Web/Auth/EmailVerificationController.php
@@ -0,0 +1,83 @@
+getAuthUserFromSession($request);
+ if (!$au) {
+ return redirect()->route('web.home')
+ ->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '안내',
+ 'message' => '잘못된 접근입니다.',
+ ]);
+ }
+
+ return view('web.auth.email_required', [
+ 'email' => $au['email'],
+ 'memNo' => $au['mem_no'],
+ ]);
+ }
+
+ public function sendVerify(Request $request, EmailVerificationService $svc)
+ {
+ $au = $svc->getAuthUserFromSession($request);
+ if (!$au) {
+ return redirect()->route('web.home')
+ ->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '안내',
+ 'message' => '잘못된 접근입니다.',
+ ]);
+ }
+
+ try {
+ $res = $svc->sendVerifyMail($request, (int)$au['mem_no'], (string)$au['email']);
+
+ return back()->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '안내',
+ 'message' => $res['message'] ?? '인증메일을 발송했습니다. 메일함을 확인해 주세요.',
+ ]);
+ } catch (\Throwable $e) {
+ \Log::error('Email verify send failed', [
+ 'mem_no' => $au['mem_no'],
+ 'email' => $au['email'],
+ 'error' => $e->getMessage(),
+ ]);
+ return back()->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '오류',
+ 'message' => '인증메일 발송 중 오류가 발생했습니다.',
+ ]);
+ }
+ }
+
+
+ public function verify(Request $request, EmailVerificationService $svc)
+ {
+ $memNo = (int)$request->query('mem_no', 0);
+ $k = (string)$request->query('k', '');
+
+ $res = $svc->verifySignedLink($request, $memNo, $k);
+
+ if (!($res['ok'] ?? false)) {
+ return redirect('/')->with('alert', $res['message'] ?? '잘못된 접근입니다.');
+ }
+
+ // 인증 완료 후 임시세션 제거
+ $request->session()->forget('auth_user');
+
+ return view('web.auth.email_verified', [
+ 'email' => (string)($res['email'] ?? ''),
+ 'loginUrl' => route('web.auth.login'),
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/Web/Auth/FindIdController.php b/app/Http/Controllers/Web/Auth/FindIdController.php
index c3ea323..46274b7 100644
--- a/app/Http/Controllers/Web/Auth/FindIdController.php
+++ b/app/Http/Controllers/Web/Auth/FindIdController.php
@@ -3,9 +3,10 @@
namespace App\Http\Controllers\Web\Auth;
use App\Http\Controllers\Controller;
-use App\Services\SmsService;
+use App\Repositories\Member\MemberAuthRepository;
use App\Support\LegacyCrypto\CiSeedCrypto;
-use App\Models\Member\MemInfo;
+use App\Rules\RecaptchaV3Rule;
+use App\Services\SmsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Validator;
@@ -33,37 +34,41 @@ class FindIdController extends Controller
{
logger()->info('HIT sendCode', ['path' => request()->path(), 'host' => request()->getHost()]);
- $v = Validator::make($request->all(), [
+ $rules = [
'phone' => ['required', 'string', 'max:20'],
- ], [
+ ];
+
+ if (app()->environment('production')) {
+ $rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('find_id')];
+ }
+
+ $v = Validator::make($request->all(), $rules, [
'phone.required' => '휴대폰 번호를 입력해 주세요.',
+ 'g-recaptcha-response.required' => '올바른 접근이 아닙니다.',
]);
if ($v->fails()) {
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
}
- $phoneRaw = $request->input('phone');
+ $phoneRaw = (string) $request->input('phone');
$phone = $this->normalizeKoreanPhone($phoneRaw);
+
if (!$phone) {
return response()->json(['ok' => false, 'message' => '휴대폰 번호 형식이 올바르지 않습니다.'], 422);
}
- // ✅ 0) DB에 가입된 휴대폰인지 먼저 확인
/** @var CiSeedCrypto $seed */
$seed = app(CiSeedCrypto::class);
// DB에 저장된 방식(동일)으로 암호화해서 비교
$phoneEnc = $seed->encrypt($phone);
- $exists = MemInfo::query()
- ->whereNotNull('email')
- ->where('email', '<>', '')
- ->where('cell_phone', $phoneEnc)
- ->exists();
+ $repo = app(MemberAuthRepository::class);
+ $exists = $repo->existsByEncryptedPhone($phoneEnc);
if (!$exists) {
- // ✅ 세션도 만들지 말고, 프론트가 Step1로 돌아가도록 힌트 제공
+ //\세션도 만들지 말고, 프론트가 Step1로 돌아가도록 힌트 제공
return response()->json([
'ok' => false,
'code' => 'PHONE_NOT_FOUND',
@@ -80,7 +85,7 @@ class FindIdController extends Controller
}
RateLimiter::hit($key, 600);
- // 6자리 OTP 생성
+ // 6자리 난수 생성
$code = (string) random_int(100000, 999999);
// 세션 저장(보안: 실제로는 해시 저장 권장, 여기선 간단 구현)
@@ -134,7 +139,7 @@ class FindIdController extends Controller
'message' => '인증번호를 발송했습니다.',
'expires_in' => 180,
'step' => 2,
- 'dev_code' => $isLocal ? $code : null,
+ //'dev_code' => $isLocal ? $code : null,
]);
}
@@ -189,15 +194,8 @@ class FindIdController extends Controller
// 키를 넘기지 말고, CiSeedCrypto가 생성자 주입된 userKey로 처리하게 통일
$phoneEnc = $crypto->encrypt($phone);
- $emails = MemInfo::query()
- ->whereNotNull('email')
- ->where('email', '<>', '')
- ->where('cell_phone', $phoneEnc) //DB에 저장된 암호문과 동일하게 매칭됨
- ->orderByDesc('mem_no')
- ->pluck('email')
- ->unique()
- ->values()
- ->all();
+ $repo = app(MemberAuthRepository::class);
+ $emails = $repo->getEmailsByEncryptedPhone($phoneEnc);
if (empty($emails)) {
// 운영에서는 암호문 노출 절대 금지 (지금은 디버그용이면 로그로만)
@@ -231,8 +229,6 @@ class FindIdController extends Controller
{
$digits = preg_replace('/\D+/', '', $input);
if (!$digits) return null;
-
- // 010XXXXXXXX (11), 01XXXXXXXXX (10) 정도만 허용 예시
if (Str::startsWith($digits, '010') && strlen($digits) === 11) return $digits;
if (preg_match('/^01[0-9]{8,9}$/', $digits)) return $digits; // 필요시 범위 조정
return null;
diff --git a/app/Http/Controllers/Web/Auth/FindPasswordController.php b/app/Http/Controllers/Web/Auth/FindPasswordController.php
index 8fae1b8..f8e0f39 100644
--- a/app/Http/Controllers/Web/Auth/FindPasswordController.php
+++ b/app/Http/Controllers/Web/Auth/FindPasswordController.php
@@ -1,22 +1,18 @@
session()->get('find_pw', []);
+ $sess = (array) $request->session()->get('find_pw', []);
$step = 1;
if (!empty($sess['verified'])) $step = 3;
@@ -24,238 +20,120 @@ class FindPasswordController extends Controller
return view('web.auth.find_password', [
'initialStep' => $step,
- 'email' => $sess['email'] ?? null,
+ 'email' => $sess['email'] ?? null,
+ 'name' => $sess['name'] ?? null,
]);
}
- public function sendCode(Request $request)
+ public function sendMail(Request $request, FindPasswordService $svc)
{
- $v = Validator::make($request->all(), [
+ $rules = [
'email' => ['required', 'email', 'max:120'],
- ], [
+ 'name' => ['required', 'string', 'max:50'],
+ ];
+
+ if (app()->environment('production')) {
+ $rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('find_pass')];
+ }
+
+ $v = Validator::make($request->all(), $rules, [
'email.required' => '이메일을 입력해 주세요.',
'email.email' => '이메일 형식이 올바르지 않습니다.',
+ 'name.required' => '성명을 입력해 주세요.',
+ 'g-recaptcha-response.required' => '올바른 접근이 아닙니다.',
]);
if ($v->fails()) {
- return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
- }
-
- $email = mb_strtolower(trim((string) $request->input('email')));
-
- // 0) 가입된 이메일인지 확인 (MemInfo 기준)
- $exists = MemInfo::query()
- ->whereNotNull('email')
- ->where('email', '<>', '')
- ->whereRaw('LOWER(email) = ?', [$email])
- ->exists();
-
- if (!$exists) {
return response()->json([
'ok' => false,
- 'code' => 'EMAIL_NOT_FOUND',
- 'message' => '해당 이메일로 가입된 계정을 찾을 수 없습니다. 이메일을 다시 확인해 주세요.',
- 'step' => 1,
- ], 404);
+ 'message' => $v->errors()->first(),
+ ], 422);
}
- try {
- app(MailService::class)->sendTemplate(
- $email,
- '[PIN FOR YOU] 비밀번호 재설정 인증번호',
- 'mail.legacy.noti_email_auth_1', // CI 템플릿명에 맞춰 선택
- [
- 'code' => $code,
- 'expires_min' => 3,
- 'email' => $email,
- ],
- queue: true
- );
- } catch (\Throwable $e) {
- $request->session()->forget('find_pw');
- Log::error('FindPassword sendCode failed', ['email' => $email, 'error' => $e->getMessage()]);
- return response()->json(['ok'=>false,'message'=>'인증번호 발송 중 오류가 발생했습니다.'], 500);
+ $email = (string) $request->input('email');
+ $name = (string) $request->input('name');
+
+ $res = $svc->sendResetMail($request, $email, $name, 30);
+
+ if (!($res['ok'] ?? false)) {
+ return response()->json([
+ 'ok' => false,
+ 'code' => $res['code'] ?? null,
+ 'message' => $res['message'] ?? '요청 처리 중 오류가 발생했습니다.',
+ 'step' => $res['step'] ?? 1,
+ ], (int)($res['status'] ?? 400));
}
- // 1) 레이트리밋(이메일 기준)
- $key = 'findpw:send:' . $email;
- if (RateLimiter::tooManyAttempts($key, 5)) { // 10분 5회 예시
- $sec = RateLimiter::availableIn($key);
- return response()->json(['ok' => false, 'message' => "요청이 너무 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
+ if (!empty($res['session'])) {
+ $request->session()->put('find_pw', $res['session']);
}
- RateLimiter::hit($key, 600);
- // 2) OTP 생성
- $code = (string) random_int(100000, 999999);
-
- // 3) 세션 저장 (코드는 해시로)
- $request->session()->put('find_pw', [
- 'sent' => true,
- 'email' => $email,
- 'code' => password_hash($code, PASSWORD_DEFAULT),
- 'code_expires_at' => now()->addMinutes(3)->timestamp,
- 'verified' => false,
- 'verified_at' => null,
+ return response()->json([
+ 'ok' => true,
+ 'message' => $res['message'] ?? '인증메일을 발송했습니다.',
+ 'expires_in' => $res['expires_in'] ?? 1800,
+ 'step' => $res['step'] ?? 2,
]);
+ }
- // 4) 실제 발송 연결 (메일/SMS 등)
- try {
- // TODO: 프로젝트에 맞게 구현
- // 예: Mail::to($email)->send(new PasswordOtpMail($code));
- } catch (\Throwable $e) {
- $request->session()->forget('find_pw');
- Log::error('FindPassword sendCode failed', [
- 'email' => $email,
- 'error' => $e->getMessage(),
+
+ public function verifyLink(Request $request, FindPasswordService $svc)
+ {
+ $memNo = (int) $request->query('mem_no', 0);
+
+ $res = $svc->verifyResetLink($request, $memNo);
+
+ if (!($res['ok'] ?? false)) {
+ return redirect()->route('web.auth.find_password.show')
+ ->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '안내',
+ 'message' => $res['message'] ?? '잘못된 접근입니다.',
+ ]);
+ }
+
+ // ✅ 인증완료 세션 세팅
+ if (!empty($res['session'])) {
+ $request->session()->put('find_pw', $res['session']);
+ }
+
+ return redirect()->route('web.auth.find_password.show')
+ ->with('ui_dialog', [
+ 'type' => 'alert',
+ 'title' => '안내',
+ 'message' => $res['message'] ?? '인증이 완료되었습니다.',
]);
- return response()->json([
- 'ok' => false,
- 'message' => '인증번호 발송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.',
- ], 500);
- }
-
- $isLocal = app()->environment(['local', 'development', 'testing']);
-
- return response()->json([
- 'ok' => true,
- 'message' => '인증번호를 발송했습니다.',
- 'expires_in' => 180,
- 'step' => 2,
- 'dev_code' => $isLocal ? $code : null,
- ]);
}
- public function verify(Request $request)
+ public function reset(Request $request, FindPasswordService $svc)
{
$v = Validator::make($request->all(), [
- 'code' => ['required', 'digits:6'],
- ], [
- 'code.required' => '인증번호를 입력해 주세요.',
- 'code.digits' => '인증번호 6자리를 입력해 주세요.',
- ]);
-
- if ($v->fails()) {
- return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
- }
-
- $sess = $request->session()->get('find_pw');
- if (!$sess || empty($sess['sent']) || empty($sess['email'])) {
- return response()->json(['ok' => false, 'message' => '먼저 인증번호를 요청해 주세요.'], 400);
- }
-
- if (empty($sess['code_expires_at']) || now()->timestamp > (int)$sess['code_expires_at']) {
- return response()->json(['ok' => false, 'message' => '인증번호가 만료되었습니다. 다시 요청해 주세요.'], 400);
- }
-
- // 검증 시도 레이트리밋
- $key = 'findpw:verify:' . $sess['email'];
- if (RateLimiter::tooManyAttempts($key, 10)) { // 10분 10회 예시
- $sec = RateLimiter::availableIn($key);
- return response()->json(['ok' => false, 'message' => "시도 횟수가 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
- }
- RateLimiter::hit($key, 600);
-
- $code = (string) $request->input('code');
- $ok = password_verify($code, $sess['code'] ?? '');
-
- if (!$ok) {
- return response()->json(['ok' => false, 'message' => '인증번호가 일치하지 않습니다.'], 422);
- }
-
- // 인증 성공 → step3 허용
- $sess['verified'] = true;
- $sess['verified_at'] = now()->timestamp;
- $request->session()->put('find_pw', $sess);
-
- return response()->json([
- 'ok' => true,
- 'message' => '인증이 완료되었습니다. 새 비밀번호를 설정해 주세요.',
- 'step' => 3,
- ]);
- }
-
- public function reset(Request $request)
- {
- $v = Validator::make($request->all(), [
- 'new_password' => ['required', 'string', 'min:8', 'max:72', 'confirmed'],
+ 'new_password' => [
+ 'required', 'string', 'min:8', 'max:20',
+ 'regex:/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z0-9\s]).+$/'
+ ],
+ 'new_password_confirmation' => ['required', 'same:new_password'],
], [
'new_password.required' => '새 비밀번호를 입력해 주세요.',
- 'new_password.min' => '비밀번호는 8자 이상으로 입력해 주세요.',
- 'new_password.confirmed' => '비밀번호 확인이 일치하지 않습니다.',
+ 'new_password.min' => '비밀번호는 8자리 이상이어야 합니다.',
+ 'new_password.max' => '비밀번호는 20자리를 초과할 수 없습니다.',
+ 'new_password.regex' => '비밀번호는 영문+숫자+특수문자를 포함해야 합니다.',
+ 'new_password_confirmation.same' => '비밀번호 확인이 일치하지 않습니다.',
]);
if ($v->fails()) {
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
}
- $sess = $request->session()->get('find_pw');
- if (!$sess || empty($sess['email']) || empty($sess['verified'])) {
- return response()->json(['ok' => false, 'message' => '인증이 필요합니다.'], 403);
- }
+ $pw = (string) $request->input('new_password');
- $email = (string) $sess['email'];
-
- // (선택) 인증 후 너무 오래 지나면 재인증 요구
- $verifiedAt = (int)($sess['verified_at'] ?? 0);
- if ($verifiedAt > 0 && now()->timestamp - $verifiedAt > 10 * 60) { // 10분 예시
- $request->session()->forget('find_pw');
- return response()->json(['ok' => false, 'message' => '인증이 만료되었습니다. 다시 진행해 주세요.'], 403);
- }
-
- $newPassword = (string) $request->input('new_password');
-
- // 실제 비밀번호 저장 컬럼은 프로젝트마다 다를 수 있어 안전하게 처리
- $member = MemInfo::query()
- ->whereNotNull('email')
- ->where('email', '<>', '')
- ->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])
- ->orderByDesc('mem_no')
- ->first();
-
- if (!$member) {
- $request->session()->forget('find_pw');
- return response()->json(['ok' => false, 'message' => '계정을 찾을 수 없습니다. 다시 진행해 주세요.'], 404);
- }
-
- // ✅ 여기서부터가 “진짜 저장 로직”
- // MemInfo의 실제 컬럼명에 맞게 1개만 쓰면 됩니다.
- // - password 컬럼을 쓰면 아래처럼
- // - 레거시 passwd 컬럼이면 passwd로 교체
- try {
- if (isset($member->password)) {
- $member->password = Hash::make($newPassword);
- } elseif (isset($member->passwd)) {
- $member->passwd = Hash::make($newPassword); // 레거시 규격이면 여기를 교체
- } else {
- // 컬럼을 모르면 여기서 명시적으로 막는게 안전
- return response()->json([
- 'ok' => false,
- 'message' => '비밀번호 저장 컬럼 설정이 필요합니다. (MemInfo password/passwd 확인)',
- ], 500);
- }
-
- $member->save();
- } catch (\Throwable $e) {
- Log::error('FindPassword reset failed', [
- 'email' => $email,
- 'error' => $e->getMessage(),
- ]);
- return response()->json(['ok' => false, 'message' => '비밀번호 변경 중 오류가 발생했습니다.'], 500);
- }
-
- $request->session()->forget('find_pw');
- $request->session()->save();
+ $res = $svc->resetPasswordFinal($request, $pw);
return response()->json([
- 'ok' => true,
- 'message' => '비밀번호가 변경되었습니다. 로그인해 주세요.',
- 'redirect_url' => route('web.auth.login'),
- ]);
+ 'ok' => (bool)($res['ok'] ?? false),
+ 'message' => $res['message'] ?? '',
+ 'redirect_url' => $res['redirect_url'] ?? null,
+ ], (int)($res['status'] ?? 200));
}
- public function resetSession(Request $request)
- {
- $request->session()->forget('find_pw');
- return response()->json(['ok' => true]);
- }
}
diff --git a/app/Http/Controllers/Web/Auth/LoginController.php b/app/Http/Controllers/Web/Auth/LoginController.php
index 5ad5d36..a031844 100644
--- a/app/Http/Controllers/Web/Auth/LoginController.php
+++ b/app/Http/Controllers/Web/Auth/LoginController.php
@@ -27,6 +27,8 @@ final class LoginController extends Controller
if (app()->environment('production')) {
$rules['g-recaptcha-response'] = ['required', new RecaptchaV3Rule('login')];
+ var_dump('google');
+ exit;
}
$v = Validator::make($request->all(), $rules, [
@@ -60,11 +62,25 @@ final class LoginController extends Controller
'return_url' => $returnUrl,
]);
- if (!$res['ok']) {
- // UI 처리 방식은 프로젝트 스타일에 맞춰
- // (일단 errors로 처리)
+ if (!($res['ok'] ?? false)) {
+
+ // 이메일 미인증이면 confirm 페이지로 이동
+ if (($res['reason'] ?? null) === 'email_unverified') {
+ // 세션 고정 공격 방지 (중요)
+ $request->session()->regenerate();
+ $request->session()->put('auth_user', [
+ 'mem_no' => (int)($res['mem_no'] ?? 0),
+ 'email' => (string)($res['email'] ?? $email),
+ 'issued_at' => now()->timestamp,
+ 'expires_at' => now()->addMinutes(30)->timestamp, // auth_user 세션 유효기간
+ ]);
+
+ return redirect()->route('web.auth.email.required');
+ }
+
+ // 그 외 실패는 기존 방식 유지
return back()
- ->withErrors(['login' => $res['message']])
+ ->withErrors(['login' => $res['message'] ?? '로그인에 실패했습니다.'])
->withInput(['mem_email' => $email]);
}
@@ -89,18 +105,18 @@ final class LoginController extends Controller
public function logout(Request $request)
{
- $request->session()->forget('_sess');
-
- // (선택) 회원가입/본인인증 진행 세션까지 같이 정리하고 싶으면 추가
- $request->session()->forget('signup');
- $request->session()->forget('register');
-
- // (선택) 디버그 세션 정리
- $request->session()->forget('debug');
-
- // ✅ 세션 저장
- $request->session()->save();
-
+// $request->session()->forget('_sess');
+//
+// // (선택) 회원가입/본인인증 진행 세션까지 같이 정리하고 싶으면 추가
+// $request->session()->forget('signup');
+// $request->session()->forget('register');
+//
+// // (선택) 디버그 세션 정리
+// $request->session()->forget('debug');
+//
+// $request->session()->save();
+ $request->session()->invalidate();
+ $request->session()->regenerateToken();
return redirect()->route('web.home')
->with('ui_dialog', [
'type' => 'alert',
diff --git a/app/Http/Controllers/Web/Mypage/InfoGateController.php b/app/Http/Controllers/Web/Mypage/InfoGateController.php
new file mode 100644
index 0000000..c842d27
--- /dev/null
+++ b/app/Http/Controllers/Web/Mypage/InfoGateController.php
@@ -0,0 +1,335 @@
+session()->get('mypage_gate', []);
+ $ok = (bool) ($gate['ok'] ?? false);
+ $at = (int) ($gate['at'] ?? 0);
+ $ttlSeconds = 5 * 60;
+ $isValid = $ok && $at > 0 && (time() - $at) <= $ttlSeconds;
+ if ($isValid) {
+ return redirect()->to('/mypage/info_renew');
+ }
+
+ return view('web.mypage.info.gate');
+ }
+
+
+ public function info_renew(Request $request)
+ {
+ // ✅ gate (기존 그대로)
+ $gate = (array) $request->session()->get('mypage_gate', []);
+ $gateOk = (bool) Arr::get($gate, 'ok', false);
+ $gateAt = (int) Arr::get($gate, 'at', 0);
+
+ $ttlSec = 5 * 60;
+ $nowTs = now()->timestamp;
+ $expireTs = ($gateOk && $gateAt > 0) ? ($gateAt + $ttlSec) : 0;
+ $remainSec = ($expireTs > 0) ? max(0, $expireTs - $nowTs) : 0;
+ $isGateValid = ($remainSec > 0);
+
+ // ✅ 회원정보: _sess 기반
+ $sess = (array) $request->session()->get('_sess', []);
+
+ $memberName = (string) Arr::get($sess, '_mname', '');
+ $memberEmail = (string) Arr::get($sess, '_mid', '');
+ $dtReg = (string) Arr::get($sess, '_dt_reg', '');
+
+ // ✅ 전화번호 복호 (인증 완료 상태라 마스킹 제외)
+ $rawPhone = (string) Arr::get($sess, '_mcell', '');
+ $memberPhone = (string) $this->seed->decrypt($rawPhone);
+ $user = $request->user();
+ $withdrawBankName = (string)($user->withdraw_bank_name ?? $user->bank_name ?? '');
+ $withdrawAccount = (string)($user->withdraw_account ?? $user->bank_account ?? '');
+ $hasWithdrawAccount = ($withdrawBankName !== '' && $withdrawAccount !== '');
+
+ $agreeEmail = (string)($user->agree_marketing_email ?? $user->agree_email ?? 'n');
+ $agreeSms = (string)($user->agree_marketing_sms ?? $user->agree_sms ?? 'n');
+
+ return view('web.mypage.info.renew', [
+ // gate
+ 'ttlSec' => $ttlSec,
+ 'expireTs' => (int) $expireTs,
+ 'remainSec' => (int) $remainSec,
+ 'isGateValid' => (bool) $isGateValid,
+
+ // member (sess)
+ 'memberName' => $memberName,
+ 'memberEmail' => $memberEmail,
+ 'memberPhone' => $memberPhone,
+ 'memberDtReg' => $dtReg,
+
+ // etc
+ 'hasWithdrawAccount' => (bool) $hasWithdrawAccount,
+ 'agreeEmail' => $agreeEmail,
+ 'agreeSms' => $agreeSms,
+ ]);
+ }
+
+
+ /**
+ * 재인증을 위한 세셔초기화
+ */
+ public function gateReset(Request $request)
+ {
+ // 게이트 인증 세션만 초기화
+ $request->session()->forget('mypage_gate');
+
+ // (선택) reauth도 같이 초기화하고 싶으면
+ $request->session()->forget('mypage.reauth.at');
+ $request->session()->forget('mypage.reauth.until');
+
+ $request->session()->save();
+
+ // 게이트 페이지로 이동
+ return redirect()->route('web.mypage.info.index');
+ }
+
+ /**
+ * 비밀번호 재인증 처리
+ */
+ public function verify(Request $request, MemInfoService $memInfoService)
+ {
+ $request->validate([
+ 'password' => ['required', 'string'],
+ ], [
+ 'password.required' => '비밀번호를 입력해 주세요.',
+ ]);
+
+ $sess = (array) $request->session()->get('_sess', []);
+ $isLogin = (bool) ($sess['_login_'] ?? false);
+ $email = (string) ($sess['_mid'] ?? '');
+
+ if (!$isLogin || $email === '') {
+ return redirect()->route('web.auth.login', ['return_url' => url('/mypage/info')]);
+ }
+
+ $pw = (string) $request->input('password');
+
+ $res = $memInfoService->attemptLegacyLogin([
+ 'email' => $email,
+ 'password' => $pw,
+ 'ip' => $request->ip(),
+ 'ua' => substr((string) $request->userAgent(), 0, 500),
+ 'return_url' => url('/mypage/info'),
+ ]);
+
+ $ok = (bool)($res['ok'] ?? $res['success'] ?? false);
+
+ if (!$ok) {
+ $msg = (string)($res['message'] ?? '비밀번호가 일치하지 않습니다.');
+ return back()
+ ->withErrors(['password' => $msg]) // 레이어 알림 스크립트가 이걸 잡음
+ ->withInput($request->except('password'));
+ }
+
+ // 게이트 통과 세션 (예: 30분)
+ $request->session()->put('mypage_gate', [
+ 'ok' => true,
+ 'email' => $email,
+ 'at' => time(),
+ ]);
+
+ return redirect()->route('web.mypage.info.renew');
+ }
+
+ public function passReady(Request $request)
+ {
+ // 목적 저장 (result에서 분기용)
+ $purpose = (string) $request->input('purpose', 'mypage_phone_change');
+ $request->session()->put('mypage.pass_purpose', $purpose);
+ $request->session()->save();
+
+ $danal = app(\App\Services\Danal\DanalAuthtelService::class)->prepare([
+ 'targetUrl' => route('web.mypage.info.danal.result'),
+ 'backUrl' => route('web.mypage.info.renew'), // 취소/뒤로가기
+ 'cpTitle' => request()->getHost(),
+ ]);
+
+ if (!($danal['ok'] ?? false)) {
+ return response()->json([
+ 'ok' => false,
+ 'message' => $danal['message'] ?? '본인인증 준비에 실패했습니다. 잠시 후 다시 시도해 주세요.',
+ ], 500);
+ }
+
+ // 필요하면 txid 저장 (회원가입과 동일)
+ $request->session()->put('mypage.danal', [
+ 'txid' => $danal['txid'] ?? null,
+ 'created_at' => now()->toDateTimeString(),
+ 'purpose' => $purpose,
+ ]);
+ $request->session()->save();
+
+ return response()->json([
+ 'ok' => true,
+ 'reason' => 'danal_ready',
+ 'popup' => [
+ 'url' => route('web.mypage.info.danal.start'),
+ 'fields' => $danal['fields'],
+ ],
+ ]);
+ }
+
+ public function danalStart(Request $request)
+ {
+ $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);
+ }
+
+ $action = $isMobile
+ ? 'https://wauth.teledit.com/Danal/WebAuth/Mobile/Start.php'
+ : 'https://wauth.teledit.com/Danal/WebAuth/Web/Start.php';
+
+ unset($fields['platform']);
+
+ // 기존 autosubmit 뷰 재사용 OK
+ return view('web.auth.danal_autosubmit', [
+ 'action' => $action,
+ 'fields' => $fields,
+ 'isMobile' => $isMobile,
+ ]);
+ }
+
+
+ public function danalResult(
+ Request $request,
+ DanalAuthtelService $danal,
+ MemberAuthRepository $repo
+ ) {
+ $payload = $request->all();
+
+ if (config('app.debug')) {
+ Log::info('[MYPAGE][DANAL][RESULT] keys', [
+ 'method' => $request->method(),
+ 'url' => $request->fullUrl(),
+ 'keys' => array_keys($payload),
+ ]);
+ }
+
+ $tid = (string)($payload['TID'] ?? '');
+ if ($tid === '') {
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => false,
+ 'message' => 'TID가 없습니다.',
+ 'redirect' => route('web.mypage.info.index'),
+ ]);
+ }
+
+ // CI와 동일: TID로 CONFIRM
+ $res = $danal->confirm($tid, 0, 1);
+
+ // 로그 저장 (성공/실패 무조건)
+ $logSeq = $repo->insertDanalAuthLog('M', (array) $res);
+ if ($logSeq > 0) {
+ $request->session()->put('mypage.pass.danal_log_seq', $logSeq);
+ }
+
+ $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.mypage.info.index'),
+ ]);
+ }
+
+ // ✅ 목적이 마이페이지 연락처 변경일 때만 추가 검증
+ $purpose = (string) $request->session()->get('mypage.pass_purpose', '');
+ if ($purpose === 'mypage_phone_change') {
+
+ $sess = (array) $request->session()->get('_sess', []);
+ $memNo = (int) ($sess['_mno'] ?? 0);
+
+ if ($memNo <= 0) {
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => false,
+ 'message' => '로그인 정보가 확인되지 않습니다. 다시 로그인 후 시도해 주세요.',
+ 'redirect' => route('web.auth.login'),
+ ]);
+ }
+
+ $svc = app(\App\Services\MypageInfoService::class);
+
+ //연락처 검증
+ $check = $svc->validatePassPhoneChange($sess, (array) $res);
+ if (!($check['ok'] ?? false)) {
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => false,
+ 'message' => $check['message'] ?? '연락처 변경 검증에 실패했습니다.',
+ 'redirect' => route('web.mypage.info.index'),
+ ]);
+ }
+
+ //전화번호 저장
+ $check = $svc->commitPhoneChange($memNo, (array) $res);
+ if (!($check['ok'] ?? false)) {
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => false,
+ 'message' => $check['message'] ?? '연락처 저장에 실패했습니다.',
+ 'redirect' => route('web.mypage.info.index'),
+ ]);
+ }
+
+ $request->session()->put('_sess._mcell', $check['_cell'] ?? ''); //전화번호 변경
+
+ }else{
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => false,
+ 'message' => $check['message'] ?? '연락처 변경에 실패했습니다.\n\n관리자에게 문의하세요!',
+ 'redirect' => route('web.mypage.info.index'),
+ ]);
+ }
+
+ // 성공: 마이페이지 인증 플래그 세션 저장
+ $request->session()->forget('mypage.pass');
+ $request->session()->forget('mypage.danal ');
+
+ $request->session()->save();
+
+ return response()->view('web.auth.danal_finish_top', [
+ 'ok' => true,
+ 'message' => '본인인증이 완료되었습니다.',
+ 'redirect' => url('/mypage/info_renew'),
+ ]);
+ }
+
+
+}
diff --git a/app/Mail/TemplateMail.php b/app/Mail/TemplateMail.php
index 7d08c57..330e978 100644
--- a/app/Mail/TemplateMail.php
+++ b/app/Mail/TemplateMail.php
@@ -2,14 +2,12 @@
namespace App\Mail;
-use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Contracts\Queue\ShouldQueue;
-class TemplateMail extends Mailable implements ShouldQueue
+class TemplateMail extends Mailable
{
- use Queueable, SerializesModels;
+ use SerializesModels;
public function __construct(
public string $subjectText,
diff --git a/app/Models/Member/MemStRing.php b/app/Models/Member/MemStRing.php
deleted file mode 100644
index 0a36a54..0000000
--- a/app/Models/Member/MemStRing.php
+++ /dev/null
@@ -1,34 +0,0 @@
-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;
- }
-}
diff --git a/app/Repositories/Member/EmailAuthRepository.php b/app/Repositories/Member/EmailAuthRepository.php
new file mode 100644
index 0000000..80ea6c0
--- /dev/null
+++ b/app/Repositories/Member/EmailAuthRepository.php
@@ -0,0 +1,208 @@
+ ..., 'auth_key' => ..., 'expires_at' => 'Y-m-d H:i:s']
+ */
+ public function prepareEmailVerify(int $memNo, string $email, string $ip, int $expiresMinutes = 30): array
+ {
+ return DB::transaction(function () use ($memNo, $email, $ip, $expiresMinutes) {
+
+ // 1) mem_auth: row lock (없으면 생성, 있으면 N으로 초기화)
+ $row = DB::table('mem_auth')
+ ->where('mem_no', $memNo)
+ ->where('auth_type', 'email')
+ ->lockForUpdate()
+ ->first();
+
+ $today = now()->toDateString();
+
+ if (!$row) {
+ DB::table('mem_auth')->insert([
+ 'mem_no' => $memNo,
+ 'auth_type' => 'email',
+ 'auth_state' => 'N',
+ 'auth_date' => $today,
+ ]);
+ } else {
+ DB::table('mem_auth')
+ ->where('mem_no', $memNo)
+ ->where('auth_type', 'email')
+ ->update([
+ 'auth_state' => 'N',
+ 'auth_date' => $today,
+ ]);
+ }
+
+ // 2) auth_key 생성
+ $authKey = now()->format('HisYmd') . $ip . '-' . bin2hex(random_bytes(8));
+ $expiresAt = now()->addMinutes($expiresMinutes)->format('Y-m-d H:i:s');
+
+ $emailInfo = [
+ 'type' => 'mem_level',
+ 'auth_hit' => 'n',
+ 'user_email' => $email,
+ 'auth_key' => $authKey,
+ 'auth_effective_time' => $expiresAt,
+ 'redate' => now()->format('Y-m-d H:i:s'),
+ ];
+
+ // 3) mem_auth_log: P (Pending)
+ DB::table('mem_auth_log')->insert([
+ 'mem_no' => $memNo,
+ 'type' => 'email',
+ 'state' => 'P',
+ 'info' => json_encode($emailInfo, JSON_UNESCAPED_UNICODE),
+ 'rgdate' => now()->format('Y-m-d H:i:s'),
+ ]);
+
+ // 4) mem_auth_info upsert (auth_info JSON)
+ $authInfoRow = DB::table('mem_auth_info')
+ ->where('mem_no', $memNo)
+ ->lockForUpdate()
+ ->first();
+
+ if (!$authInfoRow) {
+ DB::table('mem_auth_info')->insert([
+ 'mem_no' => $memNo,
+ 'auth_info' => json_encode(['email' => $emailInfo], JSON_UNESCAPED_UNICODE),
+ ]);
+ } else {
+ $current = json_decode((string)$authInfoRow->auth_info, true) ?: [];
+ $current['email'] = $emailInfo;
+
+ DB::table('mem_auth_info')
+ ->where('mem_no', $memNo)
+ ->update([
+ 'auth_info' => json_encode($current, JSON_UNESCAPED_UNICODE),
+ ]);
+ }
+
+ return [
+ 'email' => $email,
+ 'auth_key' => $authKey,
+ 'expires_at' => $expiresAt,
+ ];
+ });
+ }
+
+ /**
+ * 인증 완료 처리 (CI3 로직 동일):
+ * - 이미 Y면 예외
+ * - auth_info.email 존재/키/시간 체크
+ * - mem_auth: Y 업데이트
+ * - mem_auth_info: auth_hit=y + remote/agent/시간 merge
+ * - mem_auth_log: S insert
+ */
+ public function confirmEmailVerify(int $memNo, string $encKey, string $ip, string $agent): array
+ {
+ return DB::transaction(function () use ($memNo, $encKey, $ip, $agent) {
+
+ // 1) mem_auth 상태 확인 (lock)
+ $auth = DB::table('mem_auth')
+ ->where('mem_no', $memNo)
+ ->where('auth_type', 'email')
+ ->lockForUpdate()
+ ->first();
+
+ if ($auth && $auth->auth_state === 'Y') {
+ return ['ok' => false, 'message' => '이미 인증이 완료되었습니다.'];
+ }
+
+ // 2) mem_auth_info 가져오기 (lock)
+ $infoRow = DB::table('mem_auth_info')
+ ->where('mem_no', $memNo)
+ ->lockForUpdate()
+ ->first();
+
+ if (!$infoRow || empty($infoRow->auth_info)) {
+ return ['ok' => false, 'message' => '잘못된 접근입니다.'];
+ }
+
+ $authJson = json_decode((string)$infoRow->auth_info, true);
+ $emailAuth = $authJson['email'] ?? null;
+
+ if (!$emailAuth || empty($emailAuth['auth_hit'])) {
+ return ['ok' => false, 'message' => '정상적인 경로로 이용하세요.'];
+ }
+ if (($emailAuth['auth_hit'] ?? '') === 'y') {
+ return ['ok' => false, 'message' => '이미 인증되었습니다.'];
+ }
+
+ if (($emailAuth['auth_key'] ?? '') !== $encKey) {
+ return ['ok' => false, 'message' => '잘못된 접근입니다.'];
+ }
+
+ $effective = (string)($emailAuth['auth_effective_time'] ?? '');
+ if ($effective === '' || $effective < now()->format('Y-m-d H:i:s')) {
+ return ['ok' => false, 'message' => '인증시간이 초과되었습니다.'];
+ }
+
+ // 3) auth_hit = y + merge info
+ $emailAuth['auth_hit'] = 'y';
+ $emailAuth = array_merge($emailAuth, [
+ 'remote_addr' => $ip,
+ 'agent' => $agent,
+ 'auth_redate' => now()->format('Y-m-d H:i:s'),
+ ]);
+
+ $authJson['email'] = $emailAuth;
+
+ // 4) mem_auth: Y 업데이트 (없으면 insert)
+ $today = now()->toDateString();
+ if (!$auth) {
+ DB::table('mem_auth')->insert([
+ 'mem_no' => $memNo,
+ 'auth_type' => 'email',
+ 'auth_state' => 'Y',
+ 'auth_date' => $today,
+ ]);
+ } else {
+ DB::table('mem_auth')
+ ->where('mem_no', $memNo)
+ ->where('auth_type', 'email')
+ ->update([
+ 'auth_state' => 'Y',
+ 'auth_date' => $today,
+ ]);
+ }
+
+ // 5) mem_auth_info 업데이트
+ DB::table('mem_auth_info')
+ ->where('mem_no', $memNo)
+ ->update([
+ 'auth_info' => json_encode($authJson, JSON_UNESCAPED_UNICODE),
+ ]);
+
+ // 6) mem_auth_log: S (Success)
+ $logInfo = [
+ 'remote_addr' => $ip,
+ 'agent' => $agent,
+ 'auth_redate' => now()->format('Y-m-d H:i:s'),
+ ];
+
+ DB::table('mem_auth_log')->insert([
+ 'mem_no' => $memNo,
+ 'type' => 'email',
+ 'state' => 'S',
+ 'info' => json_encode($logInfo, JSON_UNESCAPED_UNICODE),
+ 'rgdate' => now()->format('Y-m-d H:i:s'),
+ ]);
+
+ return [
+ 'ok' => true,
+ 'message' => '이메일 인증이 확인되었습니다.',
+ 'email' => (string)($emailAuth['user_email'] ?? ''),
+ ];
+ });
+ }
+}
diff --git a/app/Repositories/Member/MemInfoRepository.php b/app/Repositories/Member/MemInfoRepository.php
new file mode 100644
index 0000000..ee47a03
--- /dev/null
+++ b/app/Repositories/Member/MemInfoRepository.php
@@ -0,0 +1,21 @@
+where('mem_no', $memNo)->first();
+ }
+
+ public function emailsMatch(int $memNo, string $email): bool
+ {
+ $mem = $this->findByMemNo($memNo);
+ if (!$mem || empty($mem->email)) return false;
+
+ return strcasecmp((string)$mem->email, $email) === 0;
+ }
+}
diff --git a/app/Repositories/Member/MemStRingRepository.php b/app/Repositories/Member/MemStRingRepository.php
index 56c2bbd..0a36a54 100644
--- a/app/Repositories/Member/MemStRingRepository.php
+++ b/app/Repositories/Member/MemStRingRepository.php
@@ -1,64 +1,34 @@
toDateTimeString();
+ $col = 'str_' . (int)$set; // str_0 / str_1 / str_2
- [$str0, $str1, $str2] = CiPassword::makeAll($plainPassword);
- $pass2 = CiPassword::makePass2($pin2);
+ $row = DB::table('mem_st_ring')
+ ->select($col)
+ ->where('mem_no', $memNo)
+ ->first();
- 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,
- ]
- );
+ if (!$row) return null;
+
+ $val = $row->{$col} ?? null;
+ $val = is_string($val) ? trim($val) : null;
+
+ return $val !== '' ? $val : null;
}
- /**
- * CI3 modify_type == "1_passwd" 대응 (1차만)
- */
- public function upsertPassword1(int $memNo, string $plainPassword, ?string $dtNow = null): void
+ public function getLegacyPass2(int $memNo): ?string
{
- $dt = $dtNow ?: Carbon::now()->toDateTimeString();
- [$str0, $str1, $str2] = CiPassword::makeAll($plainPassword);
+ $val = DB::table('mem_st_ring')
+ ->where('mem_no', $memNo)
+ ->value('passwd2');
- 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]
- );
+ $val = is_string($val) ? trim($val) : null;
+ return $val !== '' ? $val : null;
}
}
diff --git a/app/Repositories/Member/MemberAuthRepository.php b/app/Repositories/Member/MemberAuthRepository.php
index 0b7baf9..13a0266 100644
--- a/app/Repositories/Member/MemberAuthRepository.php
+++ b/app/Repositories/Member/MemberAuthRepository.php
@@ -206,16 +206,16 @@ class MemberAuthRepository
// 2) 이미 회원인지 체크
$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, // ✅ 필요시
-// ]);
-// }
+ 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);
@@ -601,6 +601,92 @@ class MemberAuthRepository
]);
}
+ public function findByEmailAndName(string $emailLower, string $name): ?MemInfo
+ {
+ // ⚠️ 성명 컬럼이 name이 아니면 여기만 바꾸면 됨
+ return MemInfo::query()
+ ->whereNotNull('email')->where('email', '<>', '')
+ ->whereRaw('LOWER(email) = ?', [$emailLower])
+ ->where('name', $name)
+ ->orderByDesc('mem_no')
+ ->first();
+ }
+
+ public function findByMemNo(int $memNo): ?MemInfo
+ {
+ return MemInfo::query()
+ ->where('mem_no', $memNo)
+ ->first();
+ }
+
+ public function existsByEncryptedPhone(string $phoneEnc): bool
+ {
+ return DB::table('mem_info')
+ ->whereNotNull('email')
+ ->where('email', '<>', '')
+ ->where('cell_phone', $phoneEnc)
+ ->exists();
+ }
+
+ /**
+ * 해당 암호화 휴대폰으로 가입된 이메일 목록(중복 제거)
+ */
+ public function getEmailsByEncryptedPhone(string $phoneEnc): array
+ {
+ // 여러 mem_no에서 같은 email이 나올 수 있으니 distinct 처리
+ return DB::table('mem_info')
+ ->select('email')
+ ->whereNotNull('email')
+ ->where('email', '<>', '')
+ ->where('cell_phone', $phoneEnc)
+ ->orderByDesc('mem_no')
+ ->distinct()
+ ->pluck('email')
+ ->values()
+ ->all();
+ }
+
+ //비밀번호 수정
+ public function updatePasswordOnly(int $memNo, string $pwPlain): void
+ {
+ if ($memNo <= 0 || $pwPlain === '') {
+ throw new \InvalidArgumentException('invalid_payload');
+ }
+
+ [$str0, $str1, $str2] = CiPassword::makeAll($pwPlain);
+ $now = now()->format('Y-m-d H:i:s');
+
+ $affected = DB::table('mem_st_ring')
+ ->where('mem_no', $memNo)
+ ->update([
+ 'str_0' => $str0,
+ 'str_1' => $str1,
+ 'str_2' => $str2,
+ 'dt_reg' => $now,
+ ]);
+
+ // ✅ 기존회원인데 mem_st_ring row가 없다? 이건 데이터 이상으로 보고 명확히 실패 처리
+ if ($affected <= 0) {
+ throw new \RuntimeException('mem_st_ring_not_found');
+ }
+ }
+
+
+ public function logPasswordResetSuccess(int $memNo, string $ip, string $agent): void
+ {
+ $now = now()->format('Y-m-d H:i:s');
+ DB::table('mem_passwd_modify')->insert([
+ 'state' => 'E',
+ 'info' => json_encode([
+ 'mem_no' => (string)$memNo,
+ 'redate' => $now,
+ 'remote_addr' => $ip,
+ 'agent' => $agent,
+ ], JSON_UNESCAPED_UNICODE),
+ 'rgdate' => $now,
+ ]);
+ }
+
}
diff --git a/app/Repositories/Mypage/MypageInfoRepository.php b/app/Repositories/Mypage/MypageInfoRepository.php
new file mode 100644
index 0000000..1aae14c
--- /dev/null
+++ b/app/Repositories/Mypage/MypageInfoRepository.php
@@ -0,0 +1,74 @@
+select([
+ self::COL_MEM_NO,
+ self::COL_CI,
+ self::COL_CELL,
+ ])
+ ->where(self::COL_MEM_NO, $memNo)
+ ->first();
+ }
+
+ /**
+ * mem_info.ci 조회
+ */
+ public function getMemberCi(int $memNo): string
+ {
+ $row = DB::table(self::TABLE_MEM_INFO)
+ ->select([self::COL_CI])
+ ->where(self::COL_MEM_NO, $memNo)
+ ->first();
+
+ return (string) ($row->{self::COL_CI} ?? '');
+ }
+
+ /**
+ * 암호화된 휴대폰(cell) 값이 이미 존재하는지 체크
+ * - excludeMemNo가 있으면 해당 회원은 제외하고 검색
+ */
+ public function existsEncryptedCell(string $encCell, ?int $excludeMemNo = null): bool
+ {
+ if ($encCell === '') return false;
+
+ $q = DB::table(self::TABLE_MEM_INFO)
+ ->where(self::COL_CELL, $encCell);
+
+ if ($excludeMemNo !== null && $excludeMemNo > 0) {
+ $q->where(self::COL_MEM_NO, '!=', $excludeMemNo);
+ }
+
+ return $q->exists();
+ }
+
+ /**
+ * mem_info.cell 업데이트
+ * @return int 영향을 받은 row 수
+ */
+ public function updateEncryptedCell(int $memNo, string $encCell): int
+ {
+ return DB::table(self::TABLE_MEM_INFO)
+ ->where(self::COL_MEM_NO, $memNo)
+ ->update([
+ self::COL_CELL => $encCell,
+ ]);
+ }
+}
diff --git a/app/Services/EmailVerificationService.php b/app/Services/EmailVerificationService.php
new file mode 100644
index 0000000..ca0c13e
--- /dev/null
+++ b/app/Services/EmailVerificationService.php
@@ -0,0 +1,78 @@
+session()->get('auth_user', []);
+
+ $memNo = (int)($au['mem_no'] ?? 0);
+ $email = (string)($au['email'] ?? '');
+ $expiresAt = (int)($au['expires_at'] ?? 0);
+
+ if ($memNo <= 0 || $email === '' || $expiresAt <= 0 || now()->timestamp > $expiresAt) {
+ $request->session()->forget('auth_user');
+ return [];
+ }
+
+ return ['mem_no' => $memNo, 'email' => $email, 'expires_at' => $expiresAt];
+ }
+
+ public function sendVerifyMail(Request $request, int $memNo, string $email): array
+ {
+ $prep = $this->emailAuthRepo->prepareEmailVerify(
+ $memNo,
+ $email,
+ (string)$request->ip(),
+ 30
+ );
+
+ $link = URL::temporarySignedRoute(
+ 'web.auth.email.verify',
+ now()->addMinutes(30),
+ [
+ 'mem_no' => $memNo,
+ 'k' => $prep['auth_key'],
+ ]
+ );
+
+ $this->mail->sendTemplate(
+ $email,
+ '[PIN FOR YOU] 이메일 인증을 완료해 주세요',
+ 'mail.auth.verify_email',
+ [
+ 'email' => $email,
+ 'link' => $link,
+ 'expires_min' => 30,
+ 'accent' => '#E4574B',
+ 'brand' => 'PIN FOR YOU',
+ 'siteUrl' => config('app.url'),
+ ],
+ queue: false
+ );
+
+ return ['ok' => true, 'status' => 200, 'message' => '인증메일을 발송했습니다. 메일함을 확인해 주세요.'];
+ }
+
+ public function verifySignedLink(Request $request, int $memNo, string $k): array
+ {
+ return $this->emailAuthRepo->confirmEmailVerify(
+ $memNo,
+ $k,
+ (string)$request->ip(),
+ (string)$request->userAgent()
+ );
+ }
+}
diff --git a/app/Services/FindPasswordService.php b/app/Services/FindPasswordService.php
new file mode 100644
index 0000000..09af488
--- /dev/null
+++ b/app/Services/FindPasswordService.php
@@ -0,0 +1,246 @@
+ip();
+ if (RateLimiter::tooManyAttempts($key, 5)) {
+ $sec = RateLimiter::availableIn($key);
+ return [
+ 'ok' => false,
+ 'status' => 429,
+ 'message' => "요청이 너무 많습니다. {$sec}초 후 다시 시도해 주세요.",
+ ];
+ }
+ RateLimiter::hit($key, 600);
+
+ // ✅ 가입자 확인(DB)
+ $member = $this->members->findByEmailAndName($emailLower, $name);
+ if (!$member) {
+ return [
+ 'ok' => false,
+ 'status' => 404,
+ 'code' => 'NOT_MATCHED',
+ 'message' => '입력하신 이메일/성명 정보가 일치하지 않습니다.',
+ 'step' => 1,
+ ];
+ }
+
+ $memNo = (int)$member->mem_no;
+
+ // ✅ DB 저장 없이 signed URL만 생성
+ $nonce = bin2hex(random_bytes(10));
+
+ $link = URL::temporarySignedRoute(
+ 'web.auth.find_password.verify',
+ now()->addMinutes($expiresMinutes),
+ [
+ 'mem_no' => $memNo,
+ 'n' => $nonce,
+ ]
+ );
+
+ // ✅ 메일 발송
+ try {
+ $this->mail->sendTemplate(
+ $emailLower,
+ '[PIN FOR YOU] 비밀번호 재설정 링크 안내',
+ 'mail.auth.reset_password',
+ [
+ 'email' => $emailLower,
+ 'name' => $name,
+ 'link' => $link,
+ 'expires_min' => $expiresMinutes,
+ 'accent' => '#E4574B',
+ 'brand' => 'PIN FOR YOU',
+ 'siteUrl' => config('app.url'),
+ ],
+ queue: false
+ );
+ } catch (\Throwable $e) {
+ Log::error('FindPassword sendResetMail failed', [
+ 'mem_no' => $memNo,
+ 'email' => $emailLower,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return [
+ 'ok' => false,
+ 'status' => 500,
+ 'message' => '메일 발송 중 오류가 발생했습니다.',
+ ];
+ }
+
+ // 컨트롤러가 세션에 저장할 payload를 서비스에서 만들어줌
+ return [
+ 'ok' => true,
+ 'status' => 200,
+ 'message' => '재설정 메일을 발송했습니다. 메일함을 확인해 주세요.',
+ 'expires_in' => $expiresMinutes * 60,
+ 'step' => 2,
+ 'session' => [
+ 'sent' => true,
+ 'verified' => false,
+ 'mem_no' => $memNo,
+ 'email' => $emailLower,
+ 'name' => $name,
+ 'sent_at' => now()->timestamp,
+ ],
+ ];
+ }
+
+ /**
+ * 링크 클릭 검증(서명/만료 + 회원존재)
+ * - DB 조회는 여기서 처리
+ * - 컨트롤러는 redirect만 담당
+ */
+ public function verifyResetLink(Request $request, int $memNo): array
+ {
+ // signed 미들웨어를 라우트에 붙이면 여기 체크는 사실상 안전장치
+ if (!$request->hasValidSignature()) {
+ return [
+ 'ok' => false,
+ 'message' => '링크가 유효하지 않거나 만료되었습니다. 다시 진행해 주세요.',
+ ];
+ }
+
+ if ($memNo <= 0) {
+ return [
+ 'ok' => false,
+ 'message' => '잘못된 접근입니다.',
+ ];
+ }
+
+ $member = $this->members->findByMemNo($memNo);
+ if (!$member) {
+ return [
+ 'ok' => false,
+ 'message' => '계정을 찾을 수 없습니다. 다시 진행해 주세요.',
+ ];
+ }
+
+ return [
+ 'ok' => true,
+ 'message' => '인증이 완료되었습니다. 새 비밀번호를 설정해 주세요.',
+ 'session' => [
+ 'sent' => true,
+ 'verified' => true,
+ 'verified_at' => now()->timestamp,
+ 'mem_no' => $memNo,
+ 'email' => (string)($member->email ?? ''),
+ 'name' => (string)($member->name ?? ''),
+ ],
+ ];
+ }
+
+ public function resetPasswordFinal(Request $request, string $newPassword): array
+ {
+ $sess = (array) $request->session()->get('find_pw', []);
+
+ // 0) 세션 체크: 이메일 링크 인증 완료 상태인지
+ if (empty($sess['verified']) || empty($sess['mem_no'])) {
+ return [
+ 'ok' => false,
+ 'status' => 403,
+ 'message' => '이메일 인증이 필요합니다. 메일 링크를 통해 다시 진행해 주세요.',
+ ];
+ }
+
+ $memNo = (int) ($sess['mem_no'] ?? 0);
+ if ($memNo <= 0) {
+ $request->session()->forget('find_pw');
+ return [
+ 'ok' => false,
+ 'status' => 403,
+ 'message' => '세션 정보가 올바르지 않습니다. 다시 진행해 주세요.',
+ ];
+ }
+
+ // 1) 인증 후 유효시간 정책 (예: 인증 후 10분 내 변경)
+ $verifiedAt = (int) ($sess['verified_at'] ?? 0);
+ if ($verifiedAt <= 0 || (now()->timestamp - $verifiedAt) > (10 * 60)) {
+ $request->session()->forget('find_pw');
+ return [
+ 'ok' => false,
+ 'status' => 403,
+ 'message' => '인증이 만료되었습니다. 비밀번호 찾기를 다시 진행해 주세요.',
+ ];
+ }
+
+ // 2) 레이트리밋 (비번 변경 시도)
+ $key = 'findpw:reset:' . $memNo . ':' . (string)$request->ip();
+ if (RateLimiter::tooManyAttempts($key, 10)) {
+ $sec = RateLimiter::availableIn($key);
+ return [
+ 'ok' => false,
+ 'status' => 429,
+ 'message' => "시도 횟수가 많습니다. {$sec}초 후 다시 시도해 주세요.",
+ ];
+ }
+ RateLimiter::hit($key, 600);
+
+ // 3) 방어적 비번 정책 체크(컨트롤러에서 검증하지만 한번 더)
+ if (!is_string($newPassword) || $newPassword === '') {
+ return ['ok'=>false,'status'=>422,'message'=>'새 비밀번호를 입력해 주세요.'];
+ }
+ if (strlen($newPassword) < 8 || strlen($newPassword) > 20) {
+ return ['ok'=>false,'status'=>422,'message'=>'비밀번호는 8~20자리로 입력해 주세요.'];
+ }
+ if (!preg_match('/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z0-9\s]).+$/', $newPassword)) {
+ return ['ok'=>false,'status'=>422,'message'=>'비밀번호는 영문+숫자+특수문자를 포함해야 합니다.'];
+ }
+
+ // 4) 회원 존재 확인 (DB는 Repo 통해서)
+ $member = $this->members->findByMemNo($memNo);
+ if (!$member) {
+ $request->session()->forget('find_pw');
+ return [
+ 'ok' => false,
+ 'status' => 404,
+ 'message' => '계정을 찾을 수 없습니다. 다시 진행해 주세요.',
+ ];
+ }
+
+ // 5) 비밀번호 저장 (mem_st_ring str_0~2) + 성공 로그
+ $this->members->updatePasswordOnly($memNo, $newPassword);
+ $this->members->logPasswordResetSuccess(
+ $memNo,
+ (string) $request->ip(),
+ (string) $request->userAgent()
+ );
+
+ // 6) 세션 정리
+ $request->session()->forget('find_pw');
+ $request->session()->save();
+
+ return [
+ 'ok' => true,
+ 'status' => 200,
+ 'message' => '비밀번호가 변경되었습니다. 로그인해 주세요.',
+ 'redirect_url' => route('web.auth.login'),
+ ];
+ }
+}
diff --git a/app/Services/MailService.php b/app/Services/MailService.php
index 0c430a7..9e88ea7 100644
--- a/app/Services/MailService.php
+++ b/app/Services/MailService.php
@@ -2,23 +2,51 @@
namespace App\Services;
-use Illuminate\Support\Facades\Mail;
use App\Mail\TemplateMail;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
class MailService
{
- /**
- * CI macro.sendmail 같은 역할
- * @param string|array $to
- */
- public function sendTemplate($to, string $subject, string $view, array $data = []): void
+ public function sendTemplate($to, string $subject, string $view, array $data = [], bool $queue = false): void
{
$toList = is_array($to) ? $to : [$to];
+ // ✅ 웹 요청 기준으로 실제 mail 설정을 로그로 남김
+ Log::info('mail_send_debug', [
+ 'queue' => $queue,
+ 'queue_default' => config('queue.default'),
+ 'mail_default' => config('mail.default'),
+ 'smtp' => [
+ 'host' => config('mail.mailers.smtp.host'),
+ 'port' => config('mail.mailers.smtp.port'),
+ 'encryption' => config('mail.mailers.smtp.encryption'),
+ 'timeout' => config('mail.mailers.smtp.timeout'),
+ 'local_domain' => config('mail.mailers.smtp.local_domain'),
+ ],
+ 'from' => config('mail.from'),
+ ]);
+
foreach ($toList as $toEmail) {
- Mail::send($view, $data, function ($m) use ($toEmail, $subject) {
- $m->to($toEmail)->subject($subject);
- });
+ $mailable = new TemplateMail(
+ subjectText: $subject,
+ viewName: $view,
+ payload: $data
+ );
+
+ Log::info('mail_send_attempt', [
+ 'to' => $toEmail,
+ 'subject' => $subject,
+ 'view' => $view,
+ ]);
+
+ if ($queue) {
+ Mail::to($toEmail)->queue($mailable);
+ Log::info('mail_send_queued', ['to' => $toEmail]);
+ } else {
+ Mail::to($toEmail)->send($mailable);
+ Log::info('mail_send_done', ['to' => $toEmail]);
+ }
}
}
}
diff --git a/app/Services/MemInfoService.php b/app/Services/MemInfoService.php
index ebe31c4..5bc54bc 100644
--- a/app/Services/MemInfoService.php
+++ b/app/Services/MemInfoService.php
@@ -408,7 +408,7 @@ class MemInfoService
$this->insertLoginLog($yearTable, $log);
if ($failCnt >= 4) {
- return ['ok'=>false, 'message'=>"5회이상 실패시 비밀번호찾기 후 이용 바랍니다.(클릭)"];
+ return ['ok'=>false, 'message'=>"비밀번호 입력 5회이상 실패 하셨습니다.\n 비밀번호찾기 후 이용 바랍니다."];
}
return ['ok'=>false, 'message'=>"비밀번호가 일치하지 않습니다.\n비밀번호 실패횟수 : ".($failCnt+1)."\n5회 이상 실패시 인증을 다시받아야 합니다."];
@@ -418,8 +418,11 @@ class MemInfoService
$levelInfo = $this->getMemLevel((int)$mem->mem_no);
if (($levelInfo['level'] ?? 0) < 1 || empty($levelInfo['auth_state']['email'])) {
return [
- 'ok'=>false,
- 'message'=>"
이메일 인증 완료후 이용가능합니다. \n이메일주소(".$mem->email.") 메일을 확인하세요\n",
+ 'ok' => false,
+ 'reason' => 'email_unverified',
+ 'email' => (string)$mem->email,
+ 'mem_no' => (int)$mem->mem_no,
+ 'message' => '이메일 인증이 필요합니다.',
];
}
diff --git a/app/Services/MypageInfoService.php b/app/Services/MypageInfoService.php
new file mode 100644
index 0000000..ad39825
--- /dev/null
+++ b/app/Services/MypageInfoService.php
@@ -0,0 +1,134 @@
+bool, 'message'=>string, 'enc_phone'=>string, 'raw_phone'=>string, 'ci'=>string, 'carrier'=>string]
+ */
+ public function validatePassPhoneChange(array $sess, array $passPayload): array
+ {
+ $memNo = (int) ($sess['_mno'] ?? 0);
+ if ($memNo <= 0) {
+ return $this->fail('로그인 정보가 확인되지 않습니다. 다시 로그인 후 시도해 주세요.');
+ }
+
+ $rawPhone = $this->normalizePhone((string) Arr::get($passPayload, 'PHONE', ''));
+ $ci = (string) Arr::get($passPayload, 'CI', '');
+ $carrier = strtoupper((string) Arr::get($passPayload, 'CARRIER', ''));
+
+ if ($rawPhone === '' || $ci === '') {
+ return $this->fail('인증 정보가 올바르지 않습니다. 다시 시도해 주세요.');
+ }
+
+ // 통신사 제한: SKT/KTF/LGT만 허용, 나머지 전부 차단 + MVNO 차단
+ $allowCarriers = ['SKT', 'KTF', 'LGT'];
+ if (!in_array($carrier, $allowCarriers, true) || $carrier === 'MVNO') {
+ return $this->fail('죄송합니다. SKT/KT/LG U+ 휴대전화만 인증할 수 있습니다. (알뜰폰/기타 통신사 불가)');
+ }
+
+ // PASS 전화번호 암호화 (CI 레거시 방식)
+ $encPassPhone = (string) $this->seed->encrypt($rawPhone);
+
+ // (1) 기존 회원 전화번호와 동일하면 변경 불가
+ $encMemberPhone = (string) ($sess['_mcell'] ?? '');
+ if ($encMemberPhone !== '' && $encPassPhone !== '' && hash_equals($encMemberPhone, $encPassPhone)) {
+ return $this->fail('현재 등록된 연락처와 동일합니다. 다른 번호로 인증해 주세요.');
+ }
+
+ // (2) PASS 전화번호가 DB에 존재하면 변경 불가
+ // 요구사항: "존재한다면 이전에 가입된 전화번호가 있습니다. 관리자 문의"
+ if ($this->repo->existsEncryptedCell($encPassPhone, $memNo)) {
+ return $this->fail('이미 가입된 휴대폰 번호가 있습니다. 관리자에게 문의해 주세요.');
+ }
+
+ // (3) CI가 회원정보 mem_info.ci와 일치해야 통과
+ $memberCi = $this->repo->getMemberCi($memNo);
+ if ($memberCi === '' || !hash_equals($memberCi, $ci)) {
+ return $this->fail('가입된 회원정보와 일치하지 않습니다. 관리자에게 문의해 주세요.');
+ }
+
+ return [
+ 'ok' => true,
+ 'message' => '검증이 완료되었습니다.',
+ 'enc_phone' => $encPassPhone,
+ 'raw_phone' => $rawPhone,
+ 'ci' => $ci,
+ 'carrier' => $carrier,
+ ];
+ }
+
+ /**
+ * 최종 저장: mem_info.cell 업데이트
+ * - 컨트롤러는 DB 처리 안 한다고 했으니, /mypage/info 저장 버튼에서 이 메서드만 호출하면 됨.
+ *
+ * @return array ['ok'=>bool, 'message'=>string, '_cell'=>string]
+ */
+ public function commitPhoneChange(int $memNo, array $passPayload): array
+ {
+ $Phone = $this->normalizePhone((string) Arr::get($passPayload, 'PHONE', ''));
+ $encPhone = (string) $this->seed->encrypt($Phone);
+
+ if ($memNo <= 0 || $encPhone === '') {
+ return $this->fail('저장할 정보가 올바르지 않습니다.');
+ }
+
+ return DB::transaction(function () use ($memNo, $encPhone) {
+ // 한번 더 중복 방어(레이스 컨디션)
+ if ($this->repo->existsEncryptedCell($encPhone, $memNo)) {
+ return $this->fail('이미 가입된 휴대폰 번호가 있습니다. 관리자에게 문의해 주세요.');
+ }
+
+ $affected = $this->repo->updateEncryptedCell($memNo, $encPhone);
+ if ($affected < 1) {
+ return $this->fail('연락처 저장에 실패했습니다. 잠시 후 다시 시도해 주세요.');
+ }
+
+ return [
+ 'ok'=>true,
+ 'message'=>'연락처가 변경되었습니다.',
+ '_cell' => $encPhone,
+ ];
+ });
+ }
+
+ /**
+ * 휴대폰 정규화 (숫자만)
+ */
+ private function normalizePhone(string $v): string
+ {
+ return preg_replace('/\D+/', '', $v) ?: '';
+ }
+
+ private function fail(string $message): array
+ {
+ return [
+ 'ok' => false,
+ 'message' => $message,
+ 'enc_phone' => '',
+ 'raw_phone' => '',
+ 'ci' => '',
+ 'carrier' => '',
+ ];
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 101c279..6bd6985 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -4,6 +4,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;
+use Illuminate\Routing\Exceptions\InvalidSignatureException;
return Application::configure(basePath: dirname(__DIR__))
// (A) Routing: 도메인별 라우트 분리
@@ -40,7 +41,8 @@ return Application::configure(basePath: dirname(__DIR__))
// - 도메인 제외, path만
// - 네 라우트 정의 기준: POST register/danal/result
$middleware->validateCsrfTokens(except: [
- 'auth/register/danal/result', //다날 PASS 콜백 (외부 서버가 호출)
+ 'auth/register/danal/result', //다날 PASS 회원가입 콜백 (외부 서버가 호출)
+ 'mypage/info/danal/result', //다날 PASS 전화번호 변경 콜백 (외부 서버가 호출)
]);
//페이지 접근권한 미들웨어 등록
@@ -49,10 +51,15 @@ return Application::configure(basePath: dirname(__DIR__))
'legacy.guest' => \App\Http\Middleware\LegacyGuest::class, //게스트 접근가능페이지
]);
- })
+
+ })
->withExceptions(function (Exceptions $exceptions): void {
- //
+
+ //서명 만료/위변조(temporarySignedRoute + signed middleware)
+ $exceptions->render(function (InvalidSignatureException $e, $request) {
+ return redirect('/')->with('alert', '잘못된 접근입니다.');
+ });
})
->create();
diff --git a/config/app.php b/config/app.php
index 423eed5..a8a8f51 100644
--- a/config/app.php
+++ b/config/app.php
@@ -65,7 +65,7 @@ return [
|
*/
- 'timezone' => 'UTC',
+ 'timezone' => env('APP_TIMEZONE', 'Asia/Seoul'),
/*
|--------------------------------------------------------------------------
diff --git a/config/mail.php b/config/mail.php
index 522b284..7d629cb 100644
--- a/config/mail.php
+++ b/config/mail.php
@@ -35,70 +35,25 @@ return [
|
*/
- 'mailers' => [
+ // config/mail.php
+ 'mailers' => [
'smtp' => [
'transport' => 'smtp',
- 'scheme' => env('MAIL_SCHEME'),
- 'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
- 'port' => env('MAIL_PORT', 2525),
+ 'port' => (int) env('MAIL_PORT', 587),
+ 'encryption' => env('MAIL_ENCRYPTION', 'tls'), // STARTTLS
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
- 'timeout' => null,
- 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
+ 'timeout' => (int) env('MAIL_TIMEOUT', 60), //timeout 적용
+ 'local_domain' => env(
+ 'MAIL_EHLO_DOMAIN',
+ parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)
+ ),
],
-
- 'ses' => [
- 'transport' => 'ses',
- ],
-
- 'postmark' => [
- 'transport' => 'postmark',
- // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
- // 'client' => [
- // 'timeout' => 5,
- // ],
- ],
-
- 'resend' => [
- 'transport' => 'resend',
- ],
-
- 'sendmail' => [
- 'transport' => 'sendmail',
- 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
- ],
-
- 'log' => [
- 'transport' => 'log',
- 'channel' => env('MAIL_LOG_CHANNEL'),
- ],
-
- 'array' => [
- 'transport' => 'array',
- ],
-
- 'failover' => [
- 'transport' => 'failover',
- 'mailers' => [
- 'smtp',
- 'log',
- ],
- 'retry_after' => 60,
- ],
-
- 'roundrobin' => [
- 'transport' => 'roundrobin',
- 'mailers' => [
- 'ses',
- 'postmark',
- ],
- 'retry_after' => 60,
- ],
-
],
+
/*
|--------------------------------------------------------------------------
| Global "From" Address
diff --git a/resources/css/web.css b/resources/css/web.css
index fa91502..5629e4f 100644
--- a/resources/css/web.css
+++ b/resources/css/web.css
@@ -1,3 +1,6 @@
+@charset "UTF-8";
+@import "./web/mypage.css";
+
/* =========================================
Voucher Mall Design System
Theme: White base + Blue accent
diff --git a/resources/css/web/mypage.css b/resources/css/web/mypage.css
new file mode 100644
index 0000000..6d59c8e
--- /dev/null
+++ b/resources/css/web/mypage.css
@@ -0,0 +1,485 @@
+/* =========================================
+ MYPAGE 공통: Gate 카드 (비밀번호 재확인)
+ 적용 범위: body.is-mypage 내부만
+ ========================================= */
+
+/* ✅ 래퍼: 가운데 정렬 + 여백 */
+.is-mypage .mypage-gate-wrap{
+ display:flex;
+ justify-content:center;
+ padding: 6px 0 18px;
+}
+
+/* ✅ 카드 */
+.is-mypage .mypage-gate-card{
+ width:100%;
+ max-width: 680px;
+ background:#fff;
+ border:1px solid rgba(16,24,40,.08);
+ border-radius: 18px;
+ box-shadow: 0 10px 30px rgba(16,24,40,.08);
+ overflow:hidden;
+ margin-top: 50px;
+ margin-bottom: 50px;
+}
+
+/* ✅ 바디 */
+.is-mypage .mypage-gate-body{
+ padding: 28px;
+}
+
+/* ✅ 타이틀/설명 */
+.is-mypage .mypage-gate-title{
+ font-size: 20px;
+ font-weight: 800;
+ letter-spacing: -.2px;
+ margin: 0 0 6px;
+ color: #101828;
+}
+.is-mypage .mypage-gate-desc{
+ margin: 0 0 14px;
+ color:#667085;
+ font-size: 14px;
+ line-height: 1.45;
+}
+
+/* ✅ 안내 박스 */
+.is-mypage .mypage-gate-note{
+ margin: 0 0 18px;
+ padding: 12px 14px;
+ border-radius: 14px;
+ background: #f8fafc;
+ border: 1px solid rgba(16,24,40,.06);
+ color:#344054;
+ font-size: 13px;
+ line-height: 1.5;
+}
+
+/* ✅ 한 줄 입력 + 버튼 */
+.is-mypage .mypage-gate-row{
+ display:flex;
+ gap: 10px;
+ align-items:center;
+}
+
+.is-mypage .mypage-gate-input{
+ flex: 1;
+ height: 46px;
+ border-radius: 12px;
+ border: 1px solid #d0d5dd;
+ padding: 0 14px;
+}
+
+.is-mypage .mypage-gate-input:focus{
+ border-color: #ff7a00;
+ box-shadow: 0 0 0 4px rgba(255,122,0,.14);
+}
+
+/* ✅ 버튼 */
+.is-mypage .mypage-gate-btn{
+ height: 46px;
+ padding: 0 18px;
+ border-radius: 12px;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+/* primary 버튼 */
+.is-mypage .btn-mypage-primary{
+ background: linear-gradient(135deg, #ff7a00, #ff3d00);
+ border: none;
+ color: #fff;
+}
+.is-mypage .btn-mypage-primary:hover{
+ filter: brightness(.98);
+ color:#fff;
+}
+
+.is-mypage .mypage-gate-card .mypage-gate-input,
+.is-mypage .mypage-gate-card input.form-control {
+ height: 52px;
+ min-height: 52px;
+ padding: 0 16px;
+ font-size: 16px; /* ✅ 모바일에서 얇아보이는/줌 이슈 방지 */
+ line-height: 52px; /* ✅ 세로 가운데 정렬 확실 */
+ border-radius: 14px;
+ border: 1px solid #d0d5dd;
+ box-sizing: border-box;
+}
+
+.is-mypage .mypage-gate-card .mypage-gate-input:focus,
+.is-mypage .mypage-gate-card input.form-control:focus {
+ border-color: #ff7a00;
+ box-shadow: 0 0 0 4px rgba(255,122,0,.15);
+}
+
+/* textarea 같은 멀티라인이 섞일 가능성 대비(선택) */
+.is-mypage .mypage-gate-card textarea.form-control {
+ line-height: 1.4;
+ padding: 12px 16px;
+ height: auto;
+ min-height: 120px;
+}
+
+/* ✅ 모바일: 버튼 아래로 */
+@media (max-width: 575.98px){
+ .is-mypage .mypage-gate-card{
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+
+ .is-mypage .mypage-gate-body{
+ padding: 20px;
+ }
+ .is-mypage .mypage-gate-row{
+ flex-direction: column;
+ align-items: stretch;
+ gap: 10px;
+ }
+ .is-mypage .mypage-gate-btn{
+ width: 100%;
+ }
+}
+
+/* ================================
+ MYPAGE Renew (Info hub)
+ ================================ */
+
+.is-mypage .mypage-hero{
+ border: 1px solid #eef0f4;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #ffffff, #fbfcff);
+ box-shadow: 0 10px 28px rgba(16, 24, 40, 0.06);
+}
+
+.is-mypage .mypage-hero__inner{
+ display: grid;
+ grid-template-columns: 1.2fr .8fr;
+ gap: 18px;
+ padding: 18px;
+ align-items: stretch;
+}
+
+.is-mypage .mypage-hero__kicker{
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: .08em;
+ color: #667085;
+}
+
+.is-mypage .mypage-hero__title{
+ margin-top: 6px;
+ font-size: 22px;
+ font-weight: 900;
+ line-height: 1.2;
+ color: #101828;
+}
+
+.is-mypage .mypage-hero__desc{
+ margin-top: 8px;
+ color: #667085;
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+.is-mypage .mypage-hero__right{
+ border-radius: 14px;
+ border: 1px solid #eef0f4;
+ background: #fff;
+ padding: 14px 14px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ justify-content: space-between;
+}
+
+/* 상태 chip */
+.is-mypage .mypage-chip{
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 30px;
+ padding: 0 12px;
+ border-radius: 999px;
+ font-weight: 800;
+ font-size: 12px;
+ border: 1px solid #e4e7ec;
+ background: #f9fafb;
+ color: #344054;
+ width: fit-content;
+}
+.is-mypage .mypage-chip.is-ok{
+ border-color: rgba(16,185,129,.35);
+ background: rgba(16,185,129,.10);
+ color: #067647;
+}
+.is-mypage .mypage-chip.is-warn{
+ border-color: rgba(245,158,11,.35);
+ background: rgba(245,158,11,.10);
+ color: #92400e;
+}
+
+/* 인증 상태 표 */
+.is-mypage .mypage-reauth{
+ border-radius: 12px;
+ background: #f8fafc;
+ border: 1px solid #eef0f4;
+ padding: 10px 12px;
+}
+.is-mypage .mypage-reauth__row{
+ display: flex;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 4px 0;
+ font-size: 12px;
+}
+.is-mypage .mypage-reauth__label{ color: #667085; font-weight: 700; }
+.is-mypage .mypage-reauth__value{ color: #101828; font-weight: 800; }
+
+/* 우측 버튼 */
+.is-mypage .mypage-hero__actions .btn{
+ width: 100%;
+ height: 44px;
+ border-radius: 12px;
+ font-weight: 800;
+}
+
+/* 카드 그리드 */
+.is-mypage .mypage-grid{
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.is-mypage .mypage-card{
+ border: 1px solid #eef0f4;
+ border-radius: 16px;
+ background: #fff;
+ padding: 14px;
+ box-shadow: 0 10px 24px rgba(16, 24, 40, 0.05);
+ display: grid;
+ grid-template-columns: 42px 1fr 16px;
+ align-items: center;
+ gap: 12px;
+ text-decoration: none;
+ transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
+}
+
+.is-mypage .mypage-card:hover{
+ transform: translateY(-1px);
+ border-color: rgba(255,122,0,.35);
+ box-shadow: 0 14px 30px rgba(16, 24, 40, 0.08);
+}
+
+.is-mypage .mypage-card__icon{
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(600px 180px at 30% 30%, rgba(255,140,0,.18), transparent 55%),
+ radial-gradient(520px 160px at 70% 30%, rgba(94,92,230,.14), transparent 55%),
+ #f8fafc;
+ font-size: 18px;
+}
+
+.is-mypage .mypage-card__title{
+ font-weight: 900;
+ color: #101828;
+ font-size: 14px;
+ margin: 0;
+}
+.is-mypage .mypage-card__desc{
+ margin-top: 4px;
+ color: #667085;
+ font-size: 12.5px;
+ line-height: 1.45;
+}
+.is-mypage .mypage-card__meta{
+ margin-top: 8px;
+ font-size: 12px;
+ font-weight: 800;
+ color: #98a2b3;
+}
+.is-mypage .mypage-card__arrow{
+ color: #98a2b3;
+ font-size: 20px;
+ font-weight: 900;
+ line-height: 1;
+}
+
+/* 하단 안내 */
+.is-mypage .mypage-note{
+ border: 1px solid #eef0f4;
+ border-radius: 16px;
+ background: #fff;
+ padding: 14px 16px;
+}
+.is-mypage .mypage-note__title{
+ font-weight: 900;
+ color: #101828;
+ margin-bottom: 8px;
+}
+.is-mypage .mypage-note__list{
+ margin: 0;
+ padding-left: 18px;
+ color: #475467;
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+/* 반응형 */
+@media (max-width: 991.98px){
+ .is-mypage .mypage-hero__inner{
+ grid-template-columns: 1fr;
+ }
+}
+@media (max-width: 575.98px){
+ .is-mypage .mypage-grid{
+ grid-template-columns: 1fr;
+ }
+ .is-mypage .mypage-hero__inner{
+ padding: 14px;
+ }
+}
+
+/* 카드가 button일 때 기본 버튼 스타일 제거 */
+.is-mypage .mypage-card--btn{
+ width: 100%;
+ text-align: left;
+ border: 1px solid #eef0f4;
+ background: #fff;
+ cursor: pointer;
+ appearance: none;
+ -webkit-appearance: none;
+}
+.is-mypage .mypage-card--btn:focus{
+ outline: none;
+}
+.is-mypage .mypage-card--btn:focus-visible{
+ border-color: rgba(255,122,0,.55);
+ box-shadow: 0 0 0 4px rgba(255,122,0,.14);
+ transform: translateY(-1px);
+}
+
+/* =========================
+ MYPAGE: info_renew 섹션 카드 UI
+ ========================= */
+.is-mypage .mypage-section { margin-bottom: 18px; }
+
+.is-mypage .mypage-card{
+ border: 1px solid #eef0f4;
+ border-radius: 16px;
+ background: #fff;
+ box-shadow: 0 10px 30px rgba(16,24,40,.06);
+ overflow: hidden;
+}
+
+.is-mypage .mypage-card__head{
+ display:flex;
+ align-items:flex-start;
+ justify-content:space-between;
+ gap: 12px;
+ padding: 18px 18px 14px;
+ border-bottom: 1px solid #f2f4f7;
+}
+
+.is-mypage .mypage-card__title{
+ font-size: 16px;
+ font-weight: 800;
+ line-height: 1.2;
+ margin: 0 0 6px;
+}
+
+.is-mypage .mypage-card__desc{
+ font-size: 13px;
+ color: #667085;
+ margin: 0;
+}
+
+.is-mypage .mypage-card__body{
+ padding: 16px 18px 18px;
+}
+
+.is-mypage .mypage-btn{
+ height: 44px;
+ padding: 0 14px;
+ border-radius: 12px;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+.is-mypage .mypage-kv{
+ display:flex;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+
+.is-mypage .mypage-kv__item{
+ flex: 1 1 220px;
+ border: 1px solid #f2f4f7;
+ border-radius: 14px;
+ padding: 12px 14px;
+ background: #fcfcfd;
+}
+
+.is-mypage .mypage-kv__label{
+ font-size: 12px;
+ color: #667085;
+ margin-bottom: 6px;
+}
+
+.is-mypage .mypage-kv__value{
+ font-size: 14px;
+ font-weight: 800;
+ color: #101828;
+}
+
+.is-mypage .mypage-badge{
+ display:inline-flex;
+ align-items:center;
+ height: 26px;
+ padding: 0 10px;
+ border-radius: 999px;
+ border: 1px solid #e4e7ec;
+ background: #fff;
+ font-size: 12px;
+ font-weight: 800;
+ color: #344054;
+}
+
+.is-mypage .mypage-badge--ok{
+ border-color: rgba(255,122,0,.35);
+ background: rgba(255,122,0,.10);
+ color: #b54708;
+}
+
+.is-mypage .mypage-help{
+ border: 1px solid #eef0f4;
+ border-radius: 14px;
+ background: #f8fafc;
+ padding: 12px 14px;
+ color: #344054;
+ font-size: 13px;
+}
+
+.is-mypage .mypage-help ul{
+ margin: 0;
+ padding-left: 18px;
+}
+
+.is-mypage .mypage-help li{
+ margin: 3px 0;
+}
+
+/* 모바일에서도 버튼/정렬 깔끔하게 */
+@media (max-width: 575.98px){
+ .is-mypage .mypage-card__head{
+ flex-direction: column;
+ align-items: stretch;
+ }
+ .is-mypage .mypage-btn{
+ width: 100%;
+ }
+}
+
diff --git a/resources/views/mail/auth/otp_code.blade.php b/resources/views/mail/auth/otp_code.blade.php
new file mode 100644
index 0000000..f4d4736
--- /dev/null
+++ b/resources/views/mail/auth/otp_code.blade.php
@@ -0,0 +1,28 @@
+@extends('mail.layouts.base')
+
+@section('content')
+
+
{{ $title ?? '인증번호 안내' }}
+
+
+
+ {{ $email }} 회원님,
+ 아래 인증번호를 입력해 주세요.
+ (인증번호는 {{ $expires_min ?? 3 }}분 후 만료됩니다.)
+
+
+
+
+
+
+
+
+
+ 본인이 요청하지 않았다면 이 메일을 무시해 주세요.
+
+
+@endsection
diff --git a/resources/views/mail/auth/reset_password.blade.php b/resources/views/mail/auth/reset_password.blade.php
new file mode 100644
index 0000000..656c2f5
--- /dev/null
+++ b/resources/views/mail/auth/reset_password.blade.php
@@ -0,0 +1,23 @@
+@extends('mail.layouts.base')
+
+@section('content')
+
+
비밀번호 재설정 안내
+
+
+ 아래 버튼을 눌러 비밀번호 재설정을 진행해 주세요.
+ 링크 유효시간: {{ $expires_min ?? 30 }}분
+
+
+
+
+ 비밀번호 재설정하기
+
+
+
+
+ 버튼이 동작하지 않으면 아래 링크를 복사해 브라우저에 붙여넣어 주세요.
+ {{ $link }}
+
+
+@endsection
diff --git a/resources/views/mail/auth/verify_email.blade.php b/resources/views/mail/auth/verify_email.blade.php
new file mode 100644
index 0000000..944ec40
--- /dev/null
+++ b/resources/views/mail/auth/verify_email.blade.php
@@ -0,0 +1,35 @@
+@extends('mail.layouts.base')
+
+@section('content')
+
+
이메일 주소 인증
+
+
+
+ {{ $email }} 회원님,
+ 아래 버튼을 클릭하여 이메일 인증을 완료해 주세요.
+ (인증 링크는 {{ $expires_min ?? 30 }}분 후 만료됩니다.)
+
+
+
+
+
+
+
+
+
+ 버튼이 동작하지 않으면 아래 주소를 복사해 브라우저에 붙여넣으세요.
+ {{ $link }}
+
+
+@endsection
diff --git a/resources/views/mail/layouts/base.blade.php b/resources/views/mail/layouts/base.blade.php
new file mode 100644
index 0000000..6078918
--- /dev/null
+++ b/resources/views/mail/layouts/base.blade.php
@@ -0,0 +1,65 @@
+@php
+ $brand = $brand ?? 'PIN FOR YOU';
+ $accent = $accent ?? '#E4574B';
+ $siteUrl = $siteUrl ?? config('app.url');
+@endphp
+
+
+
+
+
+
+
+
+
+
+
+
+ {{-- Header --}}
+
+ |
+
+ {{ $brand }}
+
+
+ 안전하고 빠른 상품권 거래
+
+ |
+
+
+ {{-- Body --}}
+
+ |
+ @yield('content')
+ |
+
+
+ {{-- Footer --}}
+
+ |
+
+
+ © {{ date('Y') }} {{ $brand }}. All rights reserved.
+
+ |
+
+
+
+ {{-- small safe spacing --}}
+
+ |
+
+
+
+
diff --git a/resources/views/mail/legacy/i_withdrawal_1.blade.php b/resources/views/mail/legacy/i_withdrawal_1.blade.php
deleted file mode 100644
index 2d03caa..0000000
--- a/resources/views/mail/legacy/i_withdrawal_1.blade.php
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_20230621_event.blade.php b/resources/views/mail/legacy/noti_20230621_event.blade.php
deleted file mode 100644
index e7b88b4..0000000
--- a/resources/views/mail/legacy/noti_20230621_event.blade.php
+++ /dev/null
@@ -1,157 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
- |
-
-
-
-
-
- |
-
- 회원님 온라인 상품권 쇼핑채널 "핀포유" 입니다.
-
-
- 고객님은 =$_PRODUCT_?> =$_MONEY_?> 지급대상입니다.
-
- |
-
-
-
-
-
- 상품권 수령방법
-
-
-
- 1. 핀포유(pinforyou.com) 접속
- 2. 로그인 후 "고객센터>1:1문의" 접수
- ㄴ문의분류선택 : "이벤트문의"
- ㄴ문의제목 : 이벤트참여
- ㄴ문의내용 : =$_PRODUCT_?> 주세요.
-
-
-
-
-
-
-
-
-
-
- |
-
- |
- 1:1문의로 이벤트참여 문의글만 남겨도 =$_PRODUCT_?>(을)를 100% 무조건 지급! |
-
-
- |
-
- |
- 상품권은 핀번호 형태로 지급되며, 상담사가 대상자 확인 후 답변으로 지급드립니다. |
-
-
- |
-
- |
- 본 이벤트 외 추가 지급 이벤트가 있으니 지금 바로 참여해서 상품권 받고 추가 이벤트도 참여하세요! |
-
-
-
-
-
-
-
-
-
-
-
- |
-
- 핀포유만의 특별혜택
- "입금수수료 무료! 1분안에 자동입금!"
- |
-
-
- |
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
- |
-
- |
- 2023년 06월 23일 발송 이메일입니다. |
-
-
-
- |
-
- |
- 본 이메일은 발신전용 이메일로 [2023년 06월 23일]기준, 이메일 수신동의 여부를 확인한 결과 회원님께서 수신에 동의하신 것으로 확인되어 발송되었습니다. |
-
-
-
-
- |
-
-
-
-
-
- |
-
-
- 주소전북특별자치도 전주시 완산구 용머리로 94, 4층 451호
-
-
- 대표이사송병수
-
-
- 사업자등록번호121-88-01191
-
-
-
-
- 전화1833-4856
-
-
- 통신판매업신고번호제 2018-전주완산-0705호
-
- master@plusmaker.co.kr
-
- Copyright (c) 2018 Pin For You. All Rights Reserved.
- |
-
-
-
-
diff --git a/resources/views/mail/legacy/noti_center_reply_1.blade.php b/resources/views/mail/legacy/noti_center_reply_1.blade.php
deleted file mode 100644
index 4380c7d..0000000
--- a/resources/views/mail/legacy/noti_center_reply_1.blade.php
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_deposit_1.blade.php b/resources/views/mail/legacy/noti_deposit_1.blade.php
deleted file mode 100644
index 8f3dc3d..0000000
--- a/resources/views/mail/legacy/noti_deposit_1.blade.php
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_dormancy_email.blade.php b/resources/views/mail/legacy/noti_dormancy_email.blade.php
deleted file mode 100644
index a221181..0000000
--- a/resources/views/mail/legacy/noti_dormancy_email.blade.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
diff --git a/resources/views/mail/legacy/noti_email_auth_1.blade.php b/resources/views/mail/legacy/noti_email_auth_1.blade.php
deleted file mode 100644
index 8f251fd..0000000
--- a/resources/views/mail/legacy/noti_email_auth_1.blade.php
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_login_1.blade.php b/resources/views/mail/legacy/noti_login_1.blade.php
deleted file mode 100644
index e0bb4d9..0000000
--- a/resources/views/mail/legacy/noti_login_1.blade.php
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
- 핀포유 (PinForYou)
- |
-
-
-
-
-
-
-
- 로그인 알림
- |
-
-
-
- ㅇㅇㅇ님! 핀포유(">http://= $const["_ROOT_DOMAIN_"] ?>)에 로그인 되었습니다.
- |
-
-
- |
- 접속 정보
- 로그인 일시 : 2017년 00.00
- 접속장소 : 국가
- IP :
- 접속기기
- |
-
-
- |
- 회원님이 로그인 하시지 않은 알림을 받으신 경우, 계정의 보안을 위해
- 비밀번호를 즉시 변경 하시기 바라며, 고객센터로 문의 하시기 바랍니다.
- |
-
-
- |
- 비밀번호 변경
- |
-
-
-
- |
-
-
- |
- 비밀번호 변경을 누르시면 비밀번호 변경 페이지로 이동합니다.
- |
-
-
- |
- 고객센터 문의하기
- |
-
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_mem_modi_1.blade.php b/resources/views/mail/legacy/noti_mem_modi_1.blade.php
deleted file mode 100644
index b98fcbe..0000000
--- a/resources/views/mail/legacy/noti_mem_modi_1.blade.php
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_qna_admin_push.blade.php b/resources/views/mail/legacy/noti_qna_admin_push.blade.php
deleted file mode 100644
index 8923385..0000000
--- a/resources/views/mail/legacy/noti_qna_admin_push.blade.php
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_set_pass_1.blade.php b/resources/views/mail/legacy/noti_set_pass_1.blade.php
deleted file mode 100644
index ac5168e..0000000
--- a/resources/views/mail/legacy/noti_set_pass_1.blade.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
\ No newline at end of file
diff --git a/resources/views/mail/legacy/noti_sign_up_1.blade.php b/resources/views/mail/legacy/noti_sign_up_1.blade.php
deleted file mode 100644
index aa64df4..0000000
--- a/resources/views/mail/legacy/noti_sign_up_1.blade.php
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
- 핀포유 (PinForYou)
- |
-
-
-
-
-
-
-
- 회원가입 인증메일
- |
-
-
-
- 온라인 상품권 쇼핑채널 '핀포유' 가입을 축하드립니다!
-
- =$_EMAIL_?> 회원님. 핀포유 가입을 진심으로 환영하며, 가입한 계정정보 확인 후 이메일 인증 버튼을 클릭하여 이메일 인증을 완료 하시기 바랍니다.
- |
-
-
- |
- 가입 계정 정보
- 핀포유 : http://= $const["_ROOT_DOMAIN_"]?>/
- 아이디 : =$_EMAIL_?>
- |
-
-
-
- |
-
- - SNS지인사칭, 공무원사칭 피싱사기 주의 (필독) -
- 1. 카카오톡/SNS메신저로 가족, 지인을 사칭한 피싱사기가 급증하고 있습니다. 누군가의 부탁으로 개인정보, 휴대폰 인증번호 노출 또는 결제부탁을 받으신 경우
- 메신저 대화를 중단 하시고 반드시 해당 지인과 통화를 하여 사실관계를 확인바랍니다.
-
- 2. 검찰 또는 수사관(공무원)을 사칭하는 사람에게 전화를 받고 회원 가입을 하거나, 구인광고를 통한 구매대행/고액알바 등의 아르바이트를 이유로 저희 “핀포유” 에 가입 후
- 상품권 구매를 지시/요청 받았다면 99.9% 보이스피싱과 같은 금융 사고/사기일 가능성이 높습니다.
- 이점 이용에 유의하여 주시기 바랍니다.
-
- |
-
-
-
- |
- 이메일 인증
- |
-
-
- |
- 이메일 인증후 사이트 이용이 가능합니다.
- |
-
-
-
-
- |
-
-
- |
- " target="_blank" style="color:#E4574B;font-weight:bold;font-size:14px;text-decoration:none">핀포유 바로가기
- |
-
-
-
\ No newline at end of file
diff --git a/resources/views/web/auth/email_required.blade.php b/resources/views/web/auth/email_required.blade.php
new file mode 100644
index 0000000..ea85810
--- /dev/null
+++ b/resources/views/web/auth/email_required.blade.php
@@ -0,0 +1,148 @@
+@extends('web.layouts.auth')
+
+@section('title', '이메일 인증 필요 | PIN FOR YOU')
+@section('meta_description', 'PIN FOR YOU 이메일 인증 안내 페이지입니다.')
+@section('canonical', url('/auth/email-required'))
+
+@section('h1', '이메일 인증이 필요합니다')
+@section('desc', '인증 후 사이트 이용이 가능합니다. 등록된 이메일로 인증 메일을 보내드릴게요.')
+@section('card_aria', '이메일 인증 안내')
+@section('show_cs_links', true)
+
+@section('auth_content')
+
+@endsection
+
+@section('auth_bottom')
+ {{-- 필요 시 하단 문구 --}}
+@endsection
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/web/auth/email_verified.blade.php b/resources/views/web/auth/email_verified.blade.php
new file mode 100644
index 0000000..0854a73
--- /dev/null
+++ b/resources/views/web/auth/email_verified.blade.php
@@ -0,0 +1,60 @@
+@extends('web.layouts.auth')
+
+@section('title', '이메일 인증 완료 | PIN FOR YOU')
+@section('meta_description', 'PIN FOR YOU 이메일 인증 완료 안내 페이지입니다.')
+@section('canonical', url('/auth/email-verified'))
+
+@section('h1', '이메일 인증이 완료되었습니다')
+@section('desc', '인증이 정상적으로 확인되었습니다. 잠시 후 로그인 페이지로 이동합니다.')
+@section('card_aria', '이메일 인증 완료 안내')
+@section('show_cs_links', true)
+
+@section('auth_content')
+
+
+
+ {{ $email }}
+ 인증이 완료되었습니다.
+
+
+
+
+
+
+ 5초 후 로그인 페이지로 이동합니다.
+
+
+@endsection
+
+@section('auth_bottom')
+ {{-- 필요 시 하단 문구 --}}
+@endsection
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/web/auth/find_id.blade.php b/resources/views/web/auth/find_id.blade.php
index f5ed92e..ca13135 100644
--- a/resources/views/web/auth/find_id.blade.php
+++ b/resources/views/web/auth/find_id.blade.php
@@ -8,6 +8,13 @@
@section('desc', '가입 시 등록한 휴대폰 번호로 아이디를 확인할 수 있어요.')
@section('card_aria', '아이디 찾기 폼')
@section('show_cs_links', true)
+{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}}
+@push('recaptcha')
+
+
+
+@endpush
+
@section('auth_content')
@@ -153,6 +159,29 @@
// -------- helpers ----------
const csrf = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
+ const getRecaptchaToken = async (action) => {
+ try {
+ // recaptcha-v3.js에서 제공하는 함수명이 다를 수 있어서 2가지 케이스 처리
+ if (typeof window.recaptchaV3Execute === 'function') {
+ const t = await window.recaptchaV3Execute(action);
+ return (t || '').toString();
+ }
+
+ // fallback: grecaptcha 직접 호출
+ if (window.grecaptcha && window.__recaptchaSiteKey) {
+ return await new Promise((resolve) => {
+ grecaptcha.ready(() => {
+ grecaptcha.execute(window.__recaptchaSiteKey, { action })
+ .then((t) => resolve((t || '').toString()))
+ .catch(() => resolve(''));
+ });
+ });
+ }
+ } catch (e) {}
+
+ return '';
+ };
+
const postJson = async (url, data) => {
const res = await fetch(url, {
method: 'POST',
@@ -268,7 +297,11 @@
setMsg('확인 중입니다...', 'info');
try {
- const json = await postJson(@json(route('web.auth.find_id.send_code')), { phone: raw });
+ const token = await getRecaptchaToken('find_id'); // ✅ 컨트롤러 Rule action과 동일
+ const json = await postJson(@json(route('web.auth.find_id.send_code')), {
+ phone: raw,
+ 'g-recaptcha-response': token,
+ });
// ✅ 성공 (ok true)
setMsg(json.message || '인증번호를 발송했습니다.', 'success');
@@ -278,9 +311,9 @@
startTimer(json.expires_in || 180);
- if(json.dev_code){
- setMsg(`(개발용) 인증번호: ${json.dev_code}`, 'info');
- }
+ // if(json.dev_code){
+ // setMsg(`(개발용) 인증번호: ${json.dev_code}`, 'info');
+ // }
} catch (err) {
// ✅ 여기서 404(PHONE_NOT_FOUND)도 UX로 처리
@@ -310,7 +343,11 @@
setMsg('인증 확인 중입니다...', 'info');
- const json = await postJson(@json(route('web.auth.find_id.verify')), { code });
+ const token = await getRecaptchaToken('find_id'); // 또는 'find_id_verify'로 분리해도 됨(서버와 동일해야 함)
+ const json = await postJson(@json(route('web.auth.find_id.verify')), {
+ code,
+ 'g-recaptcha-response': token,
+ });
// ✅ 먼저 step 이동 + 렌더 (패널 표시 보장)
diff --git a/resources/views/web/auth/find_password.blade.php b/resources/views/web/auth/find_password.blade.php
index ac23ab0..2be4aba 100644
--- a/resources/views/web/auth/find_password.blade.php
+++ b/resources/views/web/auth/find_password.blade.php
@@ -5,19 +5,21 @@
@section('canonical', url('/auth/find-password'))
@section('h1', '비밀번호 찾기')
-@section('desc', '가입된 이메일과 인증을 통해 새 비밀번호를 설정합니다.')
+@section('desc', '가입된 이메일과 성명 확인 후, 비밀번호 재설정 링크를 이메일로 보내드립니다.')
@section('card_aria', '비밀번호 찾기 폼')
@section('show_cs_links', true)
-@section('auth_content')
-
-
1. 계정 확인
-
2. 인증
-
3. 재설정
-
+{{-- ✅ reCAPTCHA 스크립트/공통함수는 이 페이지에서만 로드 --}}
+@push('recaptcha')
+
+
+
+@endpush
+@section('auth_content')
+
+ @push('styles')
+
+ @endpush
+ @push('scripts')
+
+ @endpush
+
+
+
+
+
+@endsection
diff --git a/resources/views/web/partials/auth/auth-footer-links.blade.php b/resources/views/web/partials/auth/auth-footer-links.blade.php
index aedc713..22b77c6 100644
--- a/resources/views/web/partials/auth/auth-footer-links.blade.php
+++ b/resources/views/web/partials/auth/auth-footer-links.blade.php
@@ -5,5 +5,5 @@
·
아이디 찾기
·
- 비밀번호 찾기
+ 비밀번호 찾기
diff --git a/routes/web.php b/routes/web.php
index 41ed7db..54f4db8 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -6,7 +6,9 @@ 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\Mypage\InfoGateController;
use App\Http\Controllers\Web\Cs\NoticeController;
+use App\Http\Controllers\Web\Auth\EmailVerificationController;
Route::view('/', 'web.home')->name('web.home');
@@ -34,7 +36,16 @@ Route::prefix('cs')->name('web.cs.')->group(function () {
Route::prefix('mypage')->name('web.mypage.')
->middleware('legacy.auth')
->group(function () {
- Route::view('info', 'web.mypage.info.index')->name('info.index');
+ Route::get('info', [InfoGateController::class, 'show'])->name('info.index');
+ Route::post('info', [InfoGateController::class, 'verify'])->name('info.verify');
+
+ Route::get('info_renew', [InfoGateController::class, 'info_renew'])->name('info.renew');
+
+ Route::post('info/pass/ready', [InfoGateController::class, 'passReady'])->name('info.pass.ready');
+ Route::post('info/danal/start', [InfoGateController::class, 'danalStart'])->name('info.danal.start');
+ Route::post('info/danal/result', [InfoGateController::class, 'danalResult'])->name('info.danal.result');
+ Route::get('info/gate-reset', [InfoGateController::class, 'gateReset'])->name('info.gate_reset');
+
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');
@@ -45,7 +56,7 @@ Route::prefix('mypage')->name('web.mypage.')
| 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');
@@ -96,18 +107,27 @@ Route::prefix('auth')->name('web.auth.')->group(function () {
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::get('find-password', [FindPasswordController::class, 'show'])->name('find_password.show');
+ Route::post('find-password/send-mail', [FindPasswordController::class, 'sendMail'])->name('find_password.send_mail');
+ Route::get('find-password/verify', [FindPasswordController::class, 'verifyLink'])->name('find_password.verify')->middleware('signed');
+ Route::post('find-password/reset', [FindPasswordController::class, 'reset'])->name('find_password.reset');
+
+ //이메일인증
+ Route::get('email-required', [EmailVerificationController::class, 'requiredPage'])->name('email.required');
+ Route::post('email/send-verify', [EmailVerificationController::class, 'sendVerify'])->name('email.send_verify');
+ Route::get('email/verify', [EmailVerificationController::class, 'verify'])->name('email.verify')->middleware('signed');
});
- Route::middleware('legacy.auth')->group(function () {
- Route::post('logout', [LoginController::class, 'logout'])->name('logout');
- });
+ Route::post('logout', [LoginController::class, 'logout'])->name('logout');
});
+Route::prefix('auth')->name('web.auth.')->group(function () {
+ Route::middleware('legacy.guest')->group(function () {
+ Route::get('email-required', [EmailVerificationController::class, 'requiredPage'])->name('email.required');
+ Route::post('email/send-verify', [EmailVerificationController::class, 'sendVerify'])->name('email.send_verify');
+ Route::get('email/verify', [EmailVerificationController::class, 'verify'])->name('email.verify')->middleware('signed'); // 만료/위변조 자동 차단
+ });
+});
/*
|--------------------------------------------------------------------------
@@ -117,12 +137,6 @@ 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 (로컬에서만 + 파일 존재할 때만 라우트 등록)
@@ -150,6 +164,8 @@ if (app()->environment(['local', 'development', 'testing'])
+
+
/* 개발용 페이지 세션 보기*/
use Illuminate\Http\Request;