sms인증 아이디찾기 작업
This commit is contained in:
parent
014597e4c2
commit
4d29259cc2
276
app/Http/Controllers/Web/Auth/FindIdController.php
Normal file
276
app/Http/Controllers/Web/Auth/FindIdController.php
Normal file
@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SmsService;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
use App\Models\MemInfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FindIdController extends Controller
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
// 세션에 결과가 있으면 3단계로 시작, 인증대기면 2단계, 아니면 1단계
|
||||
$sess = $request->session()->get('find_id', []);
|
||||
$step = 1;
|
||||
if (!empty($sess['result_email_masked'])) $step = 3;
|
||||
else if (!empty($sess['sent'])) $step = 2;
|
||||
|
||||
return view('web.auth.find_id', [
|
||||
'initialStep' => $step,
|
||||
'maskedEmail' => $sess['result_email_masked'] ?? null,
|
||||
'phone' => $sess['phone'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendCode(Request $request)
|
||||
{
|
||||
logger()->info('HIT sendCode', ['path' => request()->path(), 'host' => request()->getHost()]);
|
||||
|
||||
$v = Validator::make($request->all(), [
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
], [
|
||||
'phone.required' => '휴대폰 번호를 입력해 주세요.',
|
||||
]);
|
||||
|
||||
if ($v->fails()) {
|
||||
return response()->json(['ok' => false, 'message' => $v->errors()->first()], 422);
|
||||
}
|
||||
|
||||
$phoneRaw = $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();
|
||||
|
||||
if (!$exists) {
|
||||
// ✅ 세션도 만들지 말고, 프론트가 Step1로 돌아가도록 힌트 제공
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'code' => 'PHONE_NOT_FOUND',
|
||||
'message' => '해당 휴대폰 번호로 가입된 계정을 찾을 수 없습니다. 번호를 다시 확인해 주세요.',
|
||||
'step' => 1,
|
||||
], 404);
|
||||
}
|
||||
|
||||
// 레이트리밋(휴대폰 기준) - 과도한 발송 방지
|
||||
$key = 'findid:send:' . $phone;
|
||||
if (RateLimiter::tooManyAttempts($key, 5)) { // 10분에 5회 예시
|
||||
$sec = RateLimiter::availableIn($key);
|
||||
return response()->json(['ok' => false, 'message' => "요청이 너무 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
|
||||
}
|
||||
RateLimiter::hit($key, 600);
|
||||
|
||||
// 6자리 OTP 생성
|
||||
$code = (string) random_int(100000, 999999);
|
||||
|
||||
// 세션 저장(보안: 실제로는 해시 저장 권장, 여기선 간단 구현)
|
||||
$request->session()->put('find_id', [
|
||||
'sent' => true,
|
||||
'phone' => $phone,
|
||||
'code' => password_hash($code, PASSWORD_DEFAULT),
|
||||
'code_expires_at' => now()->addMinutes(3)->timestamp,
|
||||
'verified' => false,
|
||||
'result_email_masked' => null,
|
||||
]);
|
||||
|
||||
// SMS 발송
|
||||
try {
|
||||
// $smsPayload = [
|
||||
// 'from_number' => config('services.sms.from', '1833-4856'), // 기본 발신번호
|
||||
// 'to_number' => $phone,
|
||||
// 'message' => "[PIN FOR YOU] 인증번호 {$code} 를 입력해 주세요. (3분 이내)",
|
||||
// 'sms_type' => 'sms', // 짧으니 sms 고정(원하면 자동 판단으로 빼도 됨)
|
||||
// // 'country' => '82', // 필요 시 강제
|
||||
// ];
|
||||
//
|
||||
// $ok = app(SmsService::class)->send($smsPayload, 'lguplus');
|
||||
//
|
||||
// if (!$ok) {
|
||||
// // 실패 시 세션 정리(인증 진행 꼬임 방지)
|
||||
// $request->session()->forget('find_id');
|
||||
// return response()->json([
|
||||
// 'ok' => false,
|
||||
// 'message' => '문자 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.',
|
||||
// ], 500);
|
||||
// }
|
||||
} catch (\Throwable $e) {
|
||||
$request->session()->forget('find_id');
|
||||
|
||||
// 운영에서만 로그 남기기(개발 중엔 디버깅 가능)
|
||||
Log::error('FindId SMS send failed', [
|
||||
'phone' => $phone,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
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)
|
||||
{
|
||||
$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_id');
|
||||
if (!$sess || empty($sess['sent']) || empty($sess['phone'])) {
|
||||
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 = 'findid:verify:' . $sess['phone'];
|
||||
if (RateLimiter::tooManyAttempts($key, 10)) { // 10분 10회 예시
|
||||
$sec = RateLimiter::availableIn($key);
|
||||
return response()->json(['ok' => false, 'message' => "시도 횟수가 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
|
||||
}
|
||||
RateLimiter::hit($key, 600);
|
||||
|
||||
$code = $request->input('code');
|
||||
$ok = password_verify($code, $sess['code'] ?? '');
|
||||
|
||||
if (!$ok) {
|
||||
return response()->json(['ok' => false, 'message' => '인증번호가 일치하지 않습니다.'], 422);
|
||||
}
|
||||
|
||||
// 인증 성공: 휴대폰 번호로 가입된 "아이디(이메일)"들 조회 (여러개면 전부)
|
||||
$phone = (string) ($sess['phone'] ?? '');
|
||||
$phone = trim($phone);
|
||||
|
||||
if ($phone === '') {
|
||||
return response()->json(['ok' => false, 'message' => '휴대폰 번호가 비어 있습니다.'], 422);
|
||||
}
|
||||
|
||||
/** @var CiSeedCrypto $crypto */
|
||||
$crypto = app(CiSeedCrypto::class);
|
||||
|
||||
// 키를 넘기지 말고, 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();
|
||||
|
||||
if (empty($emails)) {
|
||||
// 운영에서는 암호문 노출 절대 금지 (지금은 디버그용이면 로그로만)
|
||||
// Log::debug('find-id phoneEnc', ['phoneEnc' => $phoneEnc, 'phone' => $phone]);
|
||||
return response()->json(['ok' => false, 'message' => '해당 번호로 가입된 계정을 찾을 수 없습니다.'], 404);
|
||||
}
|
||||
|
||||
// 마스킹해서 여러개 내려주기
|
||||
$maskedEmails = array_map(fn($e) => $this->maskEmail($e), $emails);
|
||||
|
||||
$request->session()->forget('find_id');
|
||||
$request->session()->save();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => '인증이 완료되었습니다.',
|
||||
'count' => count($maskedEmails),
|
||||
'masked_emails' => $maskedEmails,
|
||||
//'emails' => $emails,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function reset(Request $request)
|
||||
{
|
||||
$request->session()->forget('find_id');
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
private function normalizeKoreanPhone(string $input): ?string
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
$email = trim($email);
|
||||
|
||||
if (!str_contains($email, '@')) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
|
||||
$localLen = mb_strlen($local, 'UTF-8');
|
||||
|
||||
// 너무 짧은 로컬파트는 규칙 완화
|
||||
if ($localLen <= 2) {
|
||||
// ab@ -> a*@ 정도
|
||||
$head = mb_substr($local, 0, 1, 'UTF-8');
|
||||
return $head . '*' . '@' . $domain;
|
||||
}
|
||||
|
||||
if ($localLen <= 5) {
|
||||
// abcde -> ab*** (끝 1자리만 힌트)
|
||||
$head = mb_substr($local, 0, 2, 'UTF-8');
|
||||
$tail = mb_substr($local, -1, 1, 'UTF-8');
|
||||
return $head . str_repeat('*', max(1, $localLen - 3)) . $tail . '@' . $domain;
|
||||
}
|
||||
|
||||
// ✅ 기본 규칙: 앞 3글자 + ***** + 뒤 2글자
|
||||
$head = mb_substr($local, 0, 3, 'UTF-8');
|
||||
$tail = mb_substr($local, -2, 2, 'UTF-8');
|
||||
|
||||
// 별 개수: 최소 5개, 너무 길면 로컬 길이에 맞게 조정
|
||||
$stars = max(5, $localLen - 5); // head3 + tail2 = 5
|
||||
return $head . str_repeat('*', $stars) . $tail . '@' . $domain;
|
||||
}
|
||||
|
||||
}
|
||||
261
app/Http/Controllers/Web/Auth/FindPasswordController.php
Normal file
261
app/Http/Controllers/Web/Auth/FindPasswordController.php
Normal file
@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Auth;
|
||||
namespace App\Http\Controllers\Web\Auth;
|
||||
|
||||
use App\Services\MailService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MemInfo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FindPasswordController extends Controller
|
||||
{
|
||||
public function show(Request $request)
|
||||
{
|
||||
$sess = $request->session()->get('find_pw', []);
|
||||
|
||||
$step = 1;
|
||||
if (!empty($sess['verified'])) $step = 3;
|
||||
else if (!empty($sess['sent'])) $step = 2;
|
||||
|
||||
return view('web.auth.find_password', [
|
||||
'initialStep' => $step,
|
||||
'email' => $sess['email'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendCode(Request $request)
|
||||
{
|
||||
$v = Validator::make($request->all(), [
|
||||
'email' => ['required', 'email', 'max:120'],
|
||||
], [
|
||||
'email.required' => '이메일을 입력해 주세요.',
|
||||
'email.email' => '이메일 형식이 올바르지 않습니다.',
|
||||
]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 1) 레이트리밋(이메일 기준)
|
||||
$key = 'findpw:send:' . $email;
|
||||
if (RateLimiter::tooManyAttempts($key, 5)) { // 10분 5회 예시
|
||||
$sec = RateLimiter::availableIn($key);
|
||||
return response()->json(['ok' => false, 'message' => "요청이 너무 많습니다. {$sec}초 후 다시 시도해 주세요."], 429);
|
||||
}
|
||||
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,
|
||||
]);
|
||||
|
||||
// 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(),
|
||||
]);
|
||||
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)
|
||||
{
|
||||
$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' => '새 비밀번호를 입력해 주세요.',
|
||||
'new_password.min' => '비밀번호는 8자 이상으로 입력해 주세요.',
|
||||
'new_password.confirmed' => '비밀번호 확인이 일치하지 않습니다.',
|
||||
]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => '비밀번호가 변경되었습니다. 로그인해 주세요.',
|
||||
'redirect_url' => route('web.auth.login'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function resetSession(Request $request)
|
||||
{
|
||||
$request->session()->forget('find_pw');
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
}
|
||||
25
app/Mail/TemplateMail.php
Normal file
25
app/Mail/TemplateMail.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public string $subjectText,
|
||||
public string $viewName,
|
||||
public array $payload = []
|
||||
) {}
|
||||
|
||||
public function build()
|
||||
{
|
||||
return $this->subject($this->subjectText)
|
||||
->view($this->viewName, $this->payload);
|
||||
}
|
||||
}
|
||||
109
app/Models/MemInfo.php
Normal file
109
app/Models/MemInfo.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class MemInfo extends Model
|
||||
{
|
||||
protected $table = 'mem_info';
|
||||
protected $primaryKey = 'mem_no';
|
||||
public $incrementing = true;
|
||||
protected $keyType = 'int';
|
||||
|
||||
// mem_info는 created_at/updated_at 컬럼이 dt_reg/dt_mod 라서 기본 timestamps 안 씀
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* 대량 컬럼이지만, 일단 자주 쓰는 것만 명시
|
||||
* (전체를 fillable로 열어두지 마. 보안상 위험)
|
||||
*/
|
||||
protected $fillable = [
|
||||
'stat_1','stat_2','stat_3','stat_4','stat_5',
|
||||
'name','name_first','name_mid','name_last',
|
||||
'birth','gender','native',
|
||||
'cell_corp','cell_phone','email','pv_sns',
|
||||
'bank_code','bank_name','bank_act_num','bank_vact_num',
|
||||
'rcv_email','rcv_sms','rcv_push',
|
||||
'login_fail_cnt',
|
||||
'dt_login','dt_reg','dt_mod',
|
||||
'dt_rcv_email','dt_rcv_sms','dt_rcv_push',
|
||||
'dt_stat_1','dt_stat_2','dt_stat_3','dt_stat_4','dt_stat_5',
|
||||
'ip_reg','ci_v','ci','di',
|
||||
'country_code','country_name',
|
||||
'admin_memo','modify_log',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'birth' => 'date',
|
||||
'dt_login' => 'datetime',
|
||||
'dt_reg' => 'datetime',
|
||||
'dt_mod' => 'datetime',
|
||||
'dt_vact' => 'datetime',
|
||||
'dt_dor' => 'datetime',
|
||||
'dt_ret_dor' => 'datetime',
|
||||
'dt_out' => 'datetime',
|
||||
'dt_rcv_email' => 'datetime',
|
||||
'dt_rcv_sms' => 'datetime',
|
||||
'dt_rcv_push' => 'datetime',
|
||||
'dt_stat_1' => 'datetime',
|
||||
'dt_stat_2' => 'datetime',
|
||||
'dt_stat_3' => 'datetime',
|
||||
'dt_stat_4' => 'datetime',
|
||||
'dt_stat_5' => 'datetime',
|
||||
|
||||
// JSON 컬럼 (DB CHECK(json_valid()) 걸려있으니 array cast 쓰면 편함)
|
||||
'admin_memo' => 'array',
|
||||
'modify_log' => 'array',
|
||||
];
|
||||
|
||||
/*
|
||||
* ========== Scopes ==========
|
||||
*/
|
||||
|
||||
public function scopeActive(Builder $q): Builder
|
||||
{
|
||||
// CI에서 stat_3 == 3 접근금지 / 4 탈퇴신청 / 5 탈퇴완료
|
||||
return $q->whereNotIn('stat_3', ['3','4','5']);
|
||||
}
|
||||
|
||||
public function scopeByEmail(Builder $q, string $email): Builder
|
||||
{
|
||||
return $q->where('email', strtolower($email));
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ cell_phone이 "암호화 저장"이라면
|
||||
* 이 scope는 "정규화 컬럼(cell_phone_hash / cell_phone_norm 등)" 생긴 뒤에 완성하는 게 맞음.
|
||||
* 지금은 자리만 만들어 둠.
|
||||
*/
|
||||
public function scopeByPhoneLookup(Builder $q, string $phoneNormalized): Builder
|
||||
{
|
||||
// TODO: cell_phone이 암호화라면 단순 where 비교 불가
|
||||
// 예시(추천): cell_phone_hash 컬럼을 만들고 SHA256 같은 값으로 매칭
|
||||
// return $q->where('cell_phone_hash', hash('sha256', $phoneNormalized . config('app.key')));
|
||||
return $q;
|
||||
}
|
||||
|
||||
/*
|
||||
* ========== Helpers ==========
|
||||
*/
|
||||
|
||||
public function isBlocked(): bool
|
||||
{
|
||||
return $this->stat_3 === '3';
|
||||
}
|
||||
|
||||
public function isWithdrawnOrRequested(): bool
|
||||
{
|
||||
return in_array($this->stat_3, ['4','5'], true);
|
||||
}
|
||||
|
||||
public function isFirstLogin(): bool
|
||||
{
|
||||
if (!$this->dt_login || !$this->dt_reg) return false;
|
||||
return Carbon::parse($this->dt_login)->equalTo(Carbon::parse($this->dt_reg));
|
||||
}
|
||||
}
|
||||
14
app/Models/Sms/MmsMsg.php
Normal file
14
app/Models/Sms/MmsMsg.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Sms;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MmsMsg extends Model
|
||||
{
|
||||
protected $connection = 'sms_server';
|
||||
protected $table = 'MMS_MSG';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
13
app/Models/Sms/ScTran.php
Normal file
13
app/Models/Sms/ScTran.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace App\Models\Sms;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScTran extends Model
|
||||
{
|
||||
protected $connection = 'sms_server';
|
||||
protected $table = 'SC_TRAN';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@ -1,22 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->singleton(CiSeedCrypto::class, function () {
|
||||
return new CiSeedCrypto(
|
||||
config('legacy.seed_user_key_default'),
|
||||
config('legacy.iv'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
|
||||
24
app/Services/MailService.php
Normal file
24
app/Services/MailService.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\TemplateMail;
|
||||
|
||||
class MailService
|
||||
{
|
||||
/**
|
||||
* CI macro.sendmail 같은 역할
|
||||
* @param string|array $to
|
||||
*/
|
||||
public function sendTemplate($to, string $subject, string $view, array $data = []): void
|
||||
{
|
||||
$toList = is_array($to) ? $to : [$to];
|
||||
|
||||
foreach ($toList as $toEmail) {
|
||||
Mail::send($view, $data, function ($m) use ($toEmail, $subject) {
|
||||
$m->to($toEmail)->subject($subject);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/Services/MemInfoService.php
Normal file
147
app/Services/MemInfoService.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\MemInfo;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class MemInfoService
|
||||
{
|
||||
/**
|
||||
* CI: set_receive()
|
||||
* 프로모션 수신 동의 변경 (행 잠금)
|
||||
*/
|
||||
public function setReceive(int $memNo, string $rcvEmail, string $rcvSms, ?string $rcvPush = null): void
|
||||
{
|
||||
DB::transaction(function () use ($memNo, $rcvEmail, $rcvSms, $rcvPush) {
|
||||
/** @var MemInfo $mem */
|
||||
$mem = MemInfo::query()->whereKey($memNo)->lockForUpdate()->firstOrFail();
|
||||
|
||||
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||
|
||||
$mem->rcv_email = $rcvEmail;
|
||||
$mem->rcv_sms = $rcvSms;
|
||||
$mem->dt_rcv_email = $now;
|
||||
$mem->dt_rcv_sms = $now;
|
||||
|
||||
if ($rcvPush !== null) {
|
||||
$mem->rcv_push = $rcvPush;
|
||||
$mem->dt_rcv_push = $now;
|
||||
}
|
||||
|
||||
$mem->dt_mod = $now;
|
||||
$mem->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: mem_email_vali()
|
||||
*/
|
||||
public function emailInfo(string $email): ?MemInfo
|
||||
{
|
||||
return MemInfo::query()
|
||||
->select(['mem_no','stat_3','dt_req_out','email'])
|
||||
->where('email', strtolower($email))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: mem_reg() (간소화 버전)
|
||||
* - 실제로는 validation은 FormRequest에서 처리 권장
|
||||
* - stat_3=3 (1969 이전 출생 접근금지) 룰 포함
|
||||
*/
|
||||
public function register(array $data): MemInfo
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
|
||||
$email = strtolower($data['email']);
|
||||
|
||||
// 중복 체크 + 잠금 (CI for update)
|
||||
$exists = MemInfo::query()
|
||||
->where('email', $email)
|
||||
->lockForUpdate()
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw new \RuntimeException('이미 가입된 아이디 입니다. 다른 아이디로 진행해 주세요.');
|
||||
}
|
||||
|
||||
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||
|
||||
$mem = new MemInfo();
|
||||
$mem->email = $email;
|
||||
$mem->name = $data['name'] ?? '';
|
||||
$mem->pv_sns = $data['pv_sns'] ?? 'self';
|
||||
|
||||
$promotion = !empty($data['promotion']) ? 'y' : 'n';
|
||||
$mem->rcv_email = $promotion;
|
||||
$mem->rcv_sms = $promotion;
|
||||
$mem->rcv_push = $promotion;
|
||||
|
||||
$mem->dt_reg = $now;
|
||||
$mem->dt_login = $now;
|
||||
|
||||
$mem->dt_rcv_email = $now;
|
||||
$mem->dt_rcv_sms = $now;
|
||||
$mem->dt_rcv_push = $now;
|
||||
|
||||
$mem->dt_stat_1 = $now;
|
||||
$mem->dt_stat_2 = $now;
|
||||
$mem->dt_stat_3 = $now;
|
||||
$mem->dt_stat_4 = $now;
|
||||
$mem->dt_stat_5 = $now;
|
||||
|
||||
$mem->ip_reg = $data['ip_reg'] ?? request()->ip();
|
||||
|
||||
// 국가/본인인증 값들
|
||||
$mem->country_code = $data['country_code'] ?? '';
|
||||
$mem->country_name = $data['country_name'] ?? '';
|
||||
$mem->birth = $data['birth'] ?? '0000-00-00';
|
||||
$mem->cell_corp = $data['cell_corp'] ?? 'n';
|
||||
$mem->cell_phone = $data['cell_phone'] ?? ''; // ⚠️ 암호화 저장이라면 여기서 암호화해서 넣어야 함
|
||||
$mem->native = $data['native'] ?? 'n';
|
||||
$mem->ci = $data['ci'] ?? null;
|
||||
$mem->ci_v = $data['ci_v'] ?? '';
|
||||
$mem->di = $data['di'] ?? null;
|
||||
$mem->gender = $data['gender'] ?? 'n';
|
||||
|
||||
// 1969년 이전 출생 접근금지(stat_3=3)
|
||||
$birthY = (int)substr((string)$mem->birth, 0, 4);
|
||||
if ($birthY > 0 && $birthY <= 1969) {
|
||||
$mem->stat_3 = '3';
|
||||
}
|
||||
|
||||
$mem->save();
|
||||
|
||||
return $mem;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: last_login()
|
||||
*/
|
||||
public function updateLastLogin(int $memNo): void
|
||||
{
|
||||
MemInfo::query()
|
||||
->whereKey($memNo)
|
||||
->update([
|
||||
'dt_login' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
'login_fail_cnt' => 0,
|
||||
'dt_mod' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: fail_count()
|
||||
*/
|
||||
public function incrementLoginFail(int $memNo): void
|
||||
{
|
||||
MemInfo::query()
|
||||
->whereKey($memNo)
|
||||
->update([
|
||||
'login_fail_cnt' => DB::raw('login_fail_cnt + 1'),
|
||||
'dt_mod' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
156
app/Services/SmsService.php
Normal file
156
app/Services/SmsService.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Sms\ScTran;
|
||||
use App\Models\Sms\MmsMsg;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class SmsService
|
||||
{
|
||||
private string $companyName = 'lguplus';
|
||||
|
||||
/**
|
||||
* @param array $payload
|
||||
* - from_number (필수)
|
||||
* - to_number (필수)
|
||||
* - message (필수)
|
||||
* - subject (mms일 때 없으면 자동 생성)
|
||||
* - sms_type (sms|mms) (없으면 길이로 판단)
|
||||
* - country (없으면 세션/기본 82)
|
||||
*
|
||||
* @param string|null $companyName
|
||||
* @return bool
|
||||
*/
|
||||
public function send(array $payload, ?string $companyName = null): bool
|
||||
{
|
||||
try {
|
||||
if ($companyName) {
|
||||
$this->companyName = $companyName;
|
||||
}
|
||||
|
||||
// 1) 필수값 체크 (CI의 Param_exception 대체)
|
||||
if (empty($payload['from_number'])) return false;
|
||||
if (empty($payload['to_number'])) return false;
|
||||
if (empty($payload['message'])) return false;
|
||||
|
||||
// 2) country 결정 (CI sess['_mcountry_code'] 로직 대응)
|
||||
$country = $payload['country'] ?? null;
|
||||
|
||||
if ($country === null) {
|
||||
$sessCountry = Session::get('_mcountry_code'); // 기존 키 그대로 사용
|
||||
if (!empty($sessCountry)) {
|
||||
$country = ($sessCountry === '82' || $sessCountry === '') ? '82' : $sessCountry;
|
||||
}
|
||||
}
|
||||
$payload['country'] = $country ?: '82';
|
||||
|
||||
// 3) 수신번호 체크/정리
|
||||
if (!$this->phoneNumberCheck($payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4) 업체별 발송
|
||||
return match ($this->companyName) {
|
||||
'lguplus' => $this->lguplusSend($payload),
|
||||
'sms2' => $this->sms2Send($payload),
|
||||
default => false,
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
// 운영에서는 로깅 권장
|
||||
// logger()->error('SmsService send failed', ['e' => $e]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: phonenumber_check 로직 이식
|
||||
* - country=82면 숫자만 남기고 01xxxxxxxxx 형식만 허용
|
||||
*/
|
||||
private function phoneNumberCheck(array &$payload): bool
|
||||
{
|
||||
if (($payload['country'] ?? '82') === '82') {
|
||||
$num = preg_replace("/[^0-9]/", "", $payload['to_number'] ?? '');
|
||||
if (preg_match("/^01[0-9]{8,9}$/", $num)) {
|
||||
$payload['to_number'] = $num;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 해외는 CI 원본처럼 그냥 통과(필요하면 국가별 검증 로직 추가)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* CI: lguplus_send 이식
|
||||
* - 메시지 길이(EUC-KR) 기준 90 이하 sms, 초과 mms
|
||||
* - sms_type이 명시되면 그걸 우선
|
||||
*/
|
||||
private function lguplusSend(array $data): bool
|
||||
{
|
||||
$conn = DB::connection('sms_server');
|
||||
|
||||
// sms/mms 결정
|
||||
$smsSendType = $this->resolveSendType($data);
|
||||
|
||||
return $conn->transaction(function () use ($smsSendType, $data) {
|
||||
if ($smsSendType === 'sms') {
|
||||
// CI의 SC_TRAN insert
|
||||
$insert = [
|
||||
'TR_SENDDATE' => now()->format('Y-m-d H:i:s'),
|
||||
'TR_SENDSTAT' => '0',
|
||||
'TR_MSGTYPE' => '0',
|
||||
'TR_PHONE' => $data['to_number'],
|
||||
'TR_CALLBACK' => $data['from_number'],
|
||||
'TR_MSG' => $data['message'],
|
||||
];
|
||||
|
||||
// Eloquent 사용
|
||||
return (bool) ScTran::create($insert);
|
||||
// 또는 Query Builder:
|
||||
// return $conn->table('SC_TRAN')->insert($insert);
|
||||
|
||||
} else {
|
||||
// CI의 MMS_MSG insert
|
||||
$subject = $data['subject'] ?? mb_substr($data['message'], 0, 22, 'UTF-8');
|
||||
|
||||
$insert = [
|
||||
'SUBJECT' => $subject,
|
||||
'PHONE' => $data['to_number'],
|
||||
'CALLBACK' => $data['from_number'],
|
||||
'STATUS' => '0',
|
||||
'REQDATE' => now()->format('Y-m-d H:i:s'),
|
||||
'MSG' => $data['message'],
|
||||
'FILE_CNT' => 0,
|
||||
'FILE_PATH1' => '',
|
||||
'TYPE' => '0',
|
||||
];
|
||||
|
||||
return (bool) MmsMsg::create($insert);
|
||||
// 또는 Query Builder:
|
||||
// return $conn->table('MMS_MSG')->insert($insert);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function resolveSendType(array $data): string
|
||||
{
|
||||
// CI 로직과 동일한 우선순위 유지
|
||||
if (!empty($data['sms_type'])) {
|
||||
if ($data['sms_type'] === 'sms') {
|
||||
return (mb_strlen($data['message'], 'EUC-KR') <= 90) ? 'sms' : 'mms';
|
||||
}
|
||||
return 'mms';
|
||||
}
|
||||
|
||||
return (mb_strlen($data['message'], 'EUC-KR') <= 90) ? 'sms' : 'mms';
|
||||
}
|
||||
|
||||
private function sms2Send(array $data): bool
|
||||
{
|
||||
// TODO: 업체 연동 시 구현
|
||||
return true;
|
||||
}
|
||||
}
|
||||
211
app/Support/LegacyCrypto/CiSeedCrypto.php
Normal file
211
app/Support/LegacyCrypto/CiSeedCrypto.php
Normal file
@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\LegacyCrypto;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CiSeedCrypto
|
||||
{
|
||||
private array $userKeyBytes; // int[16] signed bytes
|
||||
private array $iv; // int[16]
|
||||
private string $serverEncoding;
|
||||
private string $innerEncoding;
|
||||
private int $block;
|
||||
|
||||
public function __construct(string $userKey, array $iv)
|
||||
{
|
||||
$this->serverEncoding = config('legacy.server_encoding', 'UTF-8');
|
||||
$this->innerEncoding = config('legacy.inner_encoding', 'UTF-8');
|
||||
$this->block = (int) config('legacy.block', 16);
|
||||
|
||||
if (count($iv) !== 16) {
|
||||
throw new RuntimeException('legacy.iv must be 16 bytes');
|
||||
}
|
||||
$this->iv = array_values($iv);
|
||||
|
||||
// 핵심: CI SeedRoundKey는 pbUserKey[0..15]만 사용 → 문자열 키의 "앞 16바이트"만 의미 있음
|
||||
$this->userKeyBytes = $this->stringKeyToSigned16Bytes($userKey);
|
||||
}
|
||||
|
||||
public function encrypt(string $plain): string
|
||||
{
|
||||
$plain = $this->convertEncoding($plain);
|
||||
if ($plain === '') return $plain;
|
||||
|
||||
$planBytes = $this->unpackSignedBytes($plain);
|
||||
|
||||
$seed = new Seed();
|
||||
$pdwRoundKey = null;
|
||||
$seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);
|
||||
|
||||
$planLen = count($planBytes);
|
||||
$start = 0;
|
||||
$end = 0;
|
||||
|
||||
$cbcBlock = [];
|
||||
$this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);
|
||||
|
||||
$ret = '';
|
||||
|
||||
while ($end < $planLen) {
|
||||
$end = $start + $this->block;
|
||||
if ($end > $planLen) $end = $planLen;
|
||||
|
||||
$cipherBlock = [];
|
||||
$this->arrayCopy($planBytes, $start, $cipherBlock, 0, $end - $start);
|
||||
|
||||
// PKCS#5 padding
|
||||
$nPad = $this->block - ($end - $start);
|
||||
for ($i = ($end - $start); $i < $this->block; $i++) {
|
||||
$cipherBlock[$i] = $nPad;
|
||||
}
|
||||
|
||||
// CBC XOR
|
||||
$this->xor16($cipherBlock, $cbcBlock, $cipherBlock);
|
||||
|
||||
// Encrypt
|
||||
$encBlock = null;
|
||||
$seed->SeedEncrypt($cipherBlock, $pdwRoundKey, $encBlock);
|
||||
|
||||
// CBC 갱신
|
||||
$this->arrayCopy($encBlock, 0, $cbcBlock, 0, $this->block);
|
||||
|
||||
foreach ($encBlock as $b) {
|
||||
$ret .= bin2hex(chr($b & 0xFF));
|
||||
}
|
||||
|
||||
$start = $end;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function decrypt(string $cipherHex): string
|
||||
{
|
||||
$cipherHex = trim($cipherHex);
|
||||
if ($cipherHex === '') return '';
|
||||
|
||||
if (strlen($cipherHex) % 32 !== 0) {
|
||||
throw new RuntimeException('Invalid cipher hex length (must be multiple of 32)');
|
||||
}
|
||||
|
||||
$seed = new Seed();
|
||||
$pdwRoundKey = null;
|
||||
$seed->SeedRoundKey($pdwRoundKey, $this->userKeyBytes);
|
||||
|
||||
$cbcBlock = [];
|
||||
$this->arrayCopy($this->iv, 0, $cbcBlock, 0, $this->block);
|
||||
|
||||
$plainBytes = [];
|
||||
$blocks = (int)(strlen($cipherHex) / 32);
|
||||
|
||||
for ($bi = 0; $bi < $blocks; $bi++) {
|
||||
$hexBlock = substr($cipherHex, $bi * 32, 32);
|
||||
$cipherBlock = $this->hexToBytesSigned($hexBlock); // signed 16 bytes
|
||||
|
||||
$decBlock = null;
|
||||
$seed->SeedDecrypt($cipherBlock, $pdwRoundKey, $decBlock);
|
||||
|
||||
$plainBlock = [];
|
||||
$this->xor16($decBlock, $cbcBlock, $plainBlock);
|
||||
|
||||
// CBC 갱신
|
||||
$this->arrayCopy($cipherBlock, 0, $cbcBlock, 0, $this->block);
|
||||
|
||||
foreach ($plainBlock as $b) {
|
||||
$plainBytes[] = $b;
|
||||
}
|
||||
}
|
||||
|
||||
$plainBytes = $this->pkcs5Unpad($plainBytes);
|
||||
$plain = $this->packSignedBytes($plainBytes);
|
||||
|
||||
return $this->convertEncodingBack($plain);
|
||||
}
|
||||
|
||||
/* -------------------- KEY NORMALIZE (핵심) -------------------- */
|
||||
|
||||
private function stringKeyToSigned16Bytes(string $key): array
|
||||
{
|
||||
// 레거시(운영 CI)에서 "문자열을 배열처럼 접근 + & 연산"하던 동작을 최대한 재현
|
||||
// 문자 1개를 (int)로 캐스팅: 숫자면 0~9, 숫자 아니면 0
|
||||
$bytes = [];
|
||||
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$ch = $key[$i] ?? "\0"; // 1-char string
|
||||
$v = (int) $ch; // 핵심: ord()가 아니라 int 캐스팅
|
||||
// signed byte 범위 맞추기(사실 0~9라 필요없지만 안전)
|
||||
if ($v > 127) $v -= 256;
|
||||
$bytes[] = $v;
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/* -------------------- UTIL -------------------- */
|
||||
|
||||
private function convertEncoding(string $s): string
|
||||
{
|
||||
$out = @iconv($this->serverEncoding, $this->innerEncoding, $s);
|
||||
return $out === false ? $s : $out;
|
||||
}
|
||||
|
||||
private function convertEncodingBack(string $s): string
|
||||
{
|
||||
$out = @iconv($this->innerEncoding, $this->serverEncoding, $s);
|
||||
return $out === false ? $s : $out;
|
||||
}
|
||||
|
||||
private function unpackSignedBytes(string $s): array
|
||||
{
|
||||
return array_values(unpack('c*', $s));
|
||||
}
|
||||
|
||||
private function packSignedBytes(array $bytes): string
|
||||
{
|
||||
$out = '';
|
||||
foreach ($bytes as $b) {
|
||||
$out .= chr($b & 0xFF);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function hexToBytesSigned(string $hexBlock): array
|
||||
{
|
||||
$bin = hex2bin($hexBlock);
|
||||
if ($bin === false || strlen($bin) !== 16) {
|
||||
throw new RuntimeException('Invalid hex block');
|
||||
}
|
||||
return $this->unpackSignedBytes($bin);
|
||||
}
|
||||
|
||||
private function pkcs5Unpad(array $bytes): array
|
||||
{
|
||||
$n = count($bytes);
|
||||
if ($n === 0) return $bytes;
|
||||
|
||||
$pad = $bytes[$n - 1] & 0xFF;
|
||||
if ($pad < 1 || $pad > 16) return $bytes;
|
||||
|
||||
for ($i = 0; $i < $pad; $i++) {
|
||||
if (($bytes[$n - 1 - $i] & 0xFF) !== $pad) {
|
||||
return $bytes;
|
||||
}
|
||||
}
|
||||
return array_slice($bytes, 0, $n - $pad);
|
||||
}
|
||||
|
||||
private function xor16(array $a, array $b, array &$out): void
|
||||
{
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$out[$i] = ($a[$i] ^ $b[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
private function arrayCopy(array $src, int $srcPos, array &$dst, int $dstPos, int $length): void
|
||||
{
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$dst[$dstPos + $i] = $src[$srcPos + $i];
|
||||
}
|
||||
}
|
||||
}
|
||||
679
app/Support/LegacyCrypto/Seed.php
Normal file
679
app/Support/LegacyCrypto/Seed.php
Normal file
@ -0,0 +1,679 @@
|
||||
<?php
|
||||
namespace App\Support\LegacyCrypto;
|
||||
class Seed
|
||||
{
|
||||
private $SS0 = array(
|
||||
0x2989a1a8, 0x05858184, 0x16c6d2d4, 0x13c3d3d0, 0x14445054, 0x1d0d111c, 0x2c8ca0ac, 0x25052124,
|
||||
0x1d4d515c, 0x03434340, 0x18081018, 0x1e0e121c, 0x11415150, 0x3cccf0fc, 0x0acac2c8, 0x23436360,
|
||||
0x28082028, 0x04444044, 0x20002020, 0x1d8d919c, 0x20c0e0e0, 0x22c2e2e0, 0x08c8c0c8, 0x17071314,
|
||||
0x2585a1a4, 0x0f8f838c, 0x03030300, 0x3b4b7378, 0x3b8bb3b8, 0x13031310, 0x12c2d2d0, 0x2ecee2ec,
|
||||
0x30407070, 0x0c8c808c, 0x3f0f333c, 0x2888a0a8, 0x32023230, 0x1dcdd1dc, 0x36c6f2f4, 0x34447074,
|
||||
0x2ccce0ec, 0x15859194, 0x0b0b0308, 0x17475354, 0x1c4c505c, 0x1b4b5358, 0x3d8db1bc, 0x01010100,
|
||||
0x24042024, 0x1c0c101c, 0x33437370, 0x18889098, 0x10001010, 0x0cccc0cc, 0x32c2f2f0, 0x19c9d1d8,
|
||||
0x2c0c202c, 0x27c7e3e4, 0x32427270, 0x03838380, 0x1b8b9398, 0x11c1d1d0, 0x06868284, 0x09c9c1c8,
|
||||
0x20406060, 0x10405050, 0x2383a3a0, 0x2bcbe3e8, 0x0d0d010c, 0x3686b2b4, 0x1e8e929c, 0x0f4f434c,
|
||||
0x3787b3b4, 0x1a4a5258, 0x06c6c2c4, 0x38487078, 0x2686a2a4, 0x12021210, 0x2f8fa3ac, 0x15c5d1d4,
|
||||
0x21416160, 0x03c3c3c0, 0x3484b0b4, 0x01414140, 0x12425250, 0x3d4d717c, 0x0d8d818c, 0x08080008,
|
||||
0x1f0f131c, 0x19899198, 0x00000000, 0x19091118, 0x04040004, 0x13435350, 0x37c7f3f4, 0x21c1e1e0,
|
||||
0x3dcdf1fc, 0x36467274, 0x2f0f232c, 0x27072324, 0x3080b0b0, 0x0b8b8388, 0x0e0e020c, 0x2b8ba3a8,
|
||||
0x2282a2a0, 0x2e4e626c, 0x13839390, 0x0d4d414c, 0x29496168, 0x3c4c707c, 0x09090108, 0x0a0a0208,
|
||||
0x3f8fb3bc, 0x2fcfe3ec, 0x33c3f3f0, 0x05c5c1c4, 0x07878384, 0x14041014, 0x3ecef2fc, 0x24446064,
|
||||
0x1eced2dc, 0x2e0e222c, 0x0b4b4348, 0x1a0a1218, 0x06060204, 0x21012120, 0x2b4b6368, 0x26466264,
|
||||
0x02020200, 0x35c5f1f4, 0x12829290, 0x0a8a8288, 0x0c0c000c, 0x3383b3b0, 0x3e4e727c, 0x10c0d0d0,
|
||||
0x3a4a7278, 0x07474344, 0x16869294, 0x25c5e1e4, 0x26062224, 0x00808080, 0x2d8da1ac, 0x1fcfd3dc,
|
||||
0x2181a1a0, 0x30003030, 0x37073334, 0x2e8ea2ac, 0x36063234, 0x15051114, 0x22022220, 0x38083038,
|
||||
0x34c4f0f4, 0x2787a3a4, 0x05454144, 0x0c4c404c, 0x01818180, 0x29c9e1e8, 0x04848084, 0x17879394,
|
||||
0x35053134, 0x0bcbc3c8, 0x0ecec2cc, 0x3c0c303c, 0x31417170, 0x11011110, 0x07c7c3c4, 0x09898188,
|
||||
0x35457174, 0x3bcbf3f8, 0x1acad2d8, 0x38c8f0f8, 0x14849094, 0x19495158, 0x02828280, 0x04c4c0c4,
|
||||
0x3fcff3fc, 0x09494148, 0x39093138, 0x27476364, 0x00c0c0c0, 0x0fcfc3cc, 0x17c7d3d4, 0x3888b0b8,
|
||||
0x0f0f030c, 0x0e8e828c, 0x02424240, 0x23032320, 0x11819190, 0x2c4c606c, 0x1bcbd3d8, 0x2484a0a4,
|
||||
0x34043034, 0x31c1f1f0, 0x08484048, 0x02c2c2c0, 0x2f4f636c, 0x3d0d313c, 0x2d0d212c, 0x00404040,
|
||||
0x3e8eb2bc, 0x3e0e323c, 0x3c8cb0bc, 0x01c1c1c0, 0x2a8aa2a8, 0x3a8ab2b8, 0x0e4e424c, 0x15455154,
|
||||
0x3b0b3338, 0x1cccd0dc, 0x28486068, 0x3f4f737c, 0x1c8c909c, 0x18c8d0d8, 0x0a4a4248, 0x16465254,
|
||||
0x37477374, 0x2080a0a0, 0x2dcde1ec, 0x06464244, 0x3585b1b4, 0x2b0b2328, 0x25456164, 0x3acaf2f8,
|
||||
0x23c3e3e0, 0x3989b1b8, 0x3181b1b0, 0x1f8f939c, 0x1e4e525c, 0x39c9f1f8, 0x26c6e2e4, 0x3282b2b0,
|
||||
0x31013130, 0x2acae2e8, 0x2d4d616c, 0x1f4f535c, 0x24c4e0e4, 0x30c0f0f0, 0x0dcdc1cc, 0x08888088,
|
||||
0x16061214, 0x3a0a3238, 0x18485058, 0x14c4d0d4, 0x22426260, 0x29092128, 0x07070304, 0x33033330,
|
||||
0x28c8e0e8, 0x1b0b1318, 0x05050104, 0x39497178, 0x10809090, 0x2a4a6268, 0x2a0a2228, 0x1a8a9298
|
||||
);
|
||||
|
||||
private $SS1 = array(
|
||||
0x38380830, 0xe828c8e0, 0x2c2d0d21, 0xa42686a2, 0xcc0fcfc3, 0xdc1eced2, 0xb03383b3, 0xb83888b0,
|
||||
0xac2f8fa3, 0x60204060, 0x54154551, 0xc407c7c3, 0x44044440, 0x6c2f4f63, 0x682b4b63, 0x581b4b53,
|
||||
0xc003c3c3, 0x60224262, 0x30330333, 0xb43585b1, 0x28290921, 0xa02080a0, 0xe022c2e2, 0xa42787a3,
|
||||
0xd013c3d3, 0x90118191, 0x10110111, 0x04060602, 0x1c1c0c10, 0xbc3c8cb0, 0x34360632, 0x480b4b43,
|
||||
0xec2fcfe3, 0x88088880, 0x6c2c4c60, 0xa82888a0, 0x14170713, 0xc404c4c0, 0x14160612, 0xf434c4f0,
|
||||
0xc002c2c2, 0x44054541, 0xe021c1e1, 0xd416c6d2, 0x3c3f0f33, 0x3c3d0d31, 0x8c0e8e82, 0x98188890,
|
||||
0x28280820, 0x4c0e4e42, 0xf436c6f2, 0x3c3e0e32, 0xa42585a1, 0xf839c9f1, 0x0c0d0d01, 0xdc1fcfd3,
|
||||
0xd818c8d0, 0x282b0b23, 0x64264662, 0x783a4a72, 0x24270723, 0x2c2f0f23, 0xf031c1f1, 0x70324272,
|
||||
0x40024242, 0xd414c4d0, 0x40014141, 0xc000c0c0, 0x70334373, 0x64274763, 0xac2c8ca0, 0x880b8b83,
|
||||
0xf437c7f3, 0xac2d8da1, 0x80008080, 0x1c1f0f13, 0xc80acac2, 0x2c2c0c20, 0xa82a8aa2, 0x34340430,
|
||||
0xd012c2d2, 0x080b0b03, 0xec2ecee2, 0xe829c9e1, 0x5c1d4d51, 0x94148490, 0x18180810, 0xf838c8f0,
|
||||
0x54174753, 0xac2e8ea2, 0x08080800, 0xc405c5c1, 0x10130313, 0xcc0dcdc1, 0x84068682, 0xb83989b1,
|
||||
0xfc3fcff3, 0x7c3d4d71, 0xc001c1c1, 0x30310131, 0xf435c5f1, 0x880a8a82, 0x682a4a62, 0xb03181b1,
|
||||
0xd011c1d1, 0x20200020, 0xd417c7d3, 0x00020202, 0x20220222, 0x04040400, 0x68284860, 0x70314171,
|
||||
0x04070703, 0xd81bcbd3, 0x9c1d8d91, 0x98198991, 0x60214161, 0xbc3e8eb2, 0xe426c6e2, 0x58194951,
|
||||
0xdc1dcdd1, 0x50114151, 0x90108090, 0xdc1cccd0, 0x981a8a92, 0xa02383a3, 0xa82b8ba3, 0xd010c0d0,
|
||||
0x80018181, 0x0c0f0f03, 0x44074743, 0x181a0a12, 0xe023c3e3, 0xec2ccce0, 0x8c0d8d81, 0xbc3f8fb3,
|
||||
0x94168692, 0x783b4b73, 0x5c1c4c50, 0xa02282a2, 0xa02181a1, 0x60234363, 0x20230323, 0x4c0d4d41,
|
||||
0xc808c8c0, 0x9c1e8e92, 0x9c1c8c90, 0x383a0a32, 0x0c0c0c00, 0x2c2e0e22, 0xb83a8ab2, 0x6c2e4e62,
|
||||
0x9c1f8f93, 0x581a4a52, 0xf032c2f2, 0x90128292, 0xf033c3f3, 0x48094941, 0x78384870, 0xcc0cccc0,
|
||||
0x14150511, 0xf83bcbf3, 0x70304070, 0x74354571, 0x7c3f4f73, 0x34350531, 0x10100010, 0x00030303,
|
||||
0x64244460, 0x6c2d4d61, 0xc406c6c2, 0x74344470, 0xd415c5d1, 0xb43484b0, 0xe82acae2, 0x08090901,
|
||||
0x74364672, 0x18190911, 0xfc3ecef2, 0x40004040, 0x10120212, 0xe020c0e0, 0xbc3d8db1, 0x04050501,
|
||||
0xf83acaf2, 0x00010101, 0xf030c0f0, 0x282a0a22, 0x5c1e4e52, 0xa82989a1, 0x54164652, 0x40034343,
|
||||
0x84058581, 0x14140410, 0x88098981, 0x981b8b93, 0xb03080b0, 0xe425c5e1, 0x48084840, 0x78394971,
|
||||
0x94178793, 0xfc3cccf0, 0x1c1e0e12, 0x80028282, 0x20210121, 0x8c0c8c80, 0x181b0b13, 0x5c1f4f53,
|
||||
0x74374773, 0x54144450, 0xb03282b2, 0x1c1d0d11, 0x24250521, 0x4c0f4f43, 0x00000000, 0x44064642,
|
||||
0xec2dcde1, 0x58184850, 0x50124252, 0xe82bcbe3, 0x7c3e4e72, 0xd81acad2, 0xc809c9c1, 0xfc3dcdf1,
|
||||
0x30300030, 0x94158591, 0x64254561, 0x3c3c0c30, 0xb43686b2, 0xe424c4e0, 0xb83b8bb3, 0x7c3c4c70,
|
||||
0x0c0e0e02, 0x50104050, 0x38390931, 0x24260622, 0x30320232, 0x84048480, 0x68294961, 0x90138393,
|
||||
0x34370733, 0xe427c7e3, 0x24240420, 0xa42484a0, 0xc80bcbc3, 0x50134353, 0x080a0a02, 0x84078783,
|
||||
0xd819c9d1, 0x4c0c4c40, 0x80038383, 0x8c0f8f83, 0xcc0ecec2, 0x383b0b33, 0x480a4a42, 0xb43787b3
|
||||
);
|
||||
|
||||
private $SS2 = array(
|
||||
0xa1a82989, 0x81840585, 0xd2d416c6, 0xd3d013c3, 0x50541444, 0x111c1d0d, 0xa0ac2c8c, 0x21242505,
|
||||
0x515c1d4d, 0x43400343, 0x10181808, 0x121c1e0e, 0x51501141, 0xf0fc3ccc, 0xc2c80aca, 0x63602343,
|
||||
0x20282808, 0x40440444, 0x20202000, 0x919c1d8d, 0xe0e020c0, 0xe2e022c2, 0xc0c808c8, 0x13141707,
|
||||
0xa1a42585, 0x838c0f8f, 0x03000303, 0x73783b4b, 0xb3b83b8b, 0x13101303, 0xd2d012c2, 0xe2ec2ece,
|
||||
0x70703040, 0x808c0c8c, 0x333c3f0f, 0xa0a82888, 0x32303202, 0xd1dc1dcd, 0xf2f436c6, 0x70743444,
|
||||
0xe0ec2ccc, 0x91941585, 0x03080b0b, 0x53541747, 0x505c1c4c, 0x53581b4b, 0xb1bc3d8d, 0x01000101,
|
||||
0x20242404, 0x101c1c0c, 0x73703343, 0x90981888, 0x10101000, 0xc0cc0ccc, 0xf2f032c2, 0xd1d819c9,
|
||||
0x202c2c0c, 0xe3e427c7, 0x72703242, 0x83800383, 0x93981b8b, 0xd1d011c1, 0x82840686, 0xc1c809c9,
|
||||
0x60602040, 0x50501040, 0xa3a02383, 0xe3e82bcb, 0x010c0d0d, 0xb2b43686, 0x929c1e8e, 0x434c0f4f,
|
||||
0xb3b43787, 0x52581a4a, 0xc2c406c6, 0x70783848, 0xa2a42686, 0x12101202, 0xa3ac2f8f, 0xd1d415c5,
|
||||
0x61602141, 0xc3c003c3, 0xb0b43484, 0x41400141, 0x52501242, 0x717c3d4d, 0x818c0d8d, 0x00080808,
|
||||
0x131c1f0f, 0x91981989, 0x00000000, 0x11181909, 0x00040404, 0x53501343, 0xf3f437c7, 0xe1e021c1,
|
||||
0xf1fc3dcd, 0x72743646, 0x232c2f0f, 0x23242707, 0xb0b03080, 0x83880b8b, 0x020c0e0e, 0xa3a82b8b,
|
||||
0xa2a02282, 0x626c2e4e, 0x93901383, 0x414c0d4d, 0x61682949, 0x707c3c4c, 0x01080909, 0x02080a0a,
|
||||
0xb3bc3f8f, 0xe3ec2fcf, 0xf3f033c3, 0xc1c405c5, 0x83840787, 0x10141404, 0xf2fc3ece, 0x60642444,
|
||||
0xd2dc1ece, 0x222c2e0e, 0x43480b4b, 0x12181a0a, 0x02040606, 0x21202101, 0x63682b4b, 0x62642646,
|
||||
0x02000202, 0xf1f435c5, 0x92901282, 0x82880a8a, 0x000c0c0c, 0xb3b03383, 0x727c3e4e, 0xd0d010c0,
|
||||
0x72783a4a, 0x43440747, 0x92941686, 0xe1e425c5, 0x22242606, 0x80800080, 0xa1ac2d8d, 0xd3dc1fcf,
|
||||
0xa1a02181, 0x30303000, 0x33343707, 0xa2ac2e8e, 0x32343606, 0x11141505, 0x22202202, 0x30383808,
|
||||
0xf0f434c4, 0xa3a42787, 0x41440545, 0x404c0c4c, 0x81800181, 0xe1e829c9, 0x80840484, 0x93941787,
|
||||
0x31343505, 0xc3c80bcb, 0xc2cc0ece, 0x303c3c0c, 0x71703141, 0x11101101, 0xc3c407c7, 0x81880989,
|
||||
0x71743545, 0xf3f83bcb, 0xd2d81aca, 0xf0f838c8, 0x90941484, 0x51581949, 0x82800282, 0xc0c404c4,
|
||||
0xf3fc3fcf, 0x41480949, 0x31383909, 0x63642747, 0xc0c000c0, 0xc3cc0fcf, 0xd3d417c7, 0xb0b83888,
|
||||
0x030c0f0f, 0x828c0e8e, 0x42400242, 0x23202303, 0x91901181, 0x606c2c4c, 0xd3d81bcb, 0xa0a42484,
|
||||
0x30343404, 0xf1f031c1, 0x40480848, 0xc2c002c2, 0x636c2f4f, 0x313c3d0d, 0x212c2d0d, 0x40400040,
|
||||
0xb2bc3e8e, 0x323c3e0e, 0xb0bc3c8c, 0xc1c001c1, 0xa2a82a8a, 0xb2b83a8a, 0x424c0e4e, 0x51541545,
|
||||
0x33383b0b, 0xd0dc1ccc, 0x60682848, 0x737c3f4f, 0x909c1c8c, 0xd0d818c8, 0x42480a4a, 0x52541646,
|
||||
0x73743747, 0xa0a02080, 0xe1ec2dcd, 0x42440646, 0xb1b43585, 0x23282b0b, 0x61642545, 0xf2f83aca,
|
||||
0xe3e023c3, 0xb1b83989, 0xb1b03181, 0x939c1f8f, 0x525c1e4e, 0xf1f839c9, 0xe2e426c6, 0xb2b03282,
|
||||
0x31303101, 0xe2e82aca, 0x616c2d4d, 0x535c1f4f, 0xe0e424c4, 0xf0f030c0, 0xc1cc0dcd, 0x80880888,
|
||||
0x12141606, 0x32383a0a, 0x50581848, 0xd0d414c4, 0x62602242, 0x21282909, 0x03040707, 0x33303303,
|
||||
0xe0e828c8, 0x13181b0b, 0x01040505, 0x71783949, 0x90901080, 0x62682a4a, 0x22282a0a, 0x92981a8a
|
||||
);
|
||||
|
||||
private $SS3 = array(
|
||||
0x08303838, 0xc8e0e828, 0x0d212c2d, 0x86a2a426, 0xcfc3cc0f, 0xced2dc1e, 0x83b3b033, 0x88b0b838,
|
||||
0x8fa3ac2f, 0x40606020, 0x45515415, 0xc7c3c407, 0x44404404, 0x4f636c2f, 0x4b63682b, 0x4b53581b,
|
||||
0xc3c3c003, 0x42626022, 0x03333033, 0x85b1b435, 0x09212829, 0x80a0a020, 0xc2e2e022, 0x87a3a427,
|
||||
0xc3d3d013, 0x81919011, 0x01111011, 0x06020406, 0x0c101c1c, 0x8cb0bc3c, 0x06323436, 0x4b43480b,
|
||||
0xcfe3ec2f, 0x88808808, 0x4c606c2c, 0x88a0a828, 0x07131417, 0xc4c0c404, 0x06121416, 0xc4f0f434,
|
||||
0xc2c2c002, 0x45414405, 0xc1e1e021, 0xc6d2d416, 0x0f333c3f, 0x0d313c3d, 0x8e828c0e, 0x88909818,
|
||||
0x08202828, 0x4e424c0e, 0xc6f2f436, 0x0e323c3e, 0x85a1a425, 0xc9f1f839, 0x0d010c0d, 0xcfd3dc1f,
|
||||
0xc8d0d818, 0x0b23282b, 0x46626426, 0x4a72783a, 0x07232427, 0x0f232c2f, 0xc1f1f031, 0x42727032,
|
||||
0x42424002, 0xc4d0d414, 0x41414001, 0xc0c0c000, 0x43737033, 0x47636427, 0x8ca0ac2c, 0x8b83880b,
|
||||
0xc7f3f437, 0x8da1ac2d, 0x80808000, 0x0f131c1f, 0xcac2c80a, 0x0c202c2c, 0x8aa2a82a, 0x04303434,
|
||||
0xc2d2d012, 0x0b03080b, 0xcee2ec2e, 0xc9e1e829, 0x4d515c1d, 0x84909414, 0x08101818, 0xc8f0f838,
|
||||
0x47535417, 0x8ea2ac2e, 0x08000808, 0xc5c1c405, 0x03131013, 0xcdc1cc0d, 0x86828406, 0x89b1b839,
|
||||
0xcff3fc3f, 0x4d717c3d, 0xc1c1c001, 0x01313031, 0xc5f1f435, 0x8a82880a, 0x4a62682a, 0x81b1b031,
|
||||
0xc1d1d011, 0x00202020, 0xc7d3d417, 0x02020002, 0x02222022, 0x04000404, 0x48606828, 0x41717031,
|
||||
0x07030407, 0xcbd3d81b, 0x8d919c1d, 0x89919819, 0x41616021, 0x8eb2bc3e, 0xc6e2e426, 0x49515819,
|
||||
0xcdd1dc1d, 0x41515011, 0x80909010, 0xccd0dc1c, 0x8a92981a, 0x83a3a023, 0x8ba3a82b, 0xc0d0d010,
|
||||
0x81818001, 0x0f030c0f, 0x47434407, 0x0a12181a, 0xc3e3e023, 0xcce0ec2c, 0x8d818c0d, 0x8fb3bc3f,
|
||||
0x86929416, 0x4b73783b, 0x4c505c1c, 0x82a2a022, 0x81a1a021, 0x43636023, 0x03232023, 0x4d414c0d,
|
||||
0xc8c0c808, 0x8e929c1e, 0x8c909c1c, 0x0a32383a, 0x0c000c0c, 0x0e222c2e, 0x8ab2b83a, 0x4e626c2e,
|
||||
0x8f939c1f, 0x4a52581a, 0xc2f2f032, 0x82929012, 0xc3f3f033, 0x49414809, 0x48707838, 0xccc0cc0c,
|
||||
0x05111415, 0xcbf3f83b, 0x40707030, 0x45717435, 0x4f737c3f, 0x05313435, 0x00101010, 0x03030003,
|
||||
0x44606424, 0x4d616c2d, 0xc6c2c406, 0x44707434, 0xc5d1d415, 0x84b0b434, 0xcae2e82a, 0x09010809,
|
||||
0x46727436, 0x09111819, 0xcef2fc3e, 0x40404000, 0x02121012, 0xc0e0e020, 0x8db1bc3d, 0x05010405,
|
||||
0xcaf2f83a, 0x01010001, 0xc0f0f030, 0x0a22282a, 0x4e525c1e, 0x89a1a829, 0x46525416, 0x43434003,
|
||||
0x85818405, 0x04101414, 0x89818809, 0x8b93981b, 0x80b0b030, 0xc5e1e425, 0x48404808, 0x49717839,
|
||||
0x87939417, 0xccf0fc3c, 0x0e121c1e, 0x82828002, 0x01212021, 0x8c808c0c, 0x0b13181b, 0x4f535c1f,
|
||||
0x47737437, 0x44505414, 0x82b2b032, 0x0d111c1d, 0x05212425, 0x4f434c0f, 0x00000000, 0x46424406,
|
||||
0xcde1ec2d, 0x48505818, 0x42525012, 0xcbe3e82b, 0x4e727c3e, 0xcad2d81a, 0xc9c1c809, 0xcdf1fc3d,
|
||||
0x00303030, 0x85919415, 0x45616425, 0x0c303c3c, 0x86b2b436, 0xc4e0e424, 0x8bb3b83b, 0x4c707c3c,
|
||||
0x0e020c0e, 0x40505010, 0x09313839, 0x06222426, 0x02323032, 0x84808404, 0x49616829, 0x83939013,
|
||||
0x07333437, 0xc7e3e427, 0x04202424, 0x84a0a424, 0xcbc3c80b, 0x43535013, 0x0a02080a, 0x87838407,
|
||||
0xc9d1d819, 0x4c404c0c, 0x83838003, 0x8f838c0f, 0xcec2cc0e, 0x0b33383b, 0x4a42480a, 0x87b3b437
|
||||
);
|
||||
|
||||
|
||||
/************************** Constants for Key schedule ************************/
|
||||
|
||||
// KC0 = golden ratio; KCi = ROTL(KCi-1, 1)
|
||||
private $KC = array(
|
||||
0x9e3779b9, 0x3c6ef373, 0x78dde6e6, 0xf1bbcdcc, 0xe3779b99, 0xc6ef3733, 0x8dde6e67, 0x1bbcdccf,
|
||||
0x3779b99e, 0x6ef3733c, 0xdde6e678, 0xbbcdccf1, 0x779b99e3, 0xef3733c6, 0xde6e678d, 0xbcdccf1b
|
||||
);
|
||||
|
||||
|
||||
/**************************** Defining Endianness *****************************/
|
||||
// If endianness is not defined correctly, you must modify here.
|
||||
|
||||
private $LITTLE = false;
|
||||
private $BIG = true;
|
||||
private $ENDIAN = true; // Java virtual machine uses big endian as a default
|
||||
//private Boolean ENDIAN = LITTLE;
|
||||
|
||||
|
||||
/**************************** Constant Definitions ****************************/
|
||||
|
||||
private $NoRounds = 16; // the number of rounds
|
||||
private $NoRoundKeys = 32; // the number of round-keys
|
||||
private $SeedBlockSize = 16; // block length in bytes
|
||||
private $SeedBlockLen = 128; // block length in bits
|
||||
|
||||
//생성자
|
||||
public function __construct() {
|
||||
}
|
||||
//소멸자
|
||||
public function __destruct() {
|
||||
}
|
||||
/****************************** Common functions ******************************/
|
||||
|
||||
//해당 자리수의 바이트를 얻는다
|
||||
private function GetB0($A){
|
||||
return 0x000000ff & $A;
|
||||
}
|
||||
private function GetB1($A){
|
||||
return 0x000000ff & ( $A >> 8 );
|
||||
}
|
||||
private function GetB2($A){
|
||||
return 0x000000ff & ( $A >> 16 );
|
||||
}
|
||||
private function GetB3($A){
|
||||
return 0x000000ff & ( $A >> 24 );
|
||||
}
|
||||
//----------------------------------------
|
||||
|
||||
//엔디안 변환 (데이터의 순서 변환)
|
||||
private function EndianChange( $dws ) {
|
||||
return ( $dws >> 24 ) | ( $dws << 24 ) | ( ( $dws << 8 ) & 0x00ff0000 ) | ( ( $dws >> 8 ) & 0x0000ff00 );
|
||||
}
|
||||
|
||||
|
||||
/***************************** SEED round function ****************************/
|
||||
|
||||
public function SeedRound(
|
||||
&$L0, &$L1, // [in, out] left-side variable at each round
|
||||
&$R0, &$R1, // [in] right-side variable at each round
|
||||
$K = array() // [in] round keys at each round
|
||||
)
|
||||
{
|
||||
$T0 = $R0 ^ $K[0];
|
||||
$T1 = $R1 ^ $K[1];
|
||||
$T1 ^= $T0;
|
||||
|
||||
$T1 = $this->SS0[$this->GetB0($T1)] ^ $this->SS1[$this->GetB1($T1)] ^ $this->SS2[$this->GetB2($T1)] ^ $this->SS3[$this->GetB3($T1)];
|
||||
$T0 += $T1;
|
||||
$T0 = $this->ConvertInt($T0);
|
||||
|
||||
$T0 = $this->SS0[$this->GetB0($T0)] ^ $this->SS1[$this->GetB1($T0)] ^ $this->SS2[$this->GetB2($T0)] ^ $this->SS3[$this->GetB3($T0)];
|
||||
$T1 += $T0;
|
||||
$T1 = $this->ConvertInt($T1);
|
||||
|
||||
$T1 = $this->SS0[$this->GetB0($T1)] ^ $this->SS1[$this->GetB1($T1)] ^ $this->SS2[$this->GetB2($T1)] ^ $this->SS3[$this->GetB3($T1)];
|
||||
$T0 += $T1;
|
||||
$T0 = $this->ConvertInt($T0);
|
||||
|
||||
$L0 ^= $T0;
|
||||
$L1 ^= $T1;
|
||||
}
|
||||
|
||||
//추가된 함수 by mibany (2011-01-21)
|
||||
//PHP 에서는 float를 int로 강제로 형변환이 되지 않는다.
|
||||
//이사실을 전혀 몰랐던 나는 C++ 의 달인 Keige 님의 도움으로
|
||||
//PHP 로 구현하기 위한 핵심부분인 이 함수를 만들게 되었다.
|
||||
//문제는 고수가 아닌 나에게 이함수 구현이란 내게 어려운 일이었다.
|
||||
//혹시라도 이함수의 오류로 인해 Seed 암호화가 문제가 있을수도 있으니
|
||||
//고수분들의 도움이 절실하다.
|
||||
private function ConvertInt($float) {
|
||||
$IntMax = PHP_INT_MAX;
|
||||
$IntMin = ( PHP_INT_MAX * -1 ) -1;
|
||||
if(is_float($float) && $float < $IntMin ) {
|
||||
$division = floor($float / $IntMin );
|
||||
$n = ($division % 2 == 0)?0:$IntMin;
|
||||
if( $float < $IntMin ) $c = $float - ( $IntMin * $division ) - $n;
|
||||
}
|
||||
elseif(is_float($float) && $float > $IntMax) {
|
||||
$division = floor($float / $IntMax );
|
||||
$n = ($division % 2 == 0)?0:$IntMax;
|
||||
if( $float > $IntMax) $c = $float - ( $IntMax * $division ) - $n - 2;
|
||||
}
|
||||
else $c = $float;
|
||||
return $c;
|
||||
}
|
||||
|
||||
/************************** SEED encrtyption function *************************/
|
||||
|
||||
public function SeedEncrypt(
|
||||
$pbData = array(), // [in] data to be encrypted
|
||||
$pdwRoundKey = array(), // [in] round keys for encryption
|
||||
&$outData = array() // [out] encrypted data
|
||||
)
|
||||
{
|
||||
$L0 = 0x0;
|
||||
$L1 = 0x0;
|
||||
$R0 = 0x0;
|
||||
$R1 = 0x0;
|
||||
$K = array();
|
||||
$nCount = 0;
|
||||
|
||||
// Set up input values for encryption
|
||||
$L0 = ( $pbData[0] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[1] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[2] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[3] & 0x000000ff );
|
||||
|
||||
$L1 = ( $pbData[4] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^ ( $pbData[5] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^ ( $pbData[6] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^ ( $pbData[7] & 0x000000ff );
|
||||
|
||||
$R0 = ( $pbData[8] & 0x000000ff );
|
||||
$R0 = ( $R0 <<8 ) ^ ( $pbData[9] & 0x000000ff );
|
||||
$R0 = ( $R0 <<8 ) ^ ( $pbData[10] & 0x000000ff );
|
||||
$R0 = ( $R0 <<8 ) ^ ( $pbData[11] & 0x000000ff );
|
||||
|
||||
$R1 = ( $pbData[12] & 0x000000ff );
|
||||
$R1 = ( $R1 <<8 ) ^ ( $pbData[13] & 0x000000ff );
|
||||
$R1 = ( $R1 <<8 ) ^ ( $pbData[14] & 0x000000ff );
|
||||
$R1 = ( $R1 <<8 ) ^ ( $pbData[15] & 0x000000ff );
|
||||
|
||||
// Reorder for little endian
|
||||
// Because java virtual machine use big endian order in default
|
||||
if (!$this->ENDIAN) {
|
||||
$this->EndianChange($L0);
|
||||
$this->EndianChange($L1);
|
||||
$this->EndianChange($R0);
|
||||
$this->EndianChange($R1);
|
||||
}
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 1 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 2 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 3 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 4 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 5 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 6 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 7 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 8 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 9 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 10 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 11 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 12 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 13 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 14 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 15 */
|
||||
|
||||
$K[0] = $pdwRoundKey[$nCount++];
|
||||
$K[1] = $pdwRoundKey[$nCount++];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 16 */
|
||||
|
||||
if (!$this->ENDIAN) {
|
||||
$this->EndianChange($L0);
|
||||
$this->EndianChange($L1);
|
||||
$this->EndianChange($R0);
|
||||
$this->EndianChange($R1);
|
||||
}
|
||||
|
||||
// Copying output values from last round to outData
|
||||
for ($i=0; $i<16; $i++) $outData[$i] = null;
|
||||
for ($i=0; $i<4; $i++)
|
||||
{
|
||||
$outData[$i] = ( ( ( $R0 ) >>( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[4+$i] = ( ( ( $R1 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[8+$i] = ( ( ( $L0 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[12+$i] = ( ( ( $L1 ) >> ( 8 * ( 3 - $i ) ) ) &0xff );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/************************** SEED decrtyption function *************************/
|
||||
|
||||
// Same as encrypt, except that round keys are applied in reverse order
|
||||
public function SeedDecrypt(
|
||||
$pbData = array(), // [in] encrypted data
|
||||
$pdwRoundKey = array(), // [in] round keys for decryption
|
||||
&$outData = array() // [out] data to be encrypted
|
||||
)
|
||||
{
|
||||
$L0 = 0x0;
|
||||
$L1 = 0x0;
|
||||
$R0 = 0x0;
|
||||
$R1 = 0x0;
|
||||
$K = array();
|
||||
$nCount = 31;
|
||||
|
||||
// Set up input values for decryption
|
||||
$L0 = ( $pbData[0] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[1] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[2] & 0x000000ff );
|
||||
$L0 = ( $L0 << 8 ) ^ ( $pbData[3] & 0x000000ff );
|
||||
|
||||
$L1 = ( $pbData[4] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^( $pbData[5] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^ ( $pbData[6] & 0x000000ff );
|
||||
$L1 = ( $L1 << 8 ) ^ ( $pbData[7] & 0x000000ff );
|
||||
|
||||
$R0 = ( $pbData[8] & 0x000000ff );
|
||||
$R0 = ( $R0 << 8 ) ^ ( $pbData[9] & 0x000000ff );
|
||||
$R0 = ( $R0 << 8 ) ^ ( $pbData[10] & 0x000000ff );
|
||||
$R0 = ( $R0 << 8 ) ^ ( $pbData[11] & 0x000000ff );
|
||||
|
||||
$R1 = ( $pbData[12] & 0x000000ff );
|
||||
$R1 = ( $R1 << 8 ) ^ ( $pbData[13] & 0x000000ff );
|
||||
$R1 = ( $R1 << 8 ) ^ ( $pbData[14] & 0x000000ff );
|
||||
$R1 = ( $R1 << 8 ) ^ ( $pbData[15] & 0x000000ff );
|
||||
|
||||
// Reorder for little endian
|
||||
if (!$this->ENDIAN) {
|
||||
$this->EndianChange($L0);
|
||||
$this->EndianChange($L1);
|
||||
$this->EndianChange($R0);
|
||||
$this->EndianChange($R1);
|
||||
}
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 1 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 2 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 3 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 4 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 5 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 6 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 7 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 8 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 9 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 10 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 11 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 12 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 13 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 14 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount--];
|
||||
$this->SeedRound($L0, $L1, $R0, $R1, $K); /* 15 */
|
||||
|
||||
$K[1] = $pdwRoundKey[$nCount--];
|
||||
$K[0] = $pdwRoundKey[$nCount];
|
||||
$this->SeedRound($R0, $R1, $L0, $L1, $K); /* 16 */
|
||||
|
||||
if (!$this->ENDIAN) {
|
||||
$this->EndianChange($L0);
|
||||
$this->EndianChange($L1);
|
||||
$this->EndianChange($R0);
|
||||
$this->EndianChange($R1);
|
||||
}
|
||||
|
||||
// Copy output values from last round to outData
|
||||
for ($i=0; $i<16; $i++) $outData[$i] = null;
|
||||
for ($i=0; $i < 4; $i++)
|
||||
{
|
||||
$outData[$i] = ( ( ( $R0 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[4+$i] = ( ( ( $R1 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[8+$i] = ( ( ( $L0 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
$outData[12+$i] = ( ( ( $L1 ) >> ( 8 * ( 3 - $i ) ) ) & 0xff );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/************************ Functions for Key schedule **************************/
|
||||
|
||||
private function EncRoundKeyUpdate0(&$K = array(), &$A, &$B, &$C, &$D, $Z)
|
||||
{
|
||||
$T0 = $A;
|
||||
|
||||
$A = ( $A >> 8 & 0x00ffffff ) ^ ( $B << 24 );
|
||||
$B = ( $B >> 8 & 0x00ffffff ) ^ ( $T0 << 24 );
|
||||
|
||||
$T00 = (int) $A + (int) $C - (int) $this->KC[$Z];
|
||||
$T00 = $this->ConvertInt($T00);
|
||||
|
||||
$T11 = (int) $B + (int) $this->KC[$Z] - (int) $D;
|
||||
$T11 = $this->ConvertInt($T11);
|
||||
|
||||
$K[0] = $this->SS0[$this->GetB0($T00)] ^ $this->SS1[$this->GetB1($T00)] ^ $this->SS2[$this->GetB2($T00)] ^ $this->SS3[$this->GetB3($T00)];
|
||||
$K[1] = $this->SS0[$this->GetB0($T11)] ^ $this->SS1[$this->GetB1($T11)] ^ $this->SS2[$this->GetB2($T11)] ^ $this->SS3[$this->GetB3($T11)];
|
||||
}
|
||||
|
||||
private function EncRoundKeyUpdate1(&$K = array(), &$A, &$B, &$C, &$D, $Z)
|
||||
{
|
||||
$T0 = $C;
|
||||
$C = ( $C << 8 ) ^ ( $D >> 24 & 0x000000ff );
|
||||
$D = ( $D << 8 ) ^ ( $T0 >> 24 & 0x000000ff );
|
||||
|
||||
$T00 = (int) $A + (int) $C - (int) $this->KC[$Z];
|
||||
$T00 = $this->ConvertInt($T00);
|
||||
|
||||
$T11 = (int) $B + (int) $this->KC[$Z] - (int) $D;
|
||||
$T11 = $this->ConvertInt($T11);
|
||||
|
||||
$K[0] = $this->SS0[$this->GetB0($T00)] ^ $this->SS1[$this->GetB1($T00)] ^ $this->SS2[$this->GetB2($T00)] ^ $this->SS3[$this->GetB3($T00)];
|
||||
$K[1] = $this->SS0[$this->GetB0($T11)] ^ $this->SS1[$this->GetB1($T11)] ^ $this->SS2[$this->GetB2($T11)] ^ $this->SS3[$this->GetB3($T11)];
|
||||
}
|
||||
|
||||
/******************************** Key Schedule ********************************/
|
||||
public function SeedRoundKey(
|
||||
&$pdwRoundKey = array(), // [out] round keys for encryption or decryption
|
||||
$pbUserKey = array() // [in] secret user key
|
||||
)
|
||||
{
|
||||
$K = array();
|
||||
$nCount = 2;
|
||||
|
||||
// Set up input values for Key Schedule
|
||||
$A = @( $pbUserKey[0] & 0x000000ff );
|
||||
$A = ( $A << 8 ) ^ @( $pbUserKey[1] & 0x000000ff );
|
||||
$A = ( $A << 8 ) ^ @( $pbUserKey[2] & 0x000000ff );
|
||||
$A = ( $A << 8 ) ^ @( $pbUserKey[3] & 0x000000ff );
|
||||
|
||||
$B = @( $pbUserKey[4] & 0x000000ff );
|
||||
$B = ( $B<<8 ) ^ @( $pbUserKey[5] & 0x000000ff );
|
||||
$B = ( $B<<8 ) ^ @( $pbUserKey[6] & 0x000000ff );
|
||||
$B = ( $B<<8 ) ^ @( $pbUserKey[7] & 0x000000ff );
|
||||
|
||||
$C = @( $pbUserKey[8] & 0x000000ff );
|
||||
$C = ( $C << 8 ) ^ @( $pbUserKey[9] & 0x000000ff );
|
||||
$C = ( $C << 8 ) ^ @( $pbUserKey[10] & 0x000000ff );
|
||||
$C = ( $C << 8 ) ^ @( $pbUserKey[11] & 0x000000ff );
|
||||
|
||||
$D = @( $pbUserKey[12] & 0x000000ff );
|
||||
$D = ( $D << 8 ) ^ @($pbUserKey[13] & 0x000000ff );
|
||||
$D = ( $D << 8 ) ^ @( $pbUserKey[14] & 0x000000ff );
|
||||
$D = ( $D << 8 ) ^ @( $pbUserKey[15] & 0x000000ff );
|
||||
|
||||
// reorder for little endian
|
||||
if (!$this->ENDIAN) {
|
||||
$A = $this->EndianChange($A);
|
||||
$B = $this->EndianChange($B);
|
||||
$C = $this->EndianChange($C);
|
||||
$D = $this->EndianChange($D);
|
||||
}
|
||||
|
||||
$T0 = (int) $A + (int) $C - (int) $this->KC[0];
|
||||
$T0 = $this->ConvertInt($T0);
|
||||
|
||||
$T1 = (int) $B - (int) $D + (int) $this->KC[0];
|
||||
$T1 = $this->ConvertInt($T1);
|
||||
|
||||
$pdwRoundKey[0] = $this->SS0[$this->GetB0($T0)] ^ $this->SS1[$this->GetB1($T0)] ^ $this->SS2[$this->GetB2($T0)] ^ $this->SS3[$this->GetB3($T0)];
|
||||
$pdwRoundKey[1] = $this->SS0[$this->GetB0($T1)] ^ $this->SS1[$this->GetB1($T1)] ^ $this->SS2[$this->GetB2($T1)] ^ $this->SS3[$this->GetB3($T1)];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 1 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 2 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 3 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 4 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 5 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 6 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 7 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 8 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 9 );
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 10);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 11);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 12);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 13);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate1($K, $A, $B, $C, $D, 14);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
|
||||
$this->EncRoundKeyUpdate0($K, $A, $B, $C, $D, 15);
|
||||
$pdwRoundKey[$nCount++] = $K[0];
|
||||
$pdwRoundKey[$nCount++] = $K[1];
|
||||
}
|
||||
public function SeedRoundKeyText(&$pdwRoundKey, $pbUserKey)
|
||||
{
|
||||
$Data = [];
|
||||
$len = strlen($pbUserKey);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$Data[$i] = ord($pbUserKey[$i]); // ✅ {} -> []
|
||||
}
|
||||
$this->SeedRoundKey($pdwRoundKey, $Data);
|
||||
}
|
||||
|
||||
public function SeedEncryptText($pbData, $pdwRoundKey, &$outData)
|
||||
{
|
||||
$Data = [];
|
||||
$len = strlen($pbData);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$Data[$i] = ord($pbData[$i]); // ✅ {} -> []
|
||||
}
|
||||
$this->SeedEncrypt($Data, $pdwRoundKey, $outData);
|
||||
}
|
||||
|
||||
public function SeedDecryptText($pbData, $pdwRoundKey, &$outData)
|
||||
{
|
||||
$Data = [];
|
||||
$len = strlen($pbData);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$Data[$i] = ord($pbData[$i]); // ✅ {} -> []
|
||||
}
|
||||
$this->SeedDecrypt($Data, $pdwRoundKey, $outData);
|
||||
}
|
||||
}
|
||||
132
config/cs_faq.php
Normal file
132
config/cs_faq.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FAQ Categories
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'categories' => [
|
||||
['key' => 'signup', 'label' => '회원가입 문의'],
|
||||
['key' => 'login', 'label' => '로그인 문의'],
|
||||
['key' => 'pay', 'label' => '결제 문의'],
|
||||
['key' => 'code', 'label' => '상품권 코드 문의'],
|
||||
['key' => 'event', 'label' => '이벤트 문의'],
|
||||
['key' => 'etc', 'label' => '기타문의'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FAQ Items
|
||||
|--------------------------------------------------------------------------
|
||||
| - 'category' must match categories.key
|
||||
| - 'q' question
|
||||
| - 'a' answer (string, allow \n)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'items' => [
|
||||
|
||||
// 회원가입 문의
|
||||
[
|
||||
'category' => 'signup',
|
||||
'q' => '회원가입에 나이 제한이 있나요?',
|
||||
'a' => "만 14세 미만은 회원가입 및 서비스 이용이 불가능합니다.\n만 14세 이상인 경우에만 회원가입이 가능합니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'signup',
|
||||
'q' => '법인회원으로 가입이 가능한가요?',
|
||||
'a' => "법인회원으로는 가입이 불가능하며, 개인회원으로만 가입이 가능합니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'signup',
|
||||
'q' => '회원가입은 어떻게 하나요?',
|
||||
'a' => "1) 사이트 상단의 ‘회원가입’을 클릭합니다.\n2) 이용약관/개인정보처리방침/수신동의 내용을 확인 후 동의합니다.\n3) 휴대폰 본인 인증을 진행합니다.\n4) 인증 완료 후 가입정보 입력 페이지로 이동합니다.\n5) 아이디(이메일), 비밀번호 등 필수 정보를 입력 후 ‘회원가입’을 클릭합니다.\n6) 가입이 완료됩니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'signup',
|
||||
'q' => '아이디를 여러 개 사용할 수 있나요?',
|
||||
'a' => "본인 인증을 필수로 진행하기 때문에, 본인 명의 기준 1개의 계정만 가입 및 사용이 가능합니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'signup',
|
||||
'q' => '본인명의 휴대폰 인증이 안돼요.',
|
||||
'a' => "아래 경우에는 인증이 제한될 수 있습니다.\n- 법인/타인 명의 휴대폰\n- 선불폰/알뜰폰\n- 분실신고/일시정지/해지/미개통 상태",
|
||||
],
|
||||
|
||||
// 로그인 문의
|
||||
[
|
||||
'category' => 'login',
|
||||
'q' => '아이디가 기억나지 않습니다.',
|
||||
'a' => "휴대폰 본인인증을 통해 아이디 정보를 확인할 수 있습니다.\n개인정보 보호를 위해 일부 정보만 표시될 수 있습니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'login',
|
||||
'q' => '비밀번호가 기억나지 않습니다.',
|
||||
'a' => "비밀번호는 암호화되어 있어 직접 확인은 불가능합니다.\n본인 명의 휴대폰 인증 후 비밀번호를 재설정해 주세요.",
|
||||
],
|
||||
|
||||
// 결제 문의
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '무통장 입금하면 바로 구매되나요?',
|
||||
'a' => "무통장 입금은 결제 완료까지 약 5~10분 정도 소요될 수 있습니다.\n입금 완료 후 안내 메시지가 발송됩니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '신용카드 결제 한도는 어떻게 되나요?',
|
||||
'a' => "개인 신용카드는 카드사별 정책에 따라 월 100만원 한도 내에서 결제가 가능합니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '신용카드 결제 수수료가 있나요?',
|
||||
'a' => "신용카드 결제 수수료는 별도로 부과되지 않습니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '신용카드 결제 할부가 가능한가요?',
|
||||
'a' => "카드사 정책에 따라 신용카드 할부 결제는 불가합니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '휴대폰 결제 한도는 어떻게 되나요?',
|
||||
'a' => "휴대폰 결제 통합 한도는 ID당 100만원(결제 수수료 포함)입니다.\n통신사/결제대행업체 정책에 따라 달라질 수 있습니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '본인 휴대폰이 아닌데 휴대폰 결제가 가능한가요?',
|
||||
'a' => "휴대폰 결제는 가입자와 휴대폰 명의자가 일치해야 합니다.\n도용 결제 예방을 위한 정책입니다.",
|
||||
],
|
||||
[
|
||||
'category' => 'pay',
|
||||
'q' => '휴대폰 결제 수수료가 있나요?',
|
||||
'a' => "휴대폰 결제는 결제대행업체 정책에 따라 결제 수수료(예: 10%)가 적용될 수 있습니다.",
|
||||
],
|
||||
|
||||
// 상품권 코드 문의
|
||||
[
|
||||
'category' => 'code',
|
||||
'q' => '상품 코드는 어디에서 확인하나요?',
|
||||
'a' => "마이페이지 > 이용내역에서 확인 가능합니다.\n단, 일부 상품은 휴대폰으로 즉시 전송되어 이용내역에서 확인이 어려울 수 있습니다.\n미수신/확인 불가 시 1:1 문의로 접수해 주세요.",
|
||||
],
|
||||
[
|
||||
'category' => 'code',
|
||||
'q' => '구입한 상품을 취소하고 싶습니다.',
|
||||
'a' => "상품 특성상 발송이 완료된 경우 취소가 불가능합니다.",
|
||||
],
|
||||
|
||||
// 이벤트 문의 (현재 원문 페이지가 비어있는 형태여서, UI상 빈 상태 안내)
|
||||
[
|
||||
'category' => 'event',
|
||||
'q' => '이벤트 관련 문의는 어디로 하면 되나요?',
|
||||
'a' => "이벤트 진행 시 FAQ에 별도 안내가 추가됩니다.\n현재 진행 중인 이벤트가 없다면 1:1 문의로 접수해 주세요.",
|
||||
],
|
||||
|
||||
// 기타문의
|
||||
[
|
||||
'category' => 'etc',
|
||||
'q' => '카카오톡 채팅 상담이 가능한가요?',
|
||||
'a' => "카카오톡 채널을 추가한 뒤 채팅 상담이 가능합니다.\n‘카카오톡 상담’ 메뉴에서도 동일하게 안내해 드립니다.",
|
||||
],
|
||||
],
|
||||
];
|
||||
@ -31,38 +31,6 @@ return [
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
@ -83,35 +51,26 @@ return [
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'sms_server' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('SMS_DB_HOST', '127.0.0.1'),
|
||||
'port' => env('SMS_DB_PORT', '3306'),
|
||||
'database' => env('SMS_DB_DATABASE', 'lguplus'),
|
||||
'username' => env('SMS_DB_USERNAME', 'lguplus'),
|
||||
'password' => env('SMS_DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
|
||||
|
||||
],
|
||||
|
||||
|
||||
10
config/legacy.php
Normal file
10
config/legacy.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'seed_user_key_default' => env('LEGACY_ENCRYPTION_KEY_WEB', ''),
|
||||
'server_encoding' => 'UTF-8',
|
||||
'inner_encoding' => 'UTF-8',
|
||||
'block' => 16,
|
||||
'iv' => [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
390
resources/views/web/auth/find_id.blade.php
Normal file
390
resources/views/web/auth/find_id.blade.php
Normal file
@ -0,0 +1,390 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '아이디 찾기 | PIN FOR YOU')
|
||||
@section('meta_description', 'PIN FOR YOU 아이디(이메일) 찾기 페이지입니다.')
|
||||
@section('canonical', url('/auth/find-id'))
|
||||
|
||||
@section('h1', '아이디 찾기')
|
||||
@section('desc', '가입 시 등록한 휴대폰 번호로 아이디를 확인할 수 있어요.')
|
||||
@section('card_aria', '아이디 찾기 폼')
|
||||
@section('show_cs_links', true)
|
||||
|
||||
@section('auth_content')
|
||||
<form class="auth-form" id="findIdForm" onsubmit="return false;">
|
||||
{{-- STEP 1 --}}
|
||||
<div class="auth-panel is-active" data-step="1">
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="fi_phone">휴대폰 번호</label>
|
||||
<input class="auth-input" id="fi_phone" type="tel" placeholder="010-0000-0000" autocomplete="tel" value="{{ $phone ? preg_replace('/(\d{3})(\d{4})(\d{4})/', '$1-$2-$3', $phone) : '' }}">
|
||||
<div class="auth-help">본인 인증 후, 가입된 아이디(이메일)를 안내합니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" type="button" data-next>인증번호 받기</button>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.login') }}">로그인으로 돌아가기</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- STEP 2 --}}
|
||||
<div class="auth-panel" data-step="2">
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="fi_code">인증번호</label>
|
||||
<input class="auth-input" id="fi_code" type="text" placeholder="6자리 인증번호" inputmode="numeric">
|
||||
<div class="auth-help">※ 현재는 UI만 구성되어 있어 실제 발송/검증은 동작하지 않습니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" type="button" data-next>확인</button>
|
||||
<button class="auth-btn auth-btn--ghost" type="button" data-prev>이전</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- STEP 3 --}}
|
||||
<div class="auth-panel" data-step="3">
|
||||
<div class="auth-field">
|
||||
<label class="auth-label">확인 결과</label>
|
||||
<div class="auth-help" id="findIdResult" style="font-weight:850;">
|
||||
가입된 아이디는 <b>example@domain.com</b> 형태로 안내됩니다. (샘플)
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<a class="auth-btn auth-btn--primary" href="{{ route('web.auth.login') }}">로그인 하기</a>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.find_password') }}">비밀번호 찾기</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('auth_bottom')
|
||||
{{-- 필요 시 하단에 추가 문구/링크를 넣고 싶으면 여기 --}}
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(function(){
|
||||
const root = document.getElementById('findIdForm');
|
||||
if(!root) return;
|
||||
|
||||
const panels = root.querySelectorAll('.auth-panel');
|
||||
const stepInd = document.querySelectorAll('.auth-step');
|
||||
|
||||
let step = Number(@json($initialStep ?? 1));
|
||||
|
||||
const $phone = document.getElementById('fi_phone');
|
||||
const $code = document.getElementById('fi_code');
|
||||
|
||||
// ✅ 결과 박스는 id로 고정 (절대 흔들리지 않음)
|
||||
const resultBox = document.getElementById('findIdResult');
|
||||
|
||||
// 메시지 영역: 항상 "현재 활성 패널"의 actions 위로 이동/생성
|
||||
const mkMsg = () => {
|
||||
let el = root.querySelector('.auth-msg');
|
||||
if(!el){
|
||||
el = document.createElement('div');
|
||||
el.className = 'auth-msg';
|
||||
el.style.marginTop = '10px';
|
||||
el.style.fontSize = '13px';
|
||||
el.style.lineHeight = '1.4';
|
||||
}
|
||||
|
||||
const activeActions = root.querySelector('.auth-panel.is-active .auth-actions');
|
||||
if(activeActions && el.parentNode !== activeActions){
|
||||
activeActions.prepend(el);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
const setMsg = (text, type='info') => {
|
||||
const el = mkMsg();
|
||||
el.textContent = text || '';
|
||||
el.style.color =
|
||||
(type === 'error') ? '#ff6b6b' :
|
||||
(type === 'success') ? '#2ecc71' :
|
||||
'#c7c7c7';
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
// ✅ 1) 전환 전에 현재 포커스 제거 (경고 원인 제거)
|
||||
const activeEl = document.activeElement;
|
||||
if (activeEl && root.contains(activeEl)) {
|
||||
activeEl.blur();
|
||||
}
|
||||
|
||||
panels.forEach(p => {
|
||||
const on = Number(p.dataset.step) === step;
|
||||
|
||||
p.classList.toggle('is-active', on);
|
||||
p.style.display = on ? 'block' : 'none';
|
||||
|
||||
// ✅ 2) aria-hidden은 유지하되, 포커스/클릭 차단은 inert로 처리
|
||||
// on=false인 패널은 inert 적용(포커스 못 감)
|
||||
if (!on) {
|
||||
p.setAttribute('aria-hidden', 'true');
|
||||
p.setAttribute('inert', '');
|
||||
} else {
|
||||
p.setAttribute('aria-hidden', 'false');
|
||||
p.removeAttribute('inert');
|
||||
}
|
||||
});
|
||||
|
||||
stepInd.forEach(s => {
|
||||
const on = Number(s.dataset.stepInd) === step;
|
||||
s.classList.toggle('is-active', on);
|
||||
});
|
||||
|
||||
mkMsg();
|
||||
|
||||
// ✅ 3) 전환 후 포커스 이동(접근성/UX)
|
||||
// 현재 step 패널의 첫 input 또는 버튼으로 포커스
|
||||
const target = root.querySelector(`.auth-panel[data-step="${step}"] input, .auth-panel[data-step="${step}"] button, .auth-panel[data-step="${step}"] a`);
|
||||
target?.focus?.();
|
||||
};
|
||||
|
||||
|
||||
// -------- helpers ----------
|
||||
const csrf = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
const postJson = async (url, data) => {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin', // ✅ include 대신 same-origin 권장(같은 도메인일 때)
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrf(),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data || {})
|
||||
});
|
||||
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
const raw = await res.text(); // ✅ 먼저 text로 받는다
|
||||
|
||||
let json = null;
|
||||
if (ct.includes('application/json')) {
|
||||
try { json = JSON.parse(raw); } catch (e) {}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = json?.message || `요청 실패 (${res.status})`;
|
||||
const err = new Error(msg);
|
||||
err.status = res.status;
|
||||
err.payload = json;
|
||||
err.raw = raw;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return json ?? { ok: true };
|
||||
};
|
||||
|
||||
|
||||
const normalizePhone = (v) => (v || '').replace(/[^\d]/g,'');
|
||||
const formatPhone = (digits) => {
|
||||
if(!digits) return '';
|
||||
if(digits.length === 11) return digits.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
|
||||
if(digits.length === 10) return digits.replace(/(\d{3})(\d{3,4})(\d{4})/, '$1-$2-$3');
|
||||
return digits;
|
||||
};
|
||||
|
||||
// -------- timer ----------
|
||||
let timerId = null;
|
||||
let remain = 0;
|
||||
|
||||
const ensureTimerUI = () => {
|
||||
let wrap = root.querySelector('.fi-timer');
|
||||
if(!wrap){
|
||||
wrap = document.createElement('div');
|
||||
wrap.className = 'fi-timer';
|
||||
wrap.style.marginTop = '8px';
|
||||
wrap.style.display = 'flex';
|
||||
wrap.style.gap = '10px';
|
||||
wrap.style.alignItems = 'center';
|
||||
|
||||
const t = document.createElement('span');
|
||||
t.className = 'fi-timer__text';
|
||||
t.style.fontSize = '13px';
|
||||
t.style.color = '#c7c7c7';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'auth-btn auth-btn--ghost fi-resend';
|
||||
btn.textContent = '재전송';
|
||||
btn.style.padding = '10px 12px';
|
||||
|
||||
wrap.appendChild(t);
|
||||
wrap.appendChild(btn);
|
||||
|
||||
const step2Field = root.querySelector('[data-step="2"] .auth-field');
|
||||
step2Field?.appendChild(wrap);
|
||||
}
|
||||
return wrap;
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
const wrap = ensureTimerUI();
|
||||
const t = wrap.querySelector('.fi-timer__text');
|
||||
const btn = wrap.querySelector('.fi-resend');
|
||||
|
||||
const mm = String(Math.floor(remain/60)).padStart(2,'0');
|
||||
const ss = String(remain%60).padStart(2,'0');
|
||||
|
||||
t.textContent = remain > 0 ? `유효시간 ${mm}:${ss}` : '인증번호가 만료되었습니다. 재전송 해주세요.';
|
||||
btn.disabled = remain > 0;
|
||||
|
||||
if(remain <= 0){
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
return;
|
||||
}
|
||||
remain -= 1;
|
||||
};
|
||||
|
||||
const startTimer = (sec) => {
|
||||
remain = Number(sec || 180);
|
||||
if(timerId) clearInterval(timerId);
|
||||
timerId = setInterval(tick, 1000);
|
||||
tick();
|
||||
};
|
||||
|
||||
// -------- actions ----------
|
||||
const sendCode = async () => {
|
||||
const raw = $phone?.value || '';
|
||||
const digits = normalizePhone(raw);
|
||||
|
||||
if(!digits){
|
||||
setMsg('휴대폰 번호를 입력해 주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setMsg('확인 중입니다...', 'info');
|
||||
|
||||
try {
|
||||
const json = await postJson(@json(route('web.auth.find_id.send_code')), { phone: raw });
|
||||
|
||||
// ✅ 성공 (ok true)
|
||||
setMsg(json.message || '인증번호를 발송했습니다.', 'success');
|
||||
|
||||
step = 2;
|
||||
render();
|
||||
|
||||
startTimer(json.expires_in || 180);
|
||||
|
||||
if(json.dev_code){
|
||||
setMsg(`(개발용) 인증번호: ${json.dev_code}`, 'info');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// ✅ 여기서 404(PHONE_NOT_FOUND)도 UX로 처리
|
||||
const p = err.payload || {};
|
||||
|
||||
if (err.status === 404 && p.code === 'PHONE_NOT_FOUND') {
|
||||
// step 1 유지
|
||||
step = 1;
|
||||
render();
|
||||
setMsg(p.message || '해당 번호로 가입된 계정을 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 기타 에러(429/422/500 등)
|
||||
setMsg(err.message || '오류가 발생했습니다.', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const verifyCode = async () => {
|
||||
const code = ($code?.value || '').trim();
|
||||
|
||||
if(!/^\d{6}$/.test(code)){
|
||||
setMsg('인증번호 6자리를 입력해 주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setMsg('인증 확인 중입니다...', 'info');
|
||||
|
||||
const json = await postJson(@json(route('web.auth.find_id.verify')), { code });
|
||||
|
||||
|
||||
// ✅ 먼저 step 이동 + 렌더 (패널 표시 보장)
|
||||
step = 3;
|
||||
render();
|
||||
|
||||
// ✅ 결과 반영은 렌더 후
|
||||
const maskedList = Array.isArray(json.masked_emails) ? json.masked_emails : [];
|
||||
if (resultBox) {
|
||||
if (maskedList.length > 0) {
|
||||
// 여러 개 출력 (줄바꿈)
|
||||
const html = maskedList.map(e => `<div>• <b>${e}</b></div>`).join('');
|
||||
resultBox.innerHTML = `가입된 아이디는 아래와 같습니다.${html}`;
|
||||
} else {
|
||||
resultBox.innerHTML = `해당 번호로 가입된 계정을 찾을 수 없습니다.`;
|
||||
}
|
||||
}
|
||||
|
||||
setMsg(json.message || '인증이 완료되었습니다.', 'success');
|
||||
};
|
||||
|
||||
// -------- events ----------
|
||||
root.addEventListener('click', async (e) => {
|
||||
const resend = e.target.closest('.fi-resend');
|
||||
const next = e.target.closest('[data-next]');
|
||||
const prev = e.target.closest('[data-prev]');
|
||||
|
||||
try{
|
||||
if(resend){
|
||||
await sendCode();
|
||||
return;
|
||||
}
|
||||
|
||||
if(next){
|
||||
if(step === 1) await sendCode();
|
||||
else if(step === 2) await verifyCode();
|
||||
return;
|
||||
}
|
||||
|
||||
if(prev){
|
||||
step = Math.max(1, step - 1);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
}catch(err){
|
||||
const stepFromServer = err?.payload?.step;
|
||||
if(stepFromServer){
|
||||
step = Number(stepFromServer);
|
||||
render();
|
||||
}
|
||||
setMsg(err.message || '오류가 발생했습니다.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// input UX
|
||||
if($phone){
|
||||
$phone.addEventListener('input', () => {
|
||||
const digits = normalizePhone($phone.value);
|
||||
$phone.value = formatPhone(digits);
|
||||
});
|
||||
|
||||
$phone.addEventListener('keydown', (e) => {
|
||||
if(e.key === 'Enter'){
|
||||
e.preventDefault();
|
||||
root.querySelector('[data-step="1"] [data-next]')?.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if($code){
|
||||
$code.addEventListener('keydown', (e) => {
|
||||
if(e.key === 'Enter'){
|
||||
e.preventDefault();
|
||||
root.querySelector('[data-step="2"] [data-next]')?.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
||||
54
resources/views/web/auth/login.blade.php
Normal file
54
resources/views/web/auth/login.blade.php
Normal file
@ -0,0 +1,54 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '로그인 | PIN FOR YOU')
|
||||
@section('meta_description', 'PIN FOR YOU 로그인 페이지입니다.')
|
||||
@section('canonical', url('/auth/login'))
|
||||
|
||||
@section('h1', '로그인')
|
||||
@section('desc', '안전한 거래를 위해 로그인해 주세요.')
|
||||
@section('headline', '안전한 핀 발송/거래')
|
||||
@section('subheadline', '로그인 후 구매/문의 내역을 빠르게 확인할 수 있어요.')
|
||||
@section('card_aria', '로그인 폼')
|
||||
|
||||
@section('auth_content')
|
||||
<form class="auth-form" onsubmit="return false;">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="login_pw">비밀번호</label>
|
||||
<input class="auth-input" id="login_pw" type="password" placeholder="비밀번호" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="auth-row">
|
||||
<label class="auth-check">
|
||||
<input type="checkbox">
|
||||
자동 로그인
|
||||
</label>
|
||||
|
||||
<div class="auth-links-inline">
|
||||
<a class="auth-link" href="{{ route('web.auth.find_id') }}">아이디 찾기</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a class="auth-link" href="{{ route('web.auth.find_password') }}">비밀번호 찾기</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">로그인</button>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.register') }}">회원가입</a>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('auth_bottom')
|
||||
<div class="auth-links">
|
||||
<a class="auth-link" href="{{ route('web.cs.faq.index') }}">FAQ</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a class="auth-link" href="{{ route('web.cs.qna.index') }}">1:1 문의</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a class="auth-link" href="{{ route('web.cs.kakao.index') }}">카카오 상담</a>
|
||||
</div>
|
||||
@endsection
|
||||
74
resources/views/web/auth/register.blade.php
Normal file
74
resources/views/web/auth/register.blade.php
Normal file
@ -0,0 +1,74 @@
|
||||
@extends('web.layouts.auth')
|
||||
|
||||
@section('title', '회원가입 | PIN FOR YOU')
|
||||
@section('meta_description', 'PIN FOR YOU 회원가입 페이지입니다.')
|
||||
@section('canonical', url('/auth/register'))
|
||||
|
||||
@section('h1', '회원가입')
|
||||
@section('desc', '간단한 정보 입력 후 본인인증을 진행합니다.')
|
||||
@section('card_aria', '회원가입 폼')
|
||||
@section('show_cs_links', true)
|
||||
|
||||
@section('auth_content')
|
||||
<form class="auth-form" onsubmit="return false;">
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_email">
|
||||
아이디(이메일) <small>로그인에 사용</small>
|
||||
</label>
|
||||
<input class="auth-input" id="reg_email" type="email"
|
||||
placeholder="example@domain.com" autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_pw">
|
||||
비밀번호 <small>영문/숫자/특수문자 권장</small>
|
||||
</label>
|
||||
<input class="auth-input" id="reg_pw" type="password"
|
||||
placeholder="비밀번호" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_pw2">비밀번호 확인</label>
|
||||
<input class="auth-input" id="reg_pw2" type="password"
|
||||
placeholder="비밀번호 재입력" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="auth-field">
|
||||
<label class="auth-label" for="reg_phone">
|
||||
휴대폰 번호 <small>본인 확인용</small>
|
||||
</label>
|
||||
<input class="auth-input" id="reg_phone" type="tel"
|
||||
placeholder="010-0000-0000" autocomplete="tel">
|
||||
</div>
|
||||
|
||||
<div class="auth-divider">약관 동의</div>
|
||||
|
||||
<label class="auth-check">
|
||||
<input type="checkbox">
|
||||
(필수) 이용약관 동의
|
||||
</label>
|
||||
|
||||
<label class="auth-check">
|
||||
<input type="checkbox">
|
||||
(필수) 개인정보처리방침 동의
|
||||
</label>
|
||||
|
||||
<label class="auth-check">
|
||||
<input type="checkbox">
|
||||
(선택) 마케팅 수신 동의
|
||||
</label>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button class="auth-btn auth-btn--primary" type="submit">가입하기</button>
|
||||
<a class="auth-btn auth-btn--ghost" href="{{ route('web.auth.login') }}">
|
||||
이미 계정이 있어요 (로그인)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('auth_bottom')
|
||||
{{-- 로그인 페이지처럼 하단에 CS 링크 들어가게 하고 싶으면(선택) --}}
|
||||
{{-- auth 레이아웃에서 show_cs_links=true 처리 중이면 이 섹션은 비워도 됩니다. --}}
|
||||
@endsection
|
||||
@ -18,14 +18,103 @@
|
||||
@section('canonical', url('/cs/faq'))
|
||||
|
||||
@section('subcontent')
|
||||
<div class="faq-page">
|
||||
@include('web.partials.content-head', [
|
||||
'title' => 'FAQ',
|
||||
'desc' => '원하시는 항목을 선택해 빠르게 해결해 보세요.'
|
||||
])
|
||||
@include('web.partials.content-head', [
|
||||
'title' => 'FAQ',
|
||||
'desc' => '원하는 키워드로 빠르게 찾고, 카테고리별로 정리해서 확인하세요.'
|
||||
])
|
||||
|
||||
{{-- TODO: FAQ 내용(아코디언/카테고리/검색 등) --}}
|
||||
<section class="faq" aria-label="자주 묻는 질문">
|
||||
{{-- Tools --}}
|
||||
<div class="faq-tools">
|
||||
<div class="faq-search" role="search">
|
||||
<label class="sr-only" for="faqSearch">FAQ 검색</label>
|
||||
<input id="faqSearch" class="faq-search__input" type="search" placeholder="예) 휴대폰 결제, 비밀번호, 코드 확인" autocomplete="off">
|
||||
<span class="faq-search__icon" aria-hidden="true">⌕</span>
|
||||
</div>
|
||||
|
||||
@include('web.partials.cs-quick-actions')
|
||||
</div>
|
||||
<div class="faq-cats" role="tablist" aria-label="FAQ 카테고리">
|
||||
<button type="button" class="faq-cat is-active" data-cat="all" role="tab" aria-selected="true">전체</button>
|
||||
@foreach(config('cs_faq.categories', []) as $c)
|
||||
<button type="button" class="faq-cat" data-cat="{{ $c['key'] }}" role="tab" aria-selected="false">
|
||||
{{ $c['label'] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- List --}}
|
||||
<div class="faq-list" id="faqList">
|
||||
@foreach(config('cs_faq.items', []) as $idx => $it)
|
||||
<details class="faq-item" data-cat="{{ $it['category'] ?? 'etc' }}">
|
||||
<summary class="faq-q">
|
||||
<span class="faq-q__badge">Q</span>
|
||||
<span class="faq-q__text">{{ $it['q'] ?? '' }}</span>
|
||||
<span class="faq-q__chev" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="faq-a">
|
||||
{!! nl2br(e($it['a'] ?? '')) !!}
|
||||
</div>
|
||||
</details>
|
||||
@endforeach
|
||||
|
||||
<div class="faq-empty" hidden>
|
||||
검색 결과가 없어요. 다른 키워드로 다시 시도해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- (선택) 하단 CS Quick 영역 재사용 가능 --}}
|
||||
@include('web.partials.cs-quick-actions')
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const wrap = document.querySelector('.faq');
|
||||
if (!wrap) return;
|
||||
|
||||
const search = wrap.querySelector('#faqSearch');
|
||||
const catBtns = [...wrap.querySelectorAll('.faq-cat')];
|
||||
const items = [...wrap.querySelectorAll('.faq-item')];
|
||||
const empty = wrap.querySelector('.faq-empty');
|
||||
|
||||
let activeCat = 'all';
|
||||
|
||||
const apply = () => {
|
||||
const q = (search?.value || '').trim().toLowerCase();
|
||||
let visible = 0;
|
||||
|
||||
items.forEach(el => {
|
||||
const cat = el.dataset.cat || 'etc';
|
||||
const text = el.innerText.toLowerCase();
|
||||
|
||||
const catOk = (activeCat === 'all') || (cat === activeCat);
|
||||
const qOk = !q || text.includes(q);
|
||||
|
||||
const show = catOk && qOk;
|
||||
el.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
});
|
||||
|
||||
empty.hidden = visible !== 0;
|
||||
};
|
||||
|
||||
catBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
catBtns.forEach(b => {
|
||||
b.classList.remove('is-active');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.add('is-active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
activeCat = btn.dataset.cat || 'all';
|
||||
apply();
|
||||
});
|
||||
});
|
||||
|
||||
search?.addEventListener('input', apply);
|
||||
|
||||
apply();
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@ -18,14 +18,142 @@
|
||||
@section('canonical', url('/cs/guide'))
|
||||
|
||||
@section('subcontent')
|
||||
<div class="guide-page">
|
||||
@include('web.partials.content-head', [
|
||||
'title' => '서비스 이용 안내',
|
||||
'desc' => '주요 흐름(구매 → 결제 → 발송 → 환불)을 기준으로 안내합니다.'
|
||||
])
|
||||
@include('web.partials.content-head', [
|
||||
'title' => '이용안내',
|
||||
'desc' => '필요한 부분만 빠르게 확인할 수 있도록 핵심 흐름만 모았습니다.'
|
||||
])
|
||||
|
||||
{{-- TODO: 이용안내 본문(단계별 가이드/주의사항/정책 링크 등) --}}
|
||||
<section class="guide" aria-label="이용안내 본문">
|
||||
|
||||
{{-- Intro --}}
|
||||
<div class="guide-hero">
|
||||
<div class="guide-hero__badge">Quick Guide</div>
|
||||
<div class="guide-hero__title">처음 방문하셨나요?</div>
|
||||
<p class="guide-hero__desc">
|
||||
회원가입 → 로그인 → 상품 선택/결제 → 코드 확인까지, 기본 흐름은 아주 간단해요.
|
||||
처음 1회는 보안을 위해 본인인증이 진행될 수 있습니다.
|
||||
</p>
|
||||
|
||||
<div class="guide-hero__chips" aria-label="핵심 요약">
|
||||
<span class="guide-chip">회원가입: 본인인증 + 필수 정보 입력</span>
|
||||
<span class="guide-chip">로그인: 이메일(ID) + 비밀번호</span>
|
||||
<span class="guide-chip">구매: 상품 선택 → 결제</span>
|
||||
<span class="guide-chip">문의: 1:1 문의로 접수</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Grid --}}
|
||||
<div class="guide-grid">
|
||||
|
||||
{{-- Signup --}}
|
||||
<article class="guide-card" aria-label="회원가입 안내">
|
||||
<div class="guide-card__top">
|
||||
<div class="guide-card__icon" aria-hidden="true">①</div>
|
||||
<h3 class="guide-card__title">회원가입</h3>
|
||||
</div>
|
||||
|
||||
<p class="guide-card__lead">
|
||||
안정적인 서비스 제공과 정보 보호를 위해 회원가입 시 최초 1회 본인인증이 진행될 수 있어요.
|
||||
</p>
|
||||
|
||||
<ol class="guide-steps">
|
||||
<li><b>본인인증(PASS)</b>을 진행합니다. (PASS로 승인)</li>
|
||||
<li><b>이메일</b>을 아이디로 사용합니다.</li>
|
||||
<li>
|
||||
<b>비밀번호</b>는 영문(대/소) + 숫자 + 특수문자 조합으로
|
||||
<b>8~16자</b> 범위로 설정합니다.
|
||||
</li>
|
||||
<li><b>2차 비밀번호</b>는 <b>숫자 4자리</b>로 설정합니다.</li>
|
||||
<li><b>만 14세 이상</b>만 가입이 가능합니다.</li>
|
||||
</ol>
|
||||
|
||||
<div class="guide-note">
|
||||
<b>Tip</b>
|
||||
같은 이메일로 재가입이 제한될 수 있어요. 가입 이메일은 신중하게 선택해 주세요.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Login --}}
|
||||
<article class="guide-card" aria-label="로그인 안내">
|
||||
<div class="guide-card__top">
|
||||
<div class="guide-card__icon" aria-hidden="true">②</div>
|
||||
<h3 class="guide-card__title">로그인</h3>
|
||||
</div>
|
||||
|
||||
<p class="guide-card__lead">
|
||||
이메일(ID)과 비밀번호로 로그인합니다. 보안을 위해 자동로그인/공용기기 사용에 유의해 주세요.
|
||||
</p>
|
||||
|
||||
<ul class="guide-bullets">
|
||||
<li><b>아이디</b>: 가입 시 사용한 이메일</li>
|
||||
<li><b>비밀번호</b>: 가입 시 설정한 비밀번호</li>
|
||||
<li><b>인증 안내</b>: 최초 1회 인증 이후에는 더 간편해질 수 있어요</li>
|
||||
</ul>
|
||||
|
||||
<div class="guide-note guide-note--muted">
|
||||
<b>권장</b>
|
||||
비밀번호는 주기적으로 변경하고, 다른 사이트와 동일한 비밀번호 사용은 피하세요.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- Purchase --}}
|
||||
<article class="guide-card" aria-label="상품권 구매 안내">
|
||||
<div class="guide-card__top">
|
||||
<div class="guide-card__icon" aria-hidden="true">③</div>
|
||||
<h3 class="guide-card__title">상품권 구매</h3>
|
||||
</div>
|
||||
|
||||
<p class="guide-card__lead">
|
||||
원하는 상품을 선택한 뒤 수량과 결제수단을 선택해 결제를 완료하면 됩니다.
|
||||
</p>
|
||||
|
||||
<ol class="guide-steps">
|
||||
<li><b>상품 선택</b>: 구매할 상품권을 고릅니다.</li>
|
||||
<li><b>구매하기</b>: 선택한 상품에서 구매 진행을 시작합니다.</li>
|
||||
<li><b>수량/결제수단 선택</b> 후 <b>결제하기</b>로 결제를 완료합니다.</li>
|
||||
</ol>
|
||||
|
||||
<div class="guide-note">
|
||||
<b>확인</b>
|
||||
결제 완료 후 안내가 진행되며, 필요한 경우 마이페이지 이용내역에서 확인할 수 있어요.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- QnA --}}
|
||||
<article class="guide-card" aria-label="1:1 문의 안내">
|
||||
<div class="guide-card__top">
|
||||
<div class="guide-card__icon" aria-hidden="true">④</div>
|
||||
<h3 class="guide-card__title">1:1 문의</h3>
|
||||
</div>
|
||||
|
||||
<p class="guide-card__lead">
|
||||
구매/발송/이용 관련 이슈는 1:1 문의로 접수하면 가장 정확하게 안내받을 수 있어요.
|
||||
</p>
|
||||
|
||||
<ul class="guide-bullets">
|
||||
<li><b>문의 주제</b>: 결제/발송/코드 확인/오류/계정</li>
|
||||
<li><b>필수 정보</b>: 주문 시각, 결제수단, 금액, 오류 화면(가능 시)</li>
|
||||
<li><b>처리 방식</b>: 접수 후 순차적으로 답변됩니다</li>
|
||||
</ul>
|
||||
|
||||
<div class="guide-note guide-note--muted">
|
||||
<b>빠른 처리 팁</b>
|
||||
화면 캡처 1장과 함께 상황을 “언제/어떤 결제/무슨 메시지”로 정리해 주시면 좋아요.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Small closing --}}
|
||||
<div class="guide-footer">
|
||||
<div class="guide-footer__box">
|
||||
<div class="guide-footer__title">가장 많이 막히는 지점</div>
|
||||
<div class="guide-footer__desc">
|
||||
회원가입 인증, 비밀번호 규칙, 결제 완료 여부 확인이 가장 많습니다.
|
||||
위 흐름대로 진행 후에도 해결이 어렵다면 1:1 문의로 접수해 주세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@include('web.partials.cs-quick-actions')
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@ -19,13 +19,105 @@
|
||||
|
||||
@section('subcontent')
|
||||
<div class="kakao-page">
|
||||
|
||||
{{-- H2 --}}
|
||||
@include('web.partials.content-head', [
|
||||
'title' => '카카오톡 상담 안내',
|
||||
'desc' => '운영시간과 안내사항을 확인하고 빠르게 문의해 주세요.'
|
||||
'title' => '카카오톡으로 빠르게 상담',
|
||||
'desc' => '간단 문의는 카카오톡이 가장 빠릅니다. 채널 추가 후 메시지를 남겨주세요.'
|
||||
])
|
||||
|
||||
{{-- TODO: 카카오 채널 링크/QR/운영시간/주의사항 등 --}}
|
||||
{{-- CTA + 운영시간 --}}
|
||||
<section class="kakao-hero">
|
||||
<div class="kakao-hero__card">
|
||||
<div class="kakao-hero__badge">KakaoTalk</div>
|
||||
<h3 class="kakao-hero__title">핀포유 채널로 바로 문의</h3>
|
||||
<p class="kakao-hero__desc">
|
||||
카카오톡에서 <b>“핀포유”</b> 또는 <b>“@pinforyou”</b>를 검색해 채널을 추가한 뒤,
|
||||
1:1 메시지로 문의를 남겨주세요.
|
||||
</p>
|
||||
|
||||
<div class="kakao-hero__meta">
|
||||
<div class="kakao-meta">
|
||||
<div class="kakao-meta__k">실시간 상담</div>
|
||||
<div class="kakao-meta__v">평일 09:00~17:00</div>
|
||||
</div>
|
||||
<div class="kakao-meta">
|
||||
<div class="kakao-meta__k">그 외 시간</div>
|
||||
<div class="kakao-meta__v">1:1 문의 접수 후 순차 답변</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kakao-hero__actions">
|
||||
{{-- ✅ “정확한 카카오 채널 URL”이 있으면 여기 href만 교체하면 됨 --}}
|
||||
<a class="btn btn--primary" href="javascript:void(0)" onclick="alert('카카오톡에서 “핀포유(@pinforyou)” 검색 후 채널 추가해 주세요.')">
|
||||
카카오톡에서 검색하기
|
||||
</a>
|
||||
<a class="btn btn--ghost" href="{{ route('web.cs.qna.index') }}">
|
||||
1:1 문의로 남기기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- 선택: 기존 이미지 가이드(원본은 pinforyou에서 이미지 1장) --}}
|
||||
{{-- 프로젝트에 이미지 넣고 싶으면: public/assets/images/cs/kakao_guide.png 로 저장 후 아래 주석 해제 --}}
|
||||
{{-- <img class="kakao-hero__img" src="{{ asset('assets/images/cs/kakao_guide.png') }}" alt="카카오톡 채널 추가 안내 이미지"> --}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- 3 Step Guide --}}
|
||||
<section class="kakao-steps">
|
||||
<h3 class="kakao-sec-title">채널 추가 3단계</h3>
|
||||
|
||||
<div class="kakao-step-grid">
|
||||
<div class="kakao-step">
|
||||
<div class="kakao-step__num">01</div>
|
||||
<div class="kakao-step__title">카카오톡 실행</div>
|
||||
<div class="kakao-step__desc">카카오톡 앱을 열고 상단 검색을 준비해요.</div>
|
||||
</div>
|
||||
|
||||
<div class="kakao-step">
|
||||
<div class="kakao-step__num">02</div>
|
||||
<div class="kakao-step__title">“핀포유 / @pinforyou” 검색</div>
|
||||
<div class="kakao-step__desc">채널 검색창에 이름 또는 아이디로 찾을 수 있어요.</div>
|
||||
</div>
|
||||
|
||||
<div class="kakao-step">
|
||||
<div class="kakao-step__num">03</div>
|
||||
<div class="kakao-step__title">채널 추가 후 메시지 문의</div>
|
||||
<div class="kakao-step__desc">친구(채널) 추가 후 1:1 메시지로 문의를 남겨주세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- 문의 전 준비 --}}
|
||||
<section class="kakao-help">
|
||||
<h3 class="kakao-sec-title">문의가 빨라지는 체크리스트</h3>
|
||||
|
||||
<div class="kakao-help-grid">
|
||||
<div class="kakao-card">
|
||||
<div class="kakao-card__title">기본 정보</div>
|
||||
<ul class="kakao-list">
|
||||
<li>주문번호(있으면 가장 빠름)</li>
|
||||
<li>결제수단(카드/휴대폰/계좌이체 등)</li>
|
||||
<li>문제 발생 시간과 상황 요약</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="kakao-card">
|
||||
<div class="kakao-card__title">스크린샷 팁</div>
|
||||
<ul class="kakao-list">
|
||||
<li>에러 화면/알림 화면 캡처</li>
|
||||
<li><b>민감정보(핀번호/개인정보)는 가리고</b> 보내주세요</li>
|
||||
<li>여러 장이면 순서대로 보내면 처리 속도↑</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@include('web.partials.cs-quick-actions')
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,30 +1,187 @@
|
||||
@extends('web.layouts.subpage')
|
||||
|
||||
@php
|
||||
$pageTitle = '1:1 문의';
|
||||
$pageDesc = '개인 문의는 1:1로 남겨주시면 순차적으로 답변드립니다.';
|
||||
$pageDesc = '결제·발송·코드 확인 등 상세 문의는 접수 후 순차적으로 답변드려요.';
|
||||
|
||||
$breadcrumbs = [
|
||||
['label' => '홈', 'url' => url('/')],
|
||||
['label' => '고객센터', 'url' => url('/cs')],
|
||||
['label' => '고객센터', 'url' => url('/cs/notice')],
|
||||
['label' => '1:1 문의', 'url' => url()->current()],
|
||||
];
|
||||
|
||||
$csActive = 'qna';
|
||||
// 고객센터 탭
|
||||
$subnavItems = collect(config('web.cs_tabs', []))->map(fn($t) => [
|
||||
'label' => $t['label'],
|
||||
'url' => route($t['route']),
|
||||
'key' => $t['key'],
|
||||
])->all();
|
||||
$subnavActive = 'qna';
|
||||
|
||||
// 문의 분류 (FAQ 카테고리와 맞춰 통일)
|
||||
$enquiryCategories = config('cs_faq.categories', [
|
||||
['key' => 'signup', 'label' => '회원가입 문의'],
|
||||
['key' => 'login', 'label' => '로그인 문의'],
|
||||
['key' => 'pay', 'label' => '결제 문의'],
|
||||
['key' => 'code', 'label' => '상품권 코드 문의'],
|
||||
['key' => 'event', 'label' => '이벤트 문의'],
|
||||
['key' => 'etc', 'label' => '기타문의'],
|
||||
]);
|
||||
@endphp
|
||||
|
||||
@extends('web.layouts.subpage')
|
||||
|
||||
@section('title', '1:1 문의 | PIN FOR YOU')
|
||||
@section('meta_description', 'PIN FOR YOU 1:1 문의 페이지입니다. 문의를 남겨주시면 순차적으로 답변드립니다.')
|
||||
@section('meta_description', '핀포유 1:1 문의 페이지입니다. 결제/발송/코드 확인 등 상세 문의를 접수하면 순차적으로 답변드립니다.')
|
||||
@section('canonical', url('/cs/qna'))
|
||||
|
||||
@section('subcontent')
|
||||
<div class="qna-page">
|
||||
@include('web.partials.content-head', [
|
||||
'title' => '문의하기',
|
||||
'desc' => '문의 내용을 남겨주시면 확인 후 빠르게 안내드리겠습니다.'
|
||||
])
|
||||
@include('web.partials.content-head', [
|
||||
'title' => '1:1 문의 접수',
|
||||
'desc' => '정확한 처리를 위해 주문 정보와 상황을 구체적으로 작성해 주세요.'
|
||||
])
|
||||
|
||||
{{-- TODO: 문의 폼 / 내 문의 내역 / 로그인 여부 처리 등 --}}
|
||||
<section class="qna" aria-label="1:1 문의 폼">
|
||||
|
||||
{{-- Top actions --}}
|
||||
<div class="qna-top">
|
||||
<div class="qna-top__hint">
|
||||
<span class="qna-pill">안내</span>
|
||||
운영시간 평일 09:00~18:00 · 점심 12:00~13:00 · 주말/공휴일 휴무
|
||||
</div>
|
||||
|
||||
<div class="qna-top__actions">
|
||||
<a class="qna-btn qna-btn--ghost" href="#" aria-disabled="true" onclick="return false;">
|
||||
내 문의내용 확인 (준비중)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Form card --}}
|
||||
<div class="qna-card">
|
||||
<div class="qna-card__head">
|
||||
<h2 class="qna-card__title">내용 작성</h2>
|
||||
<p class="qna-card__desc">답변 속도를 높이려면 “주문시각/결제수단/금액/오류문구”를 같이 적어주세요.</p>
|
||||
</div>
|
||||
|
||||
<form class="qna-form" id="qnaForm" action="#" method="post" enctype="multipart/form-data" novalidate>
|
||||
@csrf
|
||||
|
||||
{{-- Subject row --}}
|
||||
<div class="qna-grid">
|
||||
<div class="qna-field">
|
||||
<label class="qna-label" for="enquiry_code">문의분류 <span class="qna-req">*</span></label>
|
||||
<select class="qna-input" id="enquiry_code" name="enquiry_code" required>
|
||||
<option value="">문의분류선택</option>
|
||||
@foreach($enquiryCategories as $c)
|
||||
<option value="{{ $c['key'] }}">{{ $c['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="qna-help">가장 가까운 분류를 선택해 주세요.</div>
|
||||
</div>
|
||||
|
||||
<div class="qna-field">
|
||||
<label class="qna-label" for="enquiry_title">문의 제목 <span class="qna-req">*</span></label>
|
||||
<input class="qna-input" type="text" id="enquiry_title" name="enquiry_title"
|
||||
maxlength="60" placeholder="예) 결제는 완료됐는데 코드가 안 보여요" required>
|
||||
<div class="qna-help">제목은 60자 이내로 작성해 주세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="qna-field qna-field--mt">
|
||||
<label class="qna-label" for="enquiry_content">문의 내용 <span class="qna-req">*</span></label>
|
||||
<textarea class="qna-textarea" id="enquiry_content" name="enquiry_content"
|
||||
placeholder="문제 상황을 자세히 적어주세요. 예) 주문시각/결제수단/금액/오류메시지/상품명"
|
||||
required></textarea>
|
||||
<div class="qna-help">
|
||||
정확한 안내를 위해 개인정보(주민번호/전체 카드번호 등)는 작성하지 마세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Upload --}}
|
||||
<div class="qna-field qna-field--mt">
|
||||
<label class="qna-label" for="screenshots">스크린샷 <span class="qna-sub">(최대 4장)</span></label>
|
||||
<input class="qna-file" type="file" id="screenshots" name="screenshots[]"
|
||||
accept=".png,.jpeg,.jpg,.gif" multiple>
|
||||
<div class="qna-help">
|
||||
업로드 가능 확장자: .png, .jpeg, .jpg, .gif · 용량이 큰 파일은 업로드에 시간이 걸릴 수 있어요.
|
||||
</div>
|
||||
<div class="qna-filelist" id="fileList" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
{{-- Reply options --}}
|
||||
<div class="qna-field qna-field--mt">
|
||||
<div class="qna-label">추가회신</div>
|
||||
<div class="qna-choice">
|
||||
<label class="qna-check">
|
||||
<input type="checkbox" checked disabled>
|
||||
<span>웹답변</span>
|
||||
</label>
|
||||
|
||||
<label class="qna-radio">
|
||||
<input type="radio" name="return_type" value="sms">
|
||||
<span>SMS 알림</span>
|
||||
</label>
|
||||
|
||||
<label class="qna-radio">
|
||||
<input type="radio" name="return_type" value="email">
|
||||
<span>이메일 답변</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="qna-help">현재는 UI만 제공되며, 실제 알림 연동은 추후 적용됩니다.</div>
|
||||
</div>
|
||||
|
||||
{{-- Recaptcha placeholder --}}
|
||||
<div class="qna-field qna-field--mt">
|
||||
<div class="qna-recap">
|
||||
<div class="qna-recap__badge">reCAPTCHA</div>
|
||||
<div class="qna-recap__text">스팸 방지 기능은 추후 적용 예정입니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Submit --}}
|
||||
<div class="qna-actions">
|
||||
<button class="qna-btn qna-btn--primary" type="submit">문의등록</button>
|
||||
<div class="qna-actions__note">현재는 저장 기능이 준비 중입니다. (UI 확인용)</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@include('web.partials.cs-quick-actions')
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.getElementById('qnaForm');
|
||||
const fileInput = document.getElementById('screenshots');
|
||||
const fileList = document.getElementById('fileList');
|
||||
|
||||
// 파일 리스트 표시(최대 4장)
|
||||
const renderFiles = () => {
|
||||
if (!fileList || !fileInput) return;
|
||||
const files = Array.from(fileInput.files || []);
|
||||
fileList.innerHTML = '';
|
||||
|
||||
const shown = files.slice(0, 4);
|
||||
if (files.length > 4) {
|
||||
fileList.innerHTML = '<div class="qna-filelist__warn">최대 4장까지만 업로드할 수 있어요. 처음 4장만 표시됩니다.</div>';
|
||||
}
|
||||
|
||||
shown.forEach(f => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'qna-filelist__item';
|
||||
el.textContent = `• ${f.name}`;
|
||||
fileList.appendChild(el);
|
||||
});
|
||||
};
|
||||
|
||||
fileInput?.addEventListener('change', renderFiles);
|
||||
|
||||
// 임시 submit(저장 기능 추후)
|
||||
form?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
alert('현재는 문의 저장 기능이 준비 중입니다.\nUI/디자인 확인용으로 폼만 제공됩니다.');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
37
resources/views/web/layouts/auth.blade.php
Normal file
37
resources/views/web/layouts/auth.blade.php
Normal file
@ -0,0 +1,37 @@
|
||||
@extends('web.layouts.layout')
|
||||
|
||||
@section('body_class', 'auth-body')
|
||||
|
||||
{{-- auth 페이지는 보통 index 막는 게 안전 --}}
|
||||
@section('robots', 'noindex,follow')
|
||||
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-shell">
|
||||
{{-- Right: Card --}}
|
||||
<section class="auth-card" aria-label="@yield('card_aria', '인증 폼')">
|
||||
<div class="auth-card__head">
|
||||
<h1 class="auth-title">@yield('h1', '로그인')</h1>
|
||||
|
||||
@hasSection('desc')
|
||||
<p class="auth-desc">@yield('desc')</p>
|
||||
@else
|
||||
<p class="auth-desc">
|
||||
@yield('card_desc', '아이디(이메일)과 비밀번호를 입력해 주세요.')
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="auth-card__body">
|
||||
@yield('auth_content')
|
||||
</div>
|
||||
|
||||
<div class="auth-card__foot">
|
||||
@yield('auth_bottom')
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
@yield('head')
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="@yield('body_class')">
|
||||
|
||||
{{-- Header --}}
|
||||
@include('web.company.header')
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
<div class="auth-cs">
|
||||
<a class="auth-cs__link" href="{{ route('web.cs.faq.index') }}">FAQ</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a class="auth-cs__link" href="{{ route('web.cs.qna.index') }}">1:1 문의</a>
|
||||
</div>
|
||||
@ -0,0 +1,9 @@
|
||||
<nav class="auth-links" aria-label="인증 보조 링크">
|
||||
<a href="{{ route('web.auth.login') }}" class="auth-link">로그인</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a href="{{ route('web.auth.register') }}" class="auth-link">회원가입</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a href="{{ route('web.auth.find_id') }}" class="auth-link">아이디 찾기</a>
|
||||
<span class="auth-dot">·</span>
|
||||
<a href="{{ route('web.auth.find_password') }}" class="auth-link">비밀번호 찾기</a>
|
||||
</nav>
|
||||
9
resources/views/web/partials/auth/auth-header.blade.php
Normal file
9
resources/views/web/partials/auth/auth-header.blade.php
Normal file
@ -0,0 +1,9 @@
|
||||
<header class="auth-header" aria-label="인증 페이지 상단">
|
||||
<div class="auth-header__inner">
|
||||
<a class="auth-brand" href="{{ route('web.home') }}" aria-label="PIN FOR YOU 홈으로">
|
||||
<img src="{{ asset('assets/images/common/top_logo.png') }}" alt="PIN FOR YOU" class="auth-brand__logo">
|
||||
</a>
|
||||
|
||||
<a class="auth-home" href="{{ route('web.home') }}">홈</a>
|
||||
</div>
|
||||
</header>
|
||||
@ -1,37 +1,81 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
use App\Http\Controllers\Web\Auth\FindIdController;
|
||||
use App\Http\Controllers\Web\Auth\FindPasswordController;
|
||||
|
||||
Route::view('/', 'web.home')->name('web.home');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CS
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('cs')->name('web.cs.')->group(function () {
|
||||
Route::view('/notice', 'web.cs.notice.index')->name('notice.index');
|
||||
Route::view('/faq', 'web.cs.faq.index')->name('faq.index');
|
||||
Route::view('/kakao', 'web.cs.kakao.index')->name('kakao.index');
|
||||
Route::view('/qna', 'web.cs.qna.index')->name('qna.index');
|
||||
Route::view('/guide', 'web.cs.guide.index')->name('guide.index');
|
||||
Route::view('notice', 'web.cs.notice.index')->name('notice.index');
|
||||
Route::view('faq', 'web.cs.faq.index')->name('faq.index');
|
||||
Route::view('kakao', 'web.cs.kakao.index')->name('kakao.index');
|
||||
Route::view('qna', 'web.cs.qna.index')->name('qna.index');
|
||||
Route::view('guide', 'web.cs.guide.index')->name('guide.index');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| My Page
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('mypage')->name('web.mypage.')->group(function () {
|
||||
Route::view('/info', 'web.mypage.info.index')->name('info.index');
|
||||
Route::view('/usage', 'web.mypage.usage.index')->name('usage.index');
|
||||
Route::view('/exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||
Route::view('/qna', 'web.mypage.qna.index')->name('qna.index');
|
||||
Route::view('info', 'web.mypage.info.index')->name('info.index');
|
||||
Route::view('usage', 'web.mypage.usage.index')->name('usage.index');
|
||||
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||
Route::view('qna', 'web.mypage.qna.index')->name('qna.index');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Policy
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('policy')->name('web.policy.')->group(function () {
|
||||
Route::view('/', 'web.policy.index')->name('index');
|
||||
Route::view('/privacy', 'web.policy.privacy.index')->name('privacy.index');
|
||||
Route::view('/terms', 'web.policy.terms.index')->name('terms.index');
|
||||
Route::view('/email', 'web.policy.email.index')->name('email.index');
|
||||
Route::view('privacy', 'web.policy.privacy.index')->name('privacy.index');
|
||||
Route::view('terms', 'web.policy.terms.index')->name('terms.index');
|
||||
Route::view('email', 'web.policy.email.index')->name('email.index');
|
||||
});
|
||||
|
||||
Route::get('/login', function () {
|
||||
return view('web.auth.login'); // 너가 만든 로그인 뷰로 변경
|
||||
})->name('web.auth.login');
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auth
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('auth')->name('web.auth.')->group(function () {
|
||||
|
||||
Route::get('/register', function () {
|
||||
return view('web.auth.register'); // 회원가입 뷰로 변경
|
||||
})->name('web.auth.register');
|
||||
// 정적 페이지 (컨트롤러 불필요)
|
||||
Route::view('login', 'web.auth.login')->name('login');
|
||||
Route::view('register', 'web.auth.register')->name('register');
|
||||
|
||||
// 아이디 찾기 (컨트롤러)
|
||||
Route::get('find-id', [FindIdController::class, 'show'])->name('find_id');
|
||||
Route::post('find-id/send-code',[FindIdController::class, 'sendCode'])->name('find_id.send_code');
|
||||
Route::post('find-id/verify', [FindIdController::class, 'verify'])->name('find_id.verify');
|
||||
Route::post('find-id/reset', [FindIdController::class, 'reset'])->name('find_id.reset');
|
||||
|
||||
// 비밀번호 찾기 (컨트롤러)
|
||||
Route::get('find-password', [FindPasswordController::class, 'show'])->name('find_password');
|
||||
Route::post('find-password/send-code', [FindPasswordController::class, 'sendCode'])->name('find_password.send_code');
|
||||
Route::post('find-password/verify', [FindPasswordController::class, 'verify'])->name('find_password.verify');
|
||||
Route::post('find-password/reset', [FindPasswordController::class, 'reset'])->name('find_password.reset');
|
||||
Route::post('find-password/reset-session', [FindPasswordController::class, 'resetSession'])->name('find_password.reset_session');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Legacy redirects
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::get('/login', fn() => redirect()->route('web.auth.login'))->name('web.login');
|
||||
Route::get('/register', fn() => redirect()->route('web.auth.register'))->name('web.signup');
|
||||
|
||||
|
||||
Route::fallback(fn () => abort(404));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user