PhoneInput#
국가 선택과 함께 전화번호를 입력하는 컴포넌트입니다. Flutter/Web 양쪽에서 동일한 API(PhoneInput)를 제공합니다.
Live Preview#
class PhoneInputDefaultExample extends StatelessComponent {
const PhoneInputDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const PhoneInput(placeholder: 'Phone number');
}
}
class PhoneInputDefaultExample extends StatelessWidget {
const PhoneInputDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const PhoneInput(placeholder: 'Phone number');
}
}
class PhoneInputChainExample extends StatelessComponent {
const PhoneInputChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
const PhoneInput(placeholder: 'Phone number').radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
const PhoneInput(placeholder: 'Full control').withStyle(
const CorePhoneInputStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class PhoneInputChainExample extends StatelessWidget {
const PhoneInputChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
const PhoneInput(placeholder: 'Phone number').radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
const PhoneInput(placeholder: 'Full control').withStyle(
const CorePhoneInputStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 글로벌 서비스에서 여러 국가의 전화번호를 입력받아야 할 때
- 국가 코드(다이얼 코드)와 함께 전화번호를 저장해야 할 때
대신 다른 컴포넌트를 사용하세요:
TextField: 단일 국가 내수 서비스에서 단순 숫자 입력으로 충분할 때Select+TextField조합: 국가 선택과 번호 입력을 완전히 독립 제어해야 할 때
기본 사용법 (Basic Usage)#
Flutter와 Web 모두 PhoneInput 클래스와 CoreCountry 타입을 동일하게 사용합니다. 국가는 CoreCountry.byCode('KR')
룩업, const CoreCountry(...) 리터럴, 또는 전체 목록 CoreCountry.all(247개) 중에서 지정합니다.
// 기본 전화번호 입력 (CoreCountry 사용)
PhoneInput(
initialCountry: CoreCountry.byCode('KR'),
onChanged: (phoneNumber) {
handlePhoneChanged(phoneNumber);
},
)
// 초기값 설정 (초기 번호 포함)
PhoneInput(
initialCountry: CoreCountry.unitedStates,
initialNumber: '5551234567',
onChanged: handlePhoneChanged,
)
// 특정 국가만 허용
PhoneInput(
initialCountry: CoreCountry.byCode('KR'),
countries: [
CoreCountry.byCode('KR')!,
CoreCountry.byCode('US')!,
CoreCountry.byCode('JP')!,
],
onChanged: handlePhoneChanged,
)
// 기본 전화번호 입력 (CoreCountry 사용)
PhoneInput(
initialCountry: CoreCountry.byCode('KR'),
onChanged: (value) {
handlePhoneChanged(value);
},
)
// 초기값 설정 (초기 번호 포함)
PhoneInput(
initialCountry: CoreCountry.unitedStates,
initialNumber: '5551234567',
placeholder: '전화번호 입력',
onChanged: handlePhoneChanged,
)
// 특정 국가만 허용
PhoneInput(
initialCountry: CoreCountry.byCode('KR'),
countries: [
CoreCountry.byCode('KR')!,
CoreCountry.byCode('US')!,
CoreCountry.byCode('JP')!,
],
onChanged: handlePhoneChanged,
)
빠른 오버라이드 (Chain)#
이미 만든 PhoneInput 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 ==
CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class PhoneInputChainExample extends StatelessWidget {
const PhoneInputChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
const PhoneInput(placeholder: 'Phone number').radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
const PhoneInput(placeholder: 'Full control').withStyle(
const CorePhoneInputStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
class PhoneInputChainExample extends StatelessComponent {
const PhoneInputChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
const PhoneInput(placeholder: 'Phone number').radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
const PhoneInput(placeholder: 'Full control').withStyle(
const CorePhoneInputStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
Props / Parameters#
PhoneInput은 Flutter/Web에서 동일한 파라미터 이름을 사용합니다. 타입만 플랫폼별로 다릅니다.
| 속성 | Flutter 타입 | Web 타입 | 기본값 | 설명 |
|---|---|---|---|---|
initialCountry |
CoreCountry? |
CoreCountry? |
null |
초기 선택 국가 |
initialNumber |
String? |
String? |
null |
초기 번호 (국가 코드 제외) |
initialValue |
PhoneNumber? |
PhoneInputValue? |
null |
국가 + 번호 동시 지정 (편의). 여기 담긴 국가/번호가 initialCountry / initialNumber 보다 우선 |
countries |
List<CoreCountry>? |
List<CoreCountry>? |
null |
선택 가능한 국가 목록 |
onChanged |
ValueChanged<PhoneNumber>? |
CoreValueChanged<PhoneInputValue>? |
null |
값 변경 콜백 |
filterPlusCode |
bool |
bool |
true |
선택된 국가의 +다이얼코드를 타이핑했을 때만 제거 |
filterZeroCode |
bool |
bool |
false |
0.129부터 무시됨 — 아래 참조 |
filterCountryCode |
bool |
bool |
false |
0.129부터 무시됨 — 아래 참조 |
placeholder |
String? |
String? |
null |
플레이스홀더 텍스트 |
enabled |
bool |
bool |
true |
상호작용 활성화 |
onlyNumber |
bool |
bool |
true |
숫자만 입력 허용 |
phoneInputStyle |
CorePhoneInputStyle? |
CorePhoneInputStyle? |
null |
chrome 슬롯 (아래 표) |
PhoneNumber(Flutter)와 PhoneInputValue(Web)는 같은 CorePhoneNumber
의 이름 두 개입니다 — 한쪽에서 저장한 값을 다른 쪽에서 그대로 받습니다. country / number 를 들고, countryCode
· dialCode · fullNumber · value 를 파생해 줍니다.
Flutter 전용 파라미터#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
controller |
TextEditingController? |
null |
Flutter 런타임 인프라 — Web은 브라우저 native <input>이 같은 기능을 제공 |
CorePhoneInputStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
borderRadius |
CoreBorderRadius? |
Outer border radius override. |
flagGapStyle |
CoreGapStyle? |
Nested gap style between flag and dial code inside the selector — forwarded to
Gap(gapStyle: …)
.
|
countryGapStyle |
CoreGapStyle? |
Nested gap style between country selector and input field — forwarded to
Gap(gapStyle: …)
.
|
maxWidth |
double? |
Maximum width of the input row (logical px). |
padding |
CoreEdgeInsets? |
Inner padding override. No
default*
, deliberately: this is the OUTER row's padding, and a flush row is the design. The padding a phone input actually shows belongs to its parts, and each of those is stated — [defaultSelectorPadding], [defaultInputFieldPadding], [defaultDropdownItemPadding]. This field only exists so a caller can inset the assembled row, and Web emits it under
if (padding != null)
, so a default would add standing padding outside every phone input and shift the whole control against the three inner paddings that were measured for a flush row.
|
height |
double? |
Outer container height override (logical px). |
borderWidth |
double? |
Outer border stroke width override (logical px). |
inputFieldPadding |
CoreEdgeInsets? |
Inner padding of the phone-number input field. |
selectorPadding |
CoreEdgeInsets? |
Country selector trigger padding override. |
dropdownItemPadding |
CoreEdgeInsets? |
Dropdown / search item padding override. |
dropdownItemGapStyle |
CoreGapStyle? |
Nested gap style between dropdown-item flag and country name — forwarded to
Gap(gapStyle: …)
.
|
popupMaxWidth |
double? |
Maximum width of the country picker popup (logical px). |
popupMaxHeight |
double? |
Maximum height of the country picker popup (logical px). |
popoverTopOffset |
double? |
Vertical offset between the trigger and the popup (logical px). |
chevronIconStyle |
CoreIconStyle? |
Chevron icon style override — size defers to [defaultChevronIconStyle]. |
flagSize |
double? |
Flag emoji font size in the selector (logical px). |
dropdownFlagSize |
double? |
Flag emoji font size inside dropdown items (logical px). |
disabledOpacity |
double? |
Opacity applied to the whole component when disabled. |
popupBorderRadius |
CoreBorderRadius? |
Popup border radius override. |
dropdownDialCodeTextStyle |
CoreTextStyle? |
Muted dropdown text style override — drives both the dropdown-item dial-code span and the "No results" empty hint (Flutter), and the dropdown-item dial-code span (Web). Both the typography role token and the text colour are carried inside this slot, overlaying [defaultDropdownDialCodeTextStyle]. |
backgroundColor |
CoreColor? |
Outer container background fill override — defers to [defaultBackgroundColor]. |
borderColor |
CoreColor? |
Outer container / popup / divider border colour override — defers to [defaultBorderColor]. |
focusBorderColor |
CoreColor? |
Outer container focused border colour override — defers to [defaultFocusBorderColor]. |
hoverColor |
CoreColor? |
Hover / selected background override (selector trigger + dropdown rows) — defers to [defaultHoverColor]. |
separatorColor |
CoreColor? |
Selector↔input / picker divider colour override — defers to [defaultSeparatorColor]. |
placeholderColor |
CoreColor? |
Placeholder text colour override (phone field + search input) — defers to [defaultPlaceholderColor]. |
chevronColor |
CoreColor? |
Selector chevron icon colour override — enabled state, defers to [defaultChevronColor]. |
disabledChevronColor |
CoreColor? |
Selector chevron icon colour override — disabled state, defers to [defaultDisabledChevronColor]. |
dialCodeTextStyle |
CoreTextStyle? |
Selector dial-code label text style override — enabled state. Both the
labelLarge
role token and the
onSurface
colour live inside the slot, overlaying [defaultDialCodeTextStyle].
|
disabledDialCodeColor |
CoreColor? |
Selector dial-code label colour override — disabled state, defers to [defaultDisabledDialCodeColor]. |
bodyTextStyle |
CoreTextStyle? |
Body text style override (phone field + search input + country-name labels) — defers to [defaultBodyTextStyle]. |
searchFieldStyle |
CoreTextFieldStyle? |
Style override for the country-search
TextField
inside the dropdown panel — a distinct element from the outer field, so it is exposed as its own override slot (defers to [defaultSearchFieldStyle]). The phone-number field itself is
not
exposed: it is intentionally chromeless because the outer container owns the border / background (style it via this style's [backgroundColor] / [borderColor] / [padding] etc.).
|
popoverStyle |
CorePopoverStyle? |
Style override for the country-dropdown
Popover
panel — border / surface / radius / shadow flow through its nested
panelStyle
; defers to [defaultPopoverStyle].
|
테마 커스터마이징 (Theme)#
CorePhoneInputTheme으로 프로젝트 기본 스타일을 오버라이드:
CoreComponentTheme(
phoneInput: CorePhoneInputTheme(
style: CorePhoneInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius4),
flagGapStyle: CoreGapStyle(size: CoreSpace.space8),
countryGapStyle: CoreGapStyle(size: CoreSpace.space16),
maxWidth: 200.0,
),
),
)
Resolve 우선순위: 위젯 파라미터 > CorePhoneInputTheme > 디자인 시스템 기본값.
동작 스펙 (Behavior)#
인터랙션#
- 국가 선택: 다이얼 코드 버튼 클릭 시 국가 목록 팝업
- 번호 입력:
onlyNumber: true시 숫자 + 맨 앞+하나만 허용 - 키보드:
TextInputType.phone/inputmode="tel"로 전화번호 키패드 활성화 - 자동완성:
autocomplete="tel"/AutofillHints.telephoneNumber
읽기 규칙 (양쪽 공통)#
타이핑된 것만 제거하고, 나머지는 추측하지 않습니다. filterPlusCode: true(기본)는 선택된 국가의 +다이얼코드로 시작할 때 그것만 벗깁니다. 그 외에는 입력한 그대로 전달됩니다.
입력 칸을 고쳐 쓰는 게 아니라 읽을 때 파생하므로, 같은 텍스트를 두고 국가만 바꾸면 새 다이얼 코드로 다시 해석됩니다. 양 플랫폼이 CorePhoneNumber.fromInput
하나를 호출합니다.
왜 선행 0을 떼지 않나 (0.129 변경)
이전에는 선행 0과 + 없이 적힌 다이얼 코드도 제거했습니다. 둘 다 남은 숫자가 무슨 뜻인지에 대한 추측이고, 어떤 추측도 모든 나라에서 맞지 않습니다:
| 국가 | 입력 | 올바른 E.164 |
|---|---|---|
| 영국 | 07911123456 |
+447911123456 (트렁크 0 제거) |
| 이탈리아 | 0612345678 |
+390612345678 (트렁크 0 유지) |
두 입력은 모양이 같고 다이얼 코드에는 이를 구분할 정보가 없습니다. 그래서 0을 떼면 이탈리아가 깨지고, 안 떼면 영국이 깨집니다 — 손상이 사라지는 게 아니라 옮겨갑니다.
구분하려면 국가별 national prefix 표가 필요한데 CoreCountry(code/dialCode/name)에도 그 원본 데이터셋에도 없습니다.
그래서 지금은 입력한 값을 그대로 읽습니다. 다이얼 코드를 떼고 싶으면 타이핑하면 됩니다 — 그래서 onlyNumber가 켜져 있어도 맨 앞
+ 는 입력할 수 있습니다.
검색 (양쪽 공통)#
- 국가 드롭다운 내 검색 입력: name/dialCode/code 기준 필터링
- 양쪽 모두 드롭다운 패널 최상단에 검색 입력창 (autofocus)
상태#
enabled: false: 전체 컴포넌트 opacity 50% + 인터랙션 차단
사용 가이드라인 (Usage Guidelines)#
✅ Do#
국가 + 번호를 함께 다룰 때는 initialValue 하나로 넘기기
PhoneInput(
initialValue: PhoneNumber(
country: CoreCountry.byCode('KR')!,
number: '1012345678',
),
onChanged: handlePhoneChanged,
)
initialValue를 주면 그 안의 country/number가 initialCountry/initialNumber보다 우선해 완전히 대체합니다. 국가와 번호를 함께 다루는 값이 있으면 세 파라미터를 따로 채우지 말고 initialValue 하나로 넘기세요.
❌ Don't#
폐기된 필터 플래그에 기대지 않기
// ❌ 0.129부터 두 플래그는 무시된다 — 값은 '0821012345678' 그대로 온다
PhoneInput(
filterZeroCode: true,
filterCountryCode: true,
initialNumber: '0821012345678',
)
// ✅ 다이얼 코드를 떼려면 타이핑된 형태로 받는다
PhoneInput(initialNumber: '+821012345678') // → number: '1012345678'
filterZeroCode / filterCountryCode 는 0.129부터 읽히지 않습니다. 국가별로 정답이 갈리는 추측이었고, 어느 기본값도 영국과 이탈리아를 동시에 만족시키지 못합니다(위 "읽기 규칙" 참조).
접근성 (Accessibility)#
| 요소 | 처리 |
|---|---|
| 국가 선택 버튼 | aria-label="Select country" |
| 전화번호 입력 | type="tel", inputmode, autocomplete |
| 키보드 이동 | Tab 순서: 국가 선택 → 전화번호 입력 |