PhoneInput | CoUI
LogoCoUI

PhoneInput

국가 코드와 함께 전화번호를 입력하는 컴포넌트

PhoneInput#

국가 선택과 함께 전화번호를 입력하는 컴포넌트입니다. Flutter/Web 양쪽에서 동일한 API(PhoneInput)를 제공합니다.

Live Preview#

사용 시기 (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 / filterCountryCode0.129부터 읽히지 않습니다. 국가별로 정답이 갈리는 추측이었고, 어느 기본값도 영국과 이탈리아를 동시에 만족시키지 못합니다(위 "읽기 규칙" 참조).

접근성 (Accessibility)#

요소처리
국가 선택 버튼aria-label="Select country"
전화번호 입력type="tel", inputmode, autocomplete
키보드 이동Tab 순서: 국가 선택 → 전화번호 입력
  • TextField: 단일 국가용 일반 전화번호 입력
  • Select: 국가 선택만 독립 제공
  • Form: 폼 내에서 PhoneInput 사용