fix: @fox 전화번호·이메일을 시안 구조로 — 죽은 앞자리 셀렉트 제거, 이메일 줄 나눔
**FoxPhoneNumber** — `unit` 모드에서 `prefixOptions`가 비면 앞자리를 셀렉트가 아니라 입력 칸으로 그린다. 항목 없는 셀렉트는 placeholder "010"이 이미 고른 값처럼 보이는데 실제 값은 비어 있어, 뒷자리만 채운 번호가 조용히 만들어졌다(관리자 등록이 막힌 원인). 세 칸을 한 map으로 돌게 정리하면서 combine 분기와 모양을 맞췄다. **FoxEmail** — 시안(form-email 5543:9940)은 줄이 나뉜다: [아이디] @ [도메인] 한 줄, 그 아래 도메인 드롭다운, 그 아래 확인 버튼. 종전에는 넷을 한 줄에 두고 넘치면 접히게 했는데, 좁은 곳에서 드롭다운이 도메인 칸보다 먼저 접혀 순서가 어긋났다(320px 팝업에서 세 줄로 흩어졌다). `__body`(4px 간격) 안에 form-group·select·btn을 쌓고, 칸의 `min-inline-size: 24rem`을 0으로 바꿔 좁은 폭에서도 두 칸이 나눠 갖게 했다. 라벨의 필수 표시(`requirement`)도 다른 폼 컴포넌트와 같은 규약으로 추가했다. **관리자 등록 팝업** — 손으로 짜 두었던 이메일 칸(아이디·도메인·직접입력 상태 세 개)을 `FoxEmail` 하나로 바꿨다. 시안 5227:2036 그대로이고, 아이디 placeholder는 "ID"(종전 "1234-5678"은 전화번호 칸에서 복사된 값이었다), 도메인은 첫 항목이 골라진 채로 시작한다. 검증 — 시안 대조 실측: 아이디 149×40 · 도메인 149×40(readOnly, "naver.com") · 셀렉트 320×40 · 그룹↔셀렉트 간격 4 · 라벨 "이메일*"(시안 149/149/320/4와 일치). 동작: 직접입력 고르면 도메인 칸의 readOnly가 풀린다. prefixOptions 없이 쓴 FoxPhoneNumber는 앞자리가 입력 칸이 되고 "02"를 적으면 값에 실린다(종전에는 아무리 눌러도 빈 값이었다). Co-Authored-By: Claude Opus 5
@be86fe02d6960d0dbfe6a377ba10857359cb1e01
--- @fox/core/components/fox-email/fox-email.tsx
+++ @fox/core/components/fox-email/fox-email.tsx
... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useId, useState, type ReactNode } from "react";
|
| 4 |
-import { FoxFormLabel } from "../fox-form-label";
|
|
| 4 |
+import { FoxFormLabel, type FoxFormLabelRequirement } from "../fox-form-label";
|
|
| 5 | 5 |
import { cx } from "../../utils";
|
| 6 | 6 |
import { FoxButton } from "../fox-button";
|
| 7 | 7 |
import { FoxInput } from "../fox-input";
|
... | ... | @@ -29,6 +29,8 @@ |
| 29 | 29 |
verifyLabel?: string; |
| 30 | 30 |
|
| 31 | 31 |
label?: string; |
| 32 |
+ /** 라벨 뒤 필수·선택 표시. `label`이 없으면 의미 없다(`FoxInput`·`FoxSelect`와 같은 규약). */ |
|
| 33 |
+ requirement?: FoxFormLabelRequirement; |
|
| 32 | 34 |
/** 헬퍼 메시지. 오류일 때 색이 바뀐다. */ |
| 33 | 35 |
message?: string; |
| 34 | 36 |
messageIcon?: ReactNode; |
... | ... | @@ -88,6 +90,7 @@ |
| 88 | 90 |
onVerifyFail, |
| 89 | 91 |
verifyLabel = "확인", |
| 90 | 92 |
label, |
| 93 |
+ requirement, |
|
| 91 | 94 |
message, |
| 92 | 95 |
messageIcon, |
| 93 | 96 |
localPlaceholder = "아이디", |
... | ... | @@ -163,12 +166,16 @@ |
| 163 | 166 |
aria-labelledby={label ? `${autoId}-label` : undefined}
|
| 164 | 167 |
> |
| 165 | 168 |
{label && (
|
| 166 |
- <FoxFormLabel as="span" id={`${autoId}-label`}>
|
|
| 169 |
+ <FoxFormLabel as="span" id={`${autoId}-label`} requirement={requirement}>
|
|
| 167 | 170 |
{label}
|
| 168 | 171 |
</FoxFormLabel> |
| 169 | 172 |
)} |
| 170 | 173 |
|
| 171 |
- <div className="fox-email__row"> |
|
| 174 |
+ {/* 시안의 form-email 알맹이 — form-group / select / btn이 4px 간격으로 쌓인다.
|
|
| 175 |
+ 라벨·헬퍼와의 간격은 바깥(`field.root`)이 갖는다. */} |
|
| 176 |
+ <div className="fox-email__body"> |
|
| 177 |
+ {/* 시안의 form-group — 아이디·@·도메인이 한 줄이다. 도메인 목록은 아래 줄에 따로 선다. */}
|
|
| 178 |
+ <div className="fox-email__group"> |
|
| 172 | 179 |
<div className="fox-email__part"> |
| 173 | 180 |
<FoxInput |
| 174 | 181 |
size="md" |
... | ... | @@ -186,18 +193,6 @@ |
| 186 | 193 |
@ |
| 187 | 194 |
</span> |
| 188 | 195 |
|
| 189 |
- <div className="fox-email__part"> |
|
| 190 |
- <FoxSelect |
|
| 191 |
- size="md" |
|
| 192 |
- options={domainItems}
|
|
| 193 |
- value={parts.custom ? CUSTOM_DOMAIN : parts.domain}
|
|
| 194 |
- onValueChange={handleDomainPick}
|
|
| 195 |
- placeholder={domainPlaceholder}
|
|
| 196 |
- error={invalid}
|
|
| 197 |
- disabled={disabled}
|
|
| 198 |
- /> |
|
| 199 |
- </div> |
|
| 200 |
- |
|
| 201 | 196 |
{/* 목록에서 고른 값이 여기에 채워지고 잠긴다. "직접입력"일 때만 풀린다. */}
|
| 202 | 197 |
<div className="fox-email__part"> |
| 203 | 198 |
<FoxInput |
... | ... | @@ -211,17 +206,28 @@ |
| 211 | 206 |
disabled={disabled}
|
| 212 | 207 |
/> |
| 213 | 208 |
</div> |
| 209 |
+ </div> |
|
| 214 | 210 |
|
| 215 |
- {onVerify && (
|
|
| 216 |
- <FoxButton |
|
| 217 |
- size="md" |
|
| 218 |
- type="primary" |
|
| 219 |
- label={verifyLabel}
|
|
| 220 |
- loading={verifying}
|
|
| 221 |
- disabled={disabled}
|
|
| 222 |
- onAction={handleVerify}
|
|
| 223 |
- /> |
|
| 224 |
- )} |
|
| 211 |
+ <FoxSelect |
|
| 212 |
+ size="md" |
|
| 213 |
+ options={domainItems}
|
|
| 214 |
+ value={parts.custom ? CUSTOM_DOMAIN : parts.domain}
|
|
| 215 |
+ onValueChange={handleDomainPick}
|
|
| 216 |
+ placeholder={domainPlaceholder}
|
|
| 217 |
+ error={invalid}
|
|
| 218 |
+ disabled={disabled}
|
|
| 219 |
+ /> |
|
| 220 |
+ |
|
| 221 |
+ {onVerify && (
|
|
| 222 |
+ <FoxButton |
|
| 223 |
+ size="md" |
|
| 224 |
+ type="primary" |
|
| 225 |
+ label={verifyLabel}
|
|
| 226 |
+ loading={verifying}
|
|
| 227 |
+ disabled={disabled}
|
|
| 228 |
+ onAction={handleVerify}
|
|
| 229 |
+ /> |
|
| 230 |
+ )} |
|
| 225 | 231 |
</div> |
| 226 | 232 |
|
| 227 | 233 |
{message && (
|
--- @fox/core/components/fox-phone-number/fox-phone-number.tsx
+++ @fox/core/components/fox-phone-number/fox-phone-number.tsx
... | ... | @@ -184,47 +184,52 @@ |
| 184 | 184 |
const body = |
| 185 | 185 |
type === "unit" ? ( |
| 186 | 186 |
<div className="fox-phone-number__row"> |
| 187 |
- <div className="fox-phone-number__part"> |
|
| 188 |
- <FoxSelect |
|
| 189 |
- size="md" |
|
| 190 |
- options={prefixItems}
|
|
| 191 |
- value={parts[0]}
|
|
| 192 |
- onValueChange={(next) => {
|
|
| 193 |
- commit(0, next); |
|
| 194 |
- if (autoAdvance) {
|
|
| 195 |
- focusSegment(1); |
|
| 196 |
- } |
|
| 197 |
- }} |
|
| 198 |
- placeholder={placeholders[0]}
|
|
| 199 |
- error={errored}
|
|
| 200 |
- disabled={disabled || state === "disabled"}
|
|
| 201 |
- readOnly={readOnly || state === "view"}
|
|
| 202 |
- /> |
|
| 203 |
- </div> |
|
| 204 |
- |
|
| 205 |
- {[1, 2].map((index) => (
|
|
| 187 |
+ {[0, 1, 2].map((index) => (
|
|
| 206 | 188 |
<Fragment key={index}>
|
| 207 |
- <span className="fox-phone-number__separator" aria-hidden="true"> |
|
| 208 |
- - |
|
| 209 |
- </span> |
|
| 189 |
+ {index > 0 && (
|
|
| 190 |
+ <span className="fox-phone-number__separator" aria-hidden="true"> |
|
| 191 |
+ - |
|
| 192 |
+ </span> |
|
| 193 |
+ )} |
|
| 210 | 194 |
<div className="fox-phone-number__part"> |
| 211 |
- <FoxInput |
|
| 212 |
- size="md" |
|
| 213 |
- inputMode="numeric" |
|
| 214 |
- aria-label={SEGMENT_LABELS[index]}
|
|
| 215 |
- value={parts[index]}
|
|
| 216 |
- onChange={handleSegmentChange(index)}
|
|
| 217 |
- onKeyDown={handleSegmentKeyDown(index)}
|
|
| 218 |
- placeholder={placeholders[index]}
|
|
| 219 |
- maxLength={maxLengths[index]}
|
|
| 220 |
- invalid={invalid}
|
|
| 221 |
- disabled={disabled}
|
|
| 222 |
- readOnly={readOnly}
|
|
| 223 |
- state={state}
|
|
| 224 |
- ref={(node) => {
|
|
| 225 |
- tailRefs.current[index] = node; |
|
| 226 |
- }} |
|
| 227 |
- /> |
|
| 195 |
+ {/* 앞자리는 고를 목록이 있을 때만 셀렉트다. 목록이 비었는데 셀렉트를 그리면
|
|
| 196 |
+ placeholder("010")가 이미 고른 값처럼 보이는데 실제 값은 비어 있어, 뒷자리만
|
|
| 197 |
+ 채운 번호가 조용히 만들어진다 — 그때는 나머지 칸과 같은 입력 칸이 된다. */} |
|
| 198 |
+ {index === 0 && prefixItems.length > 0 ? (
|
|
| 199 |
+ <FoxSelect |
|
| 200 |
+ size="md" |
|
| 201 |
+ options={prefixItems}
|
|
| 202 |
+ value={parts[0]}
|
|
| 203 |
+ onValueChange={(next) => {
|
|
| 204 |
+ commit(0, next); |
|
| 205 |
+ if (autoAdvance) {
|
|
| 206 |
+ focusSegment(1); |
|
| 207 |
+ } |
|
| 208 |
+ }} |
|
| 209 |
+ placeholder={placeholders[0]}
|
|
| 210 |
+ error={errored}
|
|
| 211 |
+ disabled={disabled || state === "disabled"}
|
|
| 212 |
+ readOnly={readOnly || state === "view"}
|
|
| 213 |
+ /> |
|
| 214 |
+ ) : ( |
|
| 215 |
+ <FoxInput |
|
| 216 |
+ size="md" |
|
| 217 |
+ inputMode="numeric" |
|
| 218 |
+ aria-label={SEGMENT_LABELS[index]}
|
|
| 219 |
+ value={parts[index]}
|
|
| 220 |
+ onChange={handleSegmentChange(index)}
|
|
| 221 |
+ onKeyDown={handleSegmentKeyDown(index)}
|
|
| 222 |
+ placeholder={placeholders[index]}
|
|
| 223 |
+ maxLength={maxLengths[index]}
|
|
| 224 |
+ invalid={invalid}
|
|
| 225 |
+ disabled={disabled}
|
|
| 226 |
+ readOnly={readOnly}
|
|
| 227 |
+ state={state}
|
|
| 228 |
+ ref={(node) => {
|
|
| 229 |
+ tailRefs.current[index] = node; |
|
| 230 |
+ }} |
|
| 231 |
+ /> |
|
| 232 |
+ )} |
|
| 228 | 233 |
</div> |
| 229 | 234 |
</Fragment> |
| 230 | 235 |
))} |
--- @fox/styles/_fox-email.scss
+++ @fox/styles/_fox-email.scss
... | ... | @@ -1,25 +1,27 @@ |
| 1 | 1 |
// FoxEmail — 시안: 통합관리자페이지 디자인시스템(KE9UILWhB8qTBe14uXUn4F) |
| 2 | 2 |
// Figma form-email (136:3092 · 섹션 392:5820) |
| 3 | 3 |
// |
| 4 |
-// [아이디] @ [도메인 드롭다운] [도메인 직접입력] + 선택적 확인 버튼. |
|
| 5 |
-// 시안에서 세 칸이 모두 input·select 인스턴스이고 버튼도 btn 인스턴스라, 이 파일은 줄 |
|
| 6 |
-// 배치와 `@` 구분자만 갖는다 — 상자 모양과 상태는 FoxInput·FoxSelect가 가진 것을 쓴다. |
|
| 4 |
+// 시안 구조는 **줄이 나뉜다** — [아이디] @ [도메인] 한 줄, 그 아래 도메인 드롭다운, 그 아래 |
|
| 5 |
+// 선택적 확인 버튼(form-email 5543:9940: form-group 320×40 · select 320×40 · btn). |
|
| 6 |
+// 종전에는 넷을 한 줄에 두고 넘치면 접히게 했는데, 그러면 좁은 곳에서 드롭다운이 도메인 칸보다 |
|
| 7 |
+// 먼저 접혀 시안과 순서가 어긋났다. |
|
| 8 |
+// |
|
| 9 |
+// 세 칸이 모두 input·select 인스턴스이고 버튼도 btn 인스턴스라, 이 파일은 줄 배치와 `@` |
|
| 10 |
+// 구분자만 갖는다 — 상자 모양과 상태는 FoxInput·FoxSelect가 가진 것을 쓴다. |
|
| 7 | 11 |
// |
| 8 | 12 |
// 마크업 계약 (React 밖 소비자용): |
| 9 | 13 |
// <div class="fox-email"> |
| 10 |
-// <div class="fox-email__row"> |
|
| 11 |
-// <div class="fox-email__part">…fox-input…</div> |
|
| 14 |
+// <div class="fox-email__body"> |
|
| 15 |
+// <div class="fox-email__group"> |
|
| 16 |
+// <div class="fox-email__part">…fox-input(아이디)…</div> |
|
| 12 | 17 |
// <span class="fox-email__at">@</span> |
| 13 |
-// <div class="fox-email__part">…fox-select…</div> |
|
| 14 |
-// <div class="fox-email__part">…fox-input…</div> |
|
| 15 |
-// <button class="fox-button fox-button--primary fox-button--md">확인</button> |
|
| 18 |
+// <div class="fox-email__part">…fox-input(도메인)…</div> |
|
| 16 | 19 |
// </div> |
| 20 |
+// …fox-select(도메인 목록)… |
|
| 21 |
+// <button class="fox-button …">확인</button> <!-- 선택 --> |
|
| 17 | 22 |
// </div> |
| 18 | 23 |
// |
| 19 | 24 |
// 토큰이 아닌 값(사용자 승인): |
| 20 |
-// - 칸의 `min-inline-size: 24rem`(240px): 시안이 세 칸을 240px로 고정했는데 토큰 |
|
| 21 |
-// 스케일에 240이 없다. 고정 대신 최소폭으로 두어 넓은 곳에서는 나눠 갖고 좁아지면 |
|
| 22 |
-// 줄이 넘어가게 했다. |
|
| 23 | 25 |
// - line-height / letter-spacing: 시안 텍스트 스타일의 값이나 Figma가 변수로 |
| 24 | 26 |
// export하지 않는다. |
| 25 | 27 |
|
... | ... | @@ -30,18 +32,27 @@ |
| 30 | 32 |
@include field.root; |
| 31 | 33 |
|
| 32 | 34 |
|
| 33 |
- // 좁아지면 칸이 찌그러지는 대신 줄이 넘어간다. |
|
| 34 |
- &__row {
|
|
| 35 |
+ // 세 줄이 4px 간격으로 쌓인다(시안 form-group 0-40 · select 44-84). |
|
| 36 |
+ &__body {
|
|
| 35 | 37 |
display: flex; |
| 36 |
- flex-wrap: wrap; |
|
| 38 |
+ inline-size: 100%; |
|
| 39 |
+ flex-direction: column; |
|
| 40 |
+ gap: fox.gap(2); |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ // 아이디·@·도메인이 한 줄을 나눠 갖는다(시안 149 + @ + 149). |
|
| 44 |
+ &__group {
|
|
| 45 |
+ display: flex; |
|
| 37 | 46 |
align-items: center; |
| 38 | 47 |
gap: fox.gap(2); |
| 39 | 48 |
inline-size: 100%; |
| 40 | 49 |
} |
| 41 | 50 |
|
| 51 |
+ // 두 칸이 남는 폭을 똑같이 나눈다. `min-inline-size: 0`이 없으면 입력의 기본 폭 때문에 |
|
| 52 |
+ // 좁은 곳에서 넘친다. |
|
| 42 | 53 |
&__part {
|
| 43 | 54 |
flex: 1 1 0; |
| 44 |
- min-inline-size: 24rem; |
|
| 55 |
+ min-inline-size: 0; |
|
| 45 | 56 |
} |
| 46 | 57 |
|
| 47 | 58 |
&__at {
|
--- app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
... | ... | @@ -9,9 +9,8 @@ |
| 9 | 9 |
} from 'react'; |
| 10 | 10 |
import { FoxButton } from '@fox/core/components/fox-button';
|
| 11 | 11 |
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
|
| 12 |
-import { FoxFormLabel } from '@fox/core/components/fox-form-label';
|
|
| 12 |
+import { FoxEmail } from '@fox/core/components/fox-email';
|
|
| 13 | 13 |
import { FoxInput } from '@fox/core/components/fox-input';
|
| 14 |
-import { FoxSelect } from '@fox/core/components/fox-select';
|
|
| 15 | 14 |
import { FoxModal } from '@fox/core/components/fox-modal';
|
| 16 | 15 |
import {
|
| 17 | 16 |
foxPasswordValidator, |
... | ... | @@ -46,10 +45,7 @@ |
| 46 | 45 |
onClose: () => void; |
| 47 | 46 |
} |
| 48 | 47 |
|
| 49 |
-/** 목록에 없는 주소를 위해 도메인 칸을 입력으로 바꾸는 값. 실제 도메인과 겹치지 않게 잡았다. */ |
|
| 50 |
-const CUSTOM_EMAIL_DOMAIN = '__custom__'; |
|
| 51 |
- |
|
| 52 |
-/** 시안의 도메인 목록. 맨 아래 "직접입력"은 같은 칸을 입력으로 바꾼다(상자를 늘리지 않는다). */ |
|
| 48 |
+/** 시안의 도메인 목록. 맨 아래 "직접입력" 항목은 `FoxEmail`이 스스로 더한다. */ |
|
| 53 | 49 |
const EMAIL_DOMAIN_OPTIONS = [ |
| 54 | 50 |
'naver.com', |
| 55 | 51 |
'gmail.com', |
... | ... | @@ -94,9 +90,9 @@ |
| 94 | 90 |
|
| 95 | 91 |
// 앞자리는 고르지 않아도 되도록 기본값으로 시작한다(상수 주석 참조). |
| 96 | 92 |
const [phoneNumber, setPhoneNumber] = useState(DEFAULT_MOBILE_PHONE_PREFIX); |
| 97 |
- const [emailLocal, setEmailLocal] = useState('');
|
|
| 98 |
- const [emailDomain, setEmailDomain] = useState(EMAIL_DOMAIN_OPTIONS[0]); |
|
| 99 |
- const [isCustomDomain, setIsCustomDomain] = useState(false); |
|
| 93 |
+ // `FoxEmail`이 아이디·도메인을 스스로 나눠 들고 합친 값 하나를 내보낸다. |
|
| 94 |
+ // 도메인은 시안처럼 첫 항목이 골라진 채로 시작한다(아이디만 적으면 되게). |
|
| 95 |
+ const [emailValue, setEmailValue] = useState(`@${EMAIL_DOMAIN_OPTIONS[0]}`);
|
|
| 100 | 96 |
// 이메일은 칸이 둘로 나뉘어 있어 검증 대상이 **합친 값**이다 — FoxInput 한 칸이 스스로 |
| 101 | 97 |
// 판정할 수 없어 여기서 돌리고, `touched`도 직접 든다(FoxInput의 blur 규칙과 같은 시점). |
| 102 | 98 |
const [isEmailTouched, setIsEmailTouched] = useState(false); |
... | ... | @@ -117,7 +113,9 @@ |
| 117 | 113 |
|
| 118 | 114 |
// 둘 중 하나라도 비면 **빈 이메일**로 본다 — 도메인만 고른 상태(`@naver.com`)를 값으로 보내면 |
| 119 | 115 |
// "형식이 올바르지 않습니다"가 뜨지만, 사용자가 한 일은 아무것도 입력하지 않은 것이다. |
| 120 |
- const email = emailLocal && emailDomain ? `${emailLocal}@${emailDomain}` : '';
|
|
| 116 |
+ // (`FoxEmail`은 한쪽만 채워도 `@`를 붙여 내보내므로 여기서 다시 가른다.) |
|
| 117 |
+ const [emailLocal = '', emailDomain = ''] = emailValue.split('@');
|
|
| 118 |
+ const email = emailLocal && emailDomain ? emailValue : ''; |
|
| 121 | 119 |
|
| 122 | 120 |
const emailMessage = |
| 123 | 121 |
errors.email ?? |
... | ... | @@ -240,65 +238,19 @@ |
| 240 | 238 |
errors={errors}
|
| 241 | 239 |
emailField={
|
| 242 | 240 |
<> |
| 243 |
- <div className={styles.field}>
|
|
| 244 |
- <FoxFormLabel as="span" requirement="required"> |
|
| 245 |
- 이메일 |
|
| 246 |
- </FoxFormLabel> |
|
| 247 |
- <div className={styles.emailRow}>
|
|
| 248 |
- <span className={styles.emailPart}>
|
|
| 249 |
- <FoxInput |
|
| 250 |
- size="md" |
|
| 251 |
- aria-label="이메일 아이디" |
|
| 252 |
- placeholder="1234-5678" |
|
| 253 |
- value={emailLocal}
|
|
| 254 |
- onChange={setEmailLocal}
|
|
| 255 |
- onBlur={() => setIsEmailTouched(true)}
|
|
| 256 |
- invalid={Boolean(emailMessage)}
|
|
| 257 |
- /> |
|
| 258 |
- </span> |
|
| 259 |
- <span aria-hidden="true">@</span> |
|
| 260 |
- <span className={styles.emailPart}>
|
|
| 261 |
- {isCustomDomain ? (
|
|
| 262 |
- <FoxInput |
|
| 263 |
- size="md" |
|
| 264 |
- aria-label="이메일 도메인" |
|
| 265 |
- placeholder="직접입력" |
|
| 266 |
- value={emailDomain}
|
|
| 267 |
- onChange={setEmailDomain}
|
|
| 268 |
- onBlur={() => setIsEmailTouched(true)}
|
|
| 269 |
- invalid={Boolean(emailMessage)}
|
|
| 270 |
- /> |
|
| 271 |
- ) : ( |
|
| 272 |
- <FoxSelect |
|
| 273 |
- size="md" |
|
| 274 |
- options={[
|
|
| 275 |
- ...EMAIL_DOMAIN_OPTIONS.map((domain) => ({
|
|
| 276 |
- value: domain, |
|
| 277 |
- label: domain, |
|
| 278 |
- })), |
|
| 279 |
- { value: CUSTOM_EMAIL_DOMAIN, label: '직접입력' },
|
|
| 280 |
- ]} |
|
| 281 |
- value={emailDomain}
|
|
| 282 |
- onValueChange={(next) => {
|
|
| 283 |
- setIsEmailTouched(true); |
|
| 284 |
- if (next === CUSTOM_EMAIL_DOMAIN) {
|
|
| 285 |
- setIsCustomDomain(true); |
|
| 286 |
- setEmailDomain('');
|
|
| 287 |
- return; |
|
| 288 |
- } |
|
| 289 |
- setEmailDomain(next); |
|
| 290 |
- }} |
|
| 291 |
- error={Boolean(emailMessage)}
|
|
| 292 |
- /> |
|
| 293 |
- )} |
|
| 294 |
- </span> |
|
| 295 |
- </div> |
|
| 296 |
- {emailMessage && (
|
|
| 297 |
- <p className={styles.fieldError} role="alert">
|
|
| 298 |
- {emailMessage}
|
|
| 299 |
- </p> |
|
| 300 |
- )} |
|
| 301 |
- </div> |
|
| 241 |
+ <FoxEmail |
|
| 242 |
+ label="이메일" |
|
| 243 |
+ requirement="required" |
|
| 244 |
+ domainOptions={[...EMAIL_DOMAIN_OPTIONS]}
|
|
| 245 |
+ localPlaceholder="ID" |
|
| 246 |
+ value={emailValue}
|
|
| 247 |
+ onChange={(next) => {
|
|
| 248 |
+ setEmailValue(next); |
|
| 249 |
+ setIsEmailTouched(true); |
|
| 250 |
+ }} |
|
| 251 |
+ message={emailMessage}
|
|
| 252 |
+ invalid={Boolean(emailMessage)}
|
|
| 253 |
+ /> |
|
| 302 | 254 |
<input type="hidden" name="email" value={email} />
|
| 303 | 255 |
</> |
| 304 | 256 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?