diff --git a/CHANGELOG.md b/CHANGELOG.md index 7362e3d9..d40dc639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Added +- 회원가입 시 휴대폰번호와 전화번호를 선택 항목으로 입력할 수 있습니다. 두 항목 모두 필수가 아니며, 입력하면 내 정보에서 확인·수정할 수 있습니다. + #### 사이트맵 - 게시글·상품이 수백만 건으로 늘어나도 사이트맵을 안정적으로 생성하도록 개선했습니다. 예전에는 사이트맵을 만들 때 전체 데이터를 한꺼번에 메모리에 올리다가 데이터가 많은 사이트에서 생성이 실패했지만, 이제 나눠 읽어 처리하므로 메모리 부족 없이 완성됩니다. (#79 @jiwonpapa 님께서 제보해주셨습니다.) diff --git a/app/Http/Requests/Auth/RegisterRequest.php b/app/Http/Requests/Auth/RegisterRequest.php index 31e8e6bb..fa6face9 100644 --- a/app/Http/Requests/Auth/RegisterRequest.php +++ b/app/Http/Requests/Auth/RegisterRequest.php @@ -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> + * @return array|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'), ]; diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index 7f4edcb0..0bedfaed 100644 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -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(), diff --git a/docs/backend/api/auth.md b/docs/backend/api/auth.md index 8c2880f2..9d824d43 100644 --- a/docs/backend/api/auth.md +++ b/docs/backend/api/auth.md @@ -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 규칙, 미동의 시 가입 거부) | diff --git a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md index 2539ca68..dc72c475 100644 --- a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md @@ -11,6 +11,7 @@ - 확장 식별자·메뉴 역할·활동 로그 검색 조건의 안내 문구 일본어 번역 추가 — 잘못된 값을 입력했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다. - 모듈·플러그인 파일 업로드 시 허용되지 않는 파일 형식 안내 문구 일본어 번역 추가. - 플러그인을 GitHub 주소 또는 ZIP 파일로 설치할 때의 입력 검증 안내 문구 일본어 번역 추가 — 주소 형식이 잘못됐거나 ZIP 이 아닌 파일을 올렸을 때, 파일 용량을 초과했을 때의 안내가 일본어 로케일에서 자연스럽게 표시됩니다. +- 회원가입 검증 메시지의 휴대폰·전화번호 항목(`validation.auth.mobile.*`, `validation.auth.phone.*`) 일본어 번역 추가 — 회원가입 시 연락처 형식 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다. ## [1.0.6] - 2026-07-22 diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php b/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php index 2482b506..e7c1a759 100644 --- a/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php +++ b/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php @@ -721,6 +721,14 @@ return [ 'required' => 'トークンは必須です。', 'string' => 'トークンは文字列である必要があります。', ], + 'mobile' => [ + 'max' => '携帯電話番号は :max文字を超えることはできません。', + 'regex' => '携帯電話番号は数字と -, +, (), スペースのみ入力できます。', + ], + 'phone' => [ + 'max' => '電話番号は :max文字を超えることはできません。', + 'regex' => '電話番号は数字と -, +, (), スペースのみ入力できます。', + ], ], 'asset' => [ 'identifier' => [ diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md index f3df9b44..8cf6397f 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md @@ -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 diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/auth.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/auth.json index f49f6f52..26f6ab6b 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/auth.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/auth.json @@ -146,5 +146,9 @@ "divider": "または", "continue": "非会員として注文する", "hint": "会員登録なしで注文できます。" - } + }, + "mobile": "携帯電話番号", + "mobile_placeholder": "携帯電話番号を入力してください (選択)", + "phone": "電話番号", + "phone_placeholder": "電話番号を入力してください (選択)" } diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json index c79774c2..66c77bb3 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json @@ -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", diff --git a/lang/en/validation.php b/lang/en/validation.php index ba649e4b..d9fb829e 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -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.', ], diff --git a/lang/ko/validation.php b/lang/ko/validation.php index 4ec88533..549fbe5f 100644 --- a/lang/ko/validation.php +++ b/lang/ko/validation.php @@ -782,6 +782,14 @@ return [ 'nickname' => [ 'max' => '닉네임은 :max자를 초과할 수 없습니다.', ], + 'mobile' => [ + 'max' => '휴대폰번호는 :max자를 초과할 수 없습니다.', + 'regex' => '휴대폰번호는 숫자와 -, +, (), 공백만 입력할 수 있습니다.', + ], + 'phone' => [ + 'max' => '전화번호는 :max자를 초과할 수 없습니다.', + 'regex' => '전화번호는 숫자와 -, +, (), 공백만 입력할 수 있습니다.', + ], 'agree_terms' => [ 'accepted' => '이용약관에 동의해주세요.', ], diff --git a/templates/_bundled/sirsoft-basic/CHANGELOG.md b/templates/_bundled/sirsoft-basic/CHANGELOG.md index 51ba33b1..acfc0a2f 100644 --- a/templates/_bundled/sirsoft-basic/CHANGELOG.md +++ b/templates/_bundled/sirsoft-basic/CHANGELOG.md @@ -6,6 +6,10 @@ ## [1.0.4] - 2026-07-19 +### Added + +- 회원가입 화면에 휴대폰번호·전화번호 입력란을 추가했습니다. 두 항목 모두 선택 사항입니다. + ### Fixed #### 검색·봇 화면 diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/auth-register-contact-fields.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/layouts/auth-register-contact-fields.test.tsx new file mode 100644 index 00000000..600b061d --- /dev/null +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/auth-register-contact-fields.test.tsx @@ -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; + children?: Node[]; + iteration?: Record; + 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('마케팅'); + }); + }); +}); diff --git a/templates/_bundled/sirsoft-basic/lang/partial/en/auth.json b/templates/_bundled/sirsoft-basic/lang/partial/en/auth.json index ab6e20dc..9b4687d0 100644 --- a/templates/_bundled/sirsoft-basic/lang/partial/en/auth.json +++ b/templates/_bundled/sirsoft-basic/lang/partial/en/auth.json @@ -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": "한국어", diff --git a/templates/_bundled/sirsoft-basic/lang/partial/ko/auth.json b/templates/_bundled/sirsoft-basic/lang/partial/ko/auth.json index f43a0d8a..e27eb8e2 100644 --- a/templates/_bundled/sirsoft-basic/lang/partial/ko/auth.json +++ b/templates/_bundled/sirsoft-basic/lang/partial/ko/auth.json @@ -20,6 +20,10 @@ "name_placeholder": "이름을 입력하세요", "nickname": "닉네임", "nickname_placeholder": "닉네임을 입력하세요 (선택)", + "mobile": "휴대폰번호", + "mobile_placeholder": "휴대폰번호를 입력하세요 (선택)", + "phone": "전화번호", + "phone_placeholder": "전화번호를 입력하세요 (선택)", "language": "언어", "language_select": "언어 선택", "language_ko": "한국어", diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/auth/_register_form.json b/templates/_bundled/sirsoft-basic/layouts/partials/auth/_register_form.json index 6c9bd7ee..f08c5871 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/auth/_register_form.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/auth/_register_form.json @@ -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", diff --git a/tests/Feature/Api/Auth/UserAuthControllerTest.php b/tests/Feature/Api/Auth/UserAuthControllerTest.php index bff3edd8..6ebe938e 100644 --- a/tests/Feature/Api/Auth/UserAuthControllerTest.php +++ b/tests/Feature/Api/Auth/UserAuthControllerTest.php @@ -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' 역할 자동 할당 */ diff --git a/tests/scenarios/auth-register-contact-fields.yaml b/tests/scenarios/auth-register-contact-fields.yaml new file mode 100644 index 00000000..b01ea189 --- /dev/null +++ b/tests/scenarios/auth-register-contact-fields.yaml @@ -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 \ No newline at end of file