feat(auth): 회원가입 시 휴대폰·전화번호 선택 입력 추가
마이페이지·관리자 회원 폼에는 이미 연락처 필드가 있으나 회원가입 폼에만 빠져 두 화면이 불일치했다. DB·모델·Resource는 기존 존재하므로 회원가입 경로에만 검증·저장·입력 UI를 연결한다. 연락처는 선택 항목이다.
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- 회원가입 시 휴대폰번호와 전화번호를 선택 항목으로 입력할 수 있습니다. 두 항목 모두 필수가 아니며, 입력하면 내 정보에서 확인·수정할 수 있습니다.
|
||||
|
||||
#### 사이트맵
|
||||
|
||||
- 게시글·상품이 수백만 건으로 늘어나도 사이트맵을 안정적으로 생성하도록 개선했습니다. 예전에는 사이트맵을 만들 때 전체 데이터를 한꺼번에 메모리에 올리다가 데이터가 많은 사이트에서 생성이 실패했지만, 이제 나눠 읽어 처리하므로 메모리 부족 없이 완성됩니다. (#79 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Auth;
|
||||
|
||||
use App\Extension\HookManager;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -11,6 +12,8 @@ class RegisterRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool 항상 true (권한은 미들웨어 체인에서 처리)
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -20,7 +23,7 @@ class RegisterRequest extends FormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
@@ -29,7 +32,9 @@ class RegisterRequest extends FormRequest
|
||||
'nickname' => 'nullable|string|max:50',
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique(User::class)],
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'language' => 'nullable|string|in:' . implode(',', config('app.supported_locales', ['ko', 'en'])),
|
||||
'mobile' => ['nullable', 'string', 'max:20', 'regex:/^[0-9\-\+\(\)\s]+$/'],
|
||||
'phone' => ['nullable', 'string', 'max:20', 'regex:/^[0-9\-\+\(\)\s]+$/'],
|
||||
'language' => 'nullable|string|in:'.implode(',', config('app.supported_locales', ['ko', 'en'])),
|
||||
'agree_terms' => 'accepted',
|
||||
'agree_privacy' => 'accepted',
|
||||
];
|
||||
@@ -55,6 +60,10 @@ class RegisterRequest extends FormRequest
|
||||
'password.required' => __('validation.auth.password.required'),
|
||||
'password.min' => __('validation.auth.password.min'),
|
||||
'password.confirmed' => __('validation.auth.password.confirmed'),
|
||||
'mobile.max' => __('validation.auth.mobile.max'),
|
||||
'mobile.regex' => __('validation.auth.mobile.regex'),
|
||||
'phone.max' => __('validation.auth.phone.max'),
|
||||
'phone.regex' => __('validation.auth.phone.regex'),
|
||||
'agree_terms.accepted' => __('validation.auth.agree_terms.accepted'),
|
||||
'agree_privacy.accepted' => __('validation.auth.agree_privacy.accepted'),
|
||||
];
|
||||
|
||||
@@ -196,6 +196,8 @@ class AuthService
|
||||
'nickname' => $data['nickname'] ?? null,
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'mobile' => $data['mobile'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'language' => $data['language'] ?? 'ko',
|
||||
'country' => $this->detectCountryFromAcceptLanguage(),
|
||||
'ip_address' => request()->ip(),
|
||||
|
||||
@@ -1501,6 +1501,8 @@ HTTP/1.1 200
|
||||
| nickname | body | string | 아니오 | max 50 | 닉네임 |
|
||||
| email | body | string | 예 | max 255 | 이메일 주소 |
|
||||
| password | body | string | 예 | min 8 | 비밀번호 |
|
||||
| mobile | body | string | 아니오 | max 20, `숫자 - + ( ) 공백` | 휴대폰번호 (선택) |
|
||||
| phone | body | string | 아니오 | max 20, `숫자 - + ( ) 공백` | 전화번호 (선택) |
|
||||
| language | body | string | 아니오 | `ko`, `en`, `fr`, `ja` | 언어 코드 |
|
||||
| agree_terms | body | string | 아니오 | — | 이용약관 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) |
|
||||
| agree_privacy | body | string | 아니오 | — | 개인정보 처리방침 동의 (코어 필수 동의 — accepted 규칙, 미동의 시 가입 거부) |
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 확장 식별자·메뉴 역할·활동 로그 검색 조건의 안내 문구 일본어 번역 추가 — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 모듈·플러그인 파일 업로드 시 허용되지 않는 파일 형식 안내 문구 일본어 번역 추가.
|
||||
- 플러그인을 GitHub 주소 또는 ZIP 파일로 설치할 때의 입력 검증 안내 문구 일본어 번역 추가 — 주소 형식이 잘못됐거나 ZIP 이 아닌 파일을 올렸을 때, 파일 용량을 초과했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
- 회원가입 검증 메시지의 휴대폰·전화번호 항목(`validation.auth.mobile.*`, `validation.auth.phone.*`) 일본어 번역 추가 — 회원가입 시 연락처 형식 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.6] - 2026-07-22
|
||||
|
||||
|
||||
@@ -721,6 +721,14 @@ return [
|
||||
'required' => 'トークンは必須です。',
|
||||
'string' => 'トークンは文字列である必要があります。',
|
||||
],
|
||||
'mobile' => [
|
||||
'max' => '携帯電話番号は :max文字を超えることはできません。',
|
||||
'regex' => '携帯電話番号は数字と -, +, (), スペースのみ入力できます。',
|
||||
],
|
||||
'phone' => [
|
||||
'max' => '電話番号は :max文字を超えることはできません。',
|
||||
'regex' => '電話番号は数字と -, +, (), スペースのみ入力できます。',
|
||||
],
|
||||
],
|
||||
'asset' => [
|
||||
'identifier' => [
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.2] - 2026-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- 회원가입 화면의 휴대폰번호·전화번호 입력란 라벨·안내 문구(`auth.mobile`, `auth.phone`) 일본어 번역 추가 — 회원가입 시 연락처 입력란이 일본어 로케일에서 자연스럽게 표시됩니다.
|
||||
|
||||
## [1.0.1] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@@ -146,5 +146,9 @@
|
||||
"divider": "または",
|
||||
"continue": "非会員として注文する",
|
||||
"hint": "会員登録なしで注文できます。"
|
||||
}
|
||||
},
|
||||
"mobile": "携帯電話番号",
|
||||
"mobile_placeholder": "携帯電話番号を入力してください (選択)",
|
||||
"phone": "電話番号",
|
||||
"phone_placeholder": "電話番号を入力してください (選択)"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"en": "G7 template (sirsoft-basic) Japanese language pack (bundled)",
|
||||
"ja": "G7 テンプレート (sirsoft-basic) 日本語 言語パック(バンドル)"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"scope": "template",
|
||||
"target_identifier": "sirsoft-basic",
|
||||
|
||||
@@ -781,6 +781,14 @@ return [
|
||||
'nickname' => [
|
||||
'max' => 'Nickname may not be greater than :max characters.',
|
||||
],
|
||||
'mobile' => [
|
||||
'max' => 'Mobile number may not be greater than :max characters.',
|
||||
'regex' => 'Mobile number may only contain digits and -, +, (), spaces.',
|
||||
],
|
||||
'phone' => [
|
||||
'max' => 'Phone number may not be greater than :max characters.',
|
||||
'regex' => 'Phone number may only contain digits and -, +, (), spaces.',
|
||||
],
|
||||
'agree_terms' => [
|
||||
'accepted' => 'You must agree to the Terms of Service.',
|
||||
],
|
||||
|
||||
@@ -782,6 +782,14 @@ return [
|
||||
'nickname' => [
|
||||
'max' => '닉네임은 :max자를 초과할 수 없습니다.',
|
||||
],
|
||||
'mobile' => [
|
||||
'max' => '휴대폰번호는 :max자를 초과할 수 없습니다.',
|
||||
'regex' => '휴대폰번호는 숫자와 -, +, (), 공백만 입력할 수 있습니다.',
|
||||
],
|
||||
'phone' => [
|
||||
'max' => '전화번호는 :max자를 초과할 수 없습니다.',
|
||||
'regex' => '전화번호는 숫자와 -, +, (), 공백만 입력할 수 있습니다.',
|
||||
],
|
||||
'agree_terms' => [
|
||||
'accepted' => '이용약관에 동의해주세요.',
|
||||
],
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
## [1.0.4] - 2026-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- 회원가입 화면에 휴대폰번호·전화번호 입력란을 추가했습니다. 두 항목 모두 선택 사항입니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
#### 검색·봇 화면
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file auth-register-contact-fields.test.tsx
|
||||
* @description 회원가입 폼 휴대폰/전화번호 필드 정합성 회귀 테스트
|
||||
*
|
||||
* 배경: 마이페이지 프로필 편집에는 휴대폰/전화 필드가 있으나 회원가입 폼에만 빠져 있어
|
||||
* 두 화면이 불일치했다. 회원가입에도 연락처(선택) 입력 필드를 추가한다.
|
||||
*
|
||||
* 검증 항목:
|
||||
* 1. name="mobile" / name="phone" Input 노드가 각각 존재 (선택 항목이므로 required 없음)
|
||||
* 2. 라벨/placeholder 가 auth 네임스페이스 다국어 키를 사용
|
||||
* 3. 각 필드에 per-field 에러 span 이 존재 (_local.errors?.mobile / phone)
|
||||
* 4. 에러 필터 화이트리스트(폼 필드 vs 플러그인 기여 에러 구분)에 mobile/phone 이 포함
|
||||
* → 누락 시 mobile/phone 검증 에러가 상단 목록과 필드 하단에 이중 표시되는 회귀 방지
|
||||
* 5. 마케팅/광고 수신 동의 문구·체크박스는 코어 회원가입 폼에 들어가지 않는다
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import registerForm from '../../layouts/partials/auth/_register_form.json';
|
||||
|
||||
type Node = {
|
||||
type?: string;
|
||||
name?: string;
|
||||
if?: string;
|
||||
props?: Record<string, any>;
|
||||
children?: Node[];
|
||||
iteration?: Record<string, any>;
|
||||
text?: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
function walk(input: Node | Node[] | undefined, visit: (node: Node) => void): void {
|
||||
if (!input) return;
|
||||
const nodes = Array.isArray(input) ? input : [input];
|
||||
for (const node of nodes) {
|
||||
visit(node);
|
||||
if (Array.isArray(node.children)) walk(node.children, visit);
|
||||
}
|
||||
}
|
||||
|
||||
function findInput(name: string): Node | undefined {
|
||||
let found: Node | undefined;
|
||||
walk(registerForm as Node, (node) => {
|
||||
if (node.type === 'basic' && node.name === 'Input' && node.props?.name === name) {
|
||||
found = node;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
describe('회원가입 폼 휴대폰/전화번호 필드', () => {
|
||||
describe('입력 필드 존재', () => {
|
||||
it('name="mobile" Input 이 존재한다', () => {
|
||||
expect(findInput('mobile')).toBeDefined();
|
||||
});
|
||||
|
||||
it('name="phone" Input 이 존재한다', () => {
|
||||
expect(findInput('phone')).toBeDefined();
|
||||
});
|
||||
|
||||
it('휴대폰/전화 필드는 선택 항목이므로 required 가 아니다', () => {
|
||||
expect(findInput('mobile')?.props?.required).toBeUndefined();
|
||||
expect(findInput('phone')?.props?.required).toBeUndefined();
|
||||
});
|
||||
|
||||
it('라벨/placeholder 가 auth 다국어 키를 사용한다', () => {
|
||||
expect(findInput('mobile')?.props?.placeholder).toBe('$t:auth.mobile_placeholder');
|
||||
expect(findInput('phone')?.props?.placeholder).toBe('$t:auth.phone_placeholder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('per-field 에러 표시', () => {
|
||||
function hasErrorSpan(field: string): boolean {
|
||||
let found = false;
|
||||
walk(registerForm as Node, (node) => {
|
||||
if (
|
||||
node.type === 'basic' &&
|
||||
node.name === 'Span' &&
|
||||
typeof node.if === 'string' &&
|
||||
node.if.includes(`errors?.${field}`)
|
||||
) {
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
it('mobile 에러 span 이 존재한다', () => {
|
||||
expect(hasErrorSpan('mobile')).toBe(true);
|
||||
});
|
||||
|
||||
it('phone 에러 span 이 존재한다', () => {
|
||||
expect(hasErrorSpan('phone')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('에러 필터 화이트리스트 (이중 표시 회귀 방지)', () => {
|
||||
it('폼 필드 화이트리스트에 mobile/phone 이 포함되어 있다', () => {
|
||||
const raw = JSON.stringify(registerForm);
|
||||
const matches = raw.match(/\['email'[^\]]*'agree_privacy'\]/g) ?? [];
|
||||
// 두 곳(상단 목록 if + iteration source) 모두에 존재해야 함
|
||||
expect(matches.length).toBeGreaterThanOrEqual(2);
|
||||
for (const list of matches) {
|
||||
expect(list).toContain("'mobile'");
|
||||
expect(list).toContain("'phone'");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('마케팅/광고 수신 동의 미포함 (코어 중립성)', () => {
|
||||
it('코어 회원가입 폼에 마케팅/광고 수신 관련 문구가 없다', () => {
|
||||
const raw = JSON.stringify(registerForm);
|
||||
expect(raw).not.toContain('marketing');
|
||||
expect(raw).not.toContain('광고');
|
||||
expect(raw).not.toContain('마케팅');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,10 @@
|
||||
"name_placeholder": "Enter your name",
|
||||
"nickname": "Nickname",
|
||||
"nickname_placeholder": "Enter your nickname (optional)",
|
||||
"mobile": "Mobile",
|
||||
"mobile_placeholder": "Enter your mobile number (optional)",
|
||||
"phone": "Phone",
|
||||
"phone_placeholder": "Enter your phone number (optional)",
|
||||
"language": "Language",
|
||||
"language_select": "Select language",
|
||||
"language_ko": "한국어",
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
"name_placeholder": "이름을 입력하세요",
|
||||
"nickname": "닉네임",
|
||||
"nickname_placeholder": "닉네임을 입력하세요 (선택)",
|
||||
"mobile": "휴대폰번호",
|
||||
"mobile_placeholder": "휴대폰번호를 입력하세요 (선택)",
|
||||
"phone": "전화번호",
|
||||
"phone_placeholder": "전화번호를 입력하세요 (선택)",
|
||||
"language": "언어",
|
||||
"language_select": "언어 선택",
|
||||
"language_ko": "한국어",
|
||||
|
||||
@@ -68,12 +68,12 @@
|
||||
"comment": "비-form-field 에러 메시지 (identity / verification_token 등 plugin 기여 에러)를 추가 안내",
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{_local.errors && Object.keys(_local.errors).filter(function(k) { return ['email','password','password_confirmation','name','nickname','language','agree_terms','agree_privacy'].indexOf(k) === -1; }).length > 0}}",
|
||||
"if": "{{_local.errors && Object.keys(_local.errors).filter(function(k) { return ['email','password','password_confirmation','name','nickname','mobile','phone','language','agree_terms','agree_privacy'].indexOf(k) === -1; }).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.errors ?? {}).filter(function(entry) { return ['email','password','password_confirmation','name','nickname','language','agree_terms','agree_privacy'].indexOf(entry[0]) === -1; }).flatMap(function(entry) { return Array.isArray(entry[1]) ? entry[1] : [entry[1]]; })",
|
||||
"source": "Object.entries(_local.errors ?? {}).filter(function(entry) { return ['email','password','password_confirmation','name','nickname','mobile','phone','language','agree_terms','agree_privacy'].indexOf(entry[0]) === -1; }).flatMap(function(entry) { return Array.isArray(entry[1]) ? entry[1] : [entry[1]]; })",
|
||||
"item_var": "message"
|
||||
},
|
||||
"children": [
|
||||
@@ -314,6 +314,76 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "휴대폰번호 필드 (선택)",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
},
|
||||
"text": "$t:auth.mobile"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "tel",
|
||||
"name": "mobile",
|
||||
"placeholder": "$t:auth.mobile_placeholder",
|
||||
"className": "w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
||||
"disabled": "{{_local?.isRegistering}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.mobile}}",
|
||||
"props": {
|
||||
"className": "text-red-500 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.mobile?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "전화번호 필드 (선택)",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
},
|
||||
"text": "$t:auth.phone"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "tel",
|
||||
"name": "phone",
|
||||
"placeholder": "$t:auth.phone_placeholder",
|
||||
"className": "w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
|
||||
"disabled": "{{_local?.isRegistering}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.errors?.phone}}",
|
||||
"props": {
|
||||
"className": "text-red-500 text-xs mt-1"
|
||||
},
|
||||
"text": "{{_local.errors?.phone?.[0] ?? ''}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "언어 설정 필드",
|
||||
"type": "basic",
|
||||
|
||||
@@ -403,6 +403,76 @@ class UserAuthControllerTest extends TestCase
|
||||
$this->assertEquals('en', $user->language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 시 휴대폰/전화번호가 저장되는지 확인
|
||||
*/
|
||||
public function test_register_stores_mobile_and_phone(): void
|
||||
{
|
||||
$response = $this->jsonRequest()->postJson('/api/auth/register', [
|
||||
'name' => '테스트 사용자',
|
||||
'email' => 'contact@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
'mobile' => '010-1234-5678',
|
||||
'phone' => '02-123-4567',
|
||||
'agree_terms' => true,
|
||||
'agree_privacy' => true,
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'contact@example.com',
|
||||
'mobile' => '010-1234-5678',
|
||||
'phone' => '02-123-4567',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴대폰/전화번호는 선택 항목이므로 미입력 시에도 가입에 성공한다
|
||||
*/
|
||||
public function test_register_mobile_and_phone_are_optional(): void
|
||||
{
|
||||
$response = $this->jsonRequest()->postJson('/api/auth/register', [
|
||||
'name' => '테스트 사용자',
|
||||
'email' => 'nocontact@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
'agree_terms' => true,
|
||||
'agree_privacy' => true,
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
|
||||
$user = User::where('email', 'nocontact@example.com')->first();
|
||||
$this->assertNotNull($user);
|
||||
$this->assertNull($user->mobile);
|
||||
$this->assertNull($user->phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴대폰번호 형식 위반 시 검증 실패(422)
|
||||
*/
|
||||
public function test_register_validates_mobile_format(): void
|
||||
{
|
||||
$response = $this->jsonRequest()->postJson('/api/auth/register', [
|
||||
'name' => '테스트 사용자',
|
||||
'email' => 'badmobile@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
'mobile' => '휴대폰없음',
|
||||
'agree_terms' => true,
|
||||
'agree_privacy' => true,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['mobile']);
|
||||
|
||||
$this->assertDatabaseMissing('users', [
|
||||
'email' => 'badmobile@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입 시 'user' 역할 자동 할당
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# audit:allow test-scenario-coverage reason: 본 매니페스트는 신규 표면(회원가입 연락처 필드) 시나리오 SSoT 로 기록. 핵심 회귀 가드는 test_files 의 통과 테스트(백엔드 3 + 레이아웃 렌더링 8)로 커버. 선택 입력 필드 2개 추가라 발행/상태 전이 등 브라우저 특유 로직이 없어 Playwright spec 대신 렌더링 테스트로 대체(위지윅 발행 회귀 #238 유형의 위험 부재).
|
||||
|
||||
feature: 회원가입 시 휴대폰/전화번호 선택 입력
|
||||
|
||||
description: |
|
||||
회원가입 폼에서 연락처(휴대폰/전화번호)를 선택 항목으로 수집한다.
|
||||
|
||||
배경:
|
||||
- 마이페이지 프로필 편집·관리자 회원 폼에는 이미 mobile/phone 필드가 있으나
|
||||
회원가입 폼에만 빠져 두 화면이 불일치했다. 회원가입 경로에 "연결"만 추가.
|
||||
- DB(users.mobile/phone)·모델 $fillable·UserResource 는 이미 존재.
|
||||
RegisterRequest 검증 + AuthService::register $userData 주입 + 폼 입력칸만 추가.
|
||||
- 순수 개인정보 수집 (선택). 마케팅/광고 수신 동의는 코어에 넣지 않으며
|
||||
별도 마케팅 동의 플러그인이 register_extension_fields 앵커 +
|
||||
core.auth.record_consents 훅으로 주입한다.
|
||||
|
||||
axes:
|
||||
mobile_input: [empty, valid, invalid_chars]
|
||||
phone_input: [empty, valid, invalid_chars]
|
||||
|
||||
exclusions:
|
||||
- { mobile_input: invalid_chars, phone_input: valid, reason: "필드별 독립 검증 — 한 필드 위반 시 해당 필드 에러만 확인하면 충분" }
|
||||
- { mobile_input: valid, phone_input: invalid_chars, reason: "동일 사유 (필드별 독립 regex)" }
|
||||
|
||||
effects:
|
||||
- empty_mobile_and_phone_still_registers_successfully
|
||||
- valid_mobile_and_phone_persist_to_users_table
|
||||
- invalid_mobile_chars_rejected_with_422_validation_error
|
||||
- mobile_phone_are_nullable_optional_not_required
|
||||
- register_form_renders_mobile_and_phone_input_nodes
|
||||
- mobile_phone_per_field_error_span_present
|
||||
- error_filter_whitelist_includes_mobile_phone_to_prevent_double_display
|
||||
- core_register_form_excludes_marketing_consent_copy
|
||||
|
||||
test_files:
|
||||
- tests/Feature/Api/Auth/UserAuthControllerTest.php
|
||||
- templates/_bundled/sirsoft-basic/__tests__/layouts/auth-register-contact-fields.test.tsx
|
||||
Reference in New Issue
Block a user