ObjectInput | CoUI
본문으로 건너뛰기
LogoCoUI

ObjectInput

구분자로 연결된 키-값 구조화 입력 컴포넌트

목차

연/월/일처럼 여러 키-값 필드를 구분자로 연결해 한 줄로 입력받는 구조화 입력 컴포넌트입니다. 각 필드의 값이 Map<String, String> (필드 key → 값) 으로 방출됩니다. 단일 문자열을 마스킹하는 FormattedInput 의 구조화 데이터 버전입니다.

기본 사용법 (Basic Usage)#

ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: (values) => print(values),
)
ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: (values) => print(values),
)

빠른 오버라이드 (Chain)#

이미 만든 ObjectInput 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class ObjectInputChainExample extends StatelessWidget {
  const ObjectInputChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => debugPrint('$values'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => debugPrint('$values'),
        ).withStyle(
          const CoreObjectInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
    );
  }
}
class ObjectInputChainExample extends StatelessComponent {
  const ObjectInputChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => print('$values'),
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
        ObjectInput(
          fields: const [
            CoreObjectInputField(
              key: 'year',
              placeholder: 'YYYY',
              maxLength: 4,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'month',
              placeholder: 'MM',
              maxLength: 2,
              pattern: r'\d',
            ),
            CoreObjectInputField(
              key: 'day',
              placeholder: 'DD',
              maxLength: 2,
              pattern: r'\d',
            ),
          ],
          separator: '/',
          onChanged: (values) => print('$values'),
        ).withStyle(
          const CoreObjectInputStyle(
            backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            borderWidth: CoreStrokeWidth.stroke2,
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 연/월/일, 시/분/초처럼 의미가 다른 여러 필드를 한 줄로 입력받는 경우
  • 각 필드 값을 키별로 구분해 다뤄야 하는 경우

대신 다른 컴포넌트를 사용하세요:

  • FormattedInput: 단일 문자열을 마스킹하는 경우
  • TextField: 단일 자유 입력

Props / Parameters#

W는 leading/trailing 콘텐츠 타입으로 Flutter의 Widget, Web의 Component입니다. 필드별 값은 플랫폼과 무관한 Map<String, String>입니다.

속성타입기본값설명
fields List<CoreObjectInputField> — 키-값 필드 정의 목록 (필수)
values Map<String, String>? null 초기 값 (필드 key → 값)
separator String? null 필드 사이에 표시할 구분자 텍스트
enabledbooltrue상호작용 가능 여부
onChanged void Function(Map<String, String>)? null 값 변경 콜백 (전체 맵 전달)
leadingW?null필드 앞 콘텐츠
trailingW?null필드 뒤 콘텐츠
shortFieldMaxChars int 2 짧은 폭 bucket을 고르는 최대 문자 수
mediumFieldMaxChars int 4 중간 폭 bucket을 고르는 최대 문자 수
objectInputStyle CoreObjectInputStyle? null 컨테이너 색상 / 테두리 / padding 오버라이드

CoreObjectInputField#

속성타입설명
keyString값 맵에서의 고유 키
placeholderString빈 필드 placeholder
maxLengthint?최대 입력 길이
pattern String? 각 문자가 만족해야 하는 정규식 소스. 생략하면 문자 필터 없음

스타일 시스템 (Style System)#

ObjectInput 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreObjectInputStyle 단일 슬롯으로 흐릅니다.

CoreObjectInputStyle 필드#

필드타입설명
backgroundColor CoreColor? Container background colour override.
borderColor CoreColor? Container border colour override.
hoverBorderColor CoreColor? Container border colour override while the pointer is over the container. null → [defaultHoverBorderColor]. Never applied while the container is focused (the focus border wins) or disabled.
focusBorderColor CoreColor? Container focus-ring border colour override.
disabledBackgroundColor CoreColor? Container background colour override while disabled. null → [defaultDisabledBackgroundColor].
disabledBorderColor CoreColor? Container border colour override while disabled. null → [defaultDisabledBorderColor].
disabledSeparatorColor CoreColor? Field separator text colour override while disabled. null → [defaultDisabledSeparatorColor].
borderRadius CoreBorderRadius? Container border radius override.
borderWidth double? Container border width override (logical px).
padding CoreEdgeInsets? Container padding override.
height double? Container height override (logical px). null → [defaultHeight], which moves with the density; a stated height stays as stated.
fieldSpacing double? Gap between the container's children override (logical px). null → [defaultFieldSpacing].
separatorTextStyle CoreTextStyle? Field separator text style override (typography role / font / colour).
fieldInputStyle CoreTextFieldStyle? Per-field TextField style override (chrome + value / placeholder typography). Nested child-component slot forwarded to the composed field TextField on both platforms.
shortFieldMaxWidth double? Max-width override for short fields (1–2 chars) (logical px).
mediumFieldMaxWidth double? Max-width override for medium fields (3–4 chars) (logical px).
longFieldMaxWidth double? Max-width override for long fields (5+ chars) (logical px).
separatorPadding CoreEdgeInsets? Custom padding around the field separator glyph (pre-scaling). null → [defaultSeparatorPadding].
charWidth double? Per-character width used to size a field to its maxChars bucket (logical px, pre-scaling). null → [defaultCharWidth].

동작 스펙 (Behavior)#

상자#

  • 컨테이너가 테두리 · 면 · 포커스를 하나만 그립니다. 높이는 컨트롤 크기 사다리의 lg 행(48), 모서리 12, 좌우 패딩 14, 칸 사이 간격 4 로 TextField 와 한 줄에 놓입니다.
  • 안쪽 칸은 TextField 의 plain 변형을 lg 행으로 합성합니다 — 자기 테두리와 면이 없어 상자 안에 상자가 생기지 않습니다. 구분자는 onSurfaceSubtle 이고 좌우 패딩은 2 입니다.
  • 칸 폭의 상한은 글자 수 구간별로 28 · 28 · 48 입니다(shortFieldMaxWidth · mediumFieldMaxWidth · longFieldMaxWidth).

인터랙션#

  • 입력: 각 필드 입력 시 onChanged에 전체 값 맵 전달
  • 포커스: 컨테이너가 포커스되면 테두리 색이 primary 로 강조됨
  • 호버: 쉬는 테두리는 outlineVariant 이고, 포인터가 컨테이너(필드 위 포함)에 올라가면 outline 으로 한 단계 진해집니다(hoverBorderColor). 우선순위는 disabled > focus > hover > rest — 포커스된 컨테이너는 포커스 테두리를 그대로 두고, 비활성 컨테이너에는 호버 단계가 없습니다
  • enabled가 false이면 비활성 — 불투명도가 아니라 면 disabledContainer, 테두리 disabledOutline, 글자와 구분자 disabledContent 토큰으로 그립니다. 포커스를 쥔 채 비활성이 된 컨테이너도 포커스 테두리를 그리지 않습니다

사용 가이드라인 (Usage Guidelines)#

구간별 키와 값 맵이 필요하면 ObjectInput을 사용합니다. 한 문자열에 마스크를 적용하려면 FormattedInput, 독립적인 한 입력값은 TextField를 선택합니다.

✅ Do#

pattern으로 필드별 허용 문자를 제한하기

ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4, pattern: r'\d'),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2, pattern: r'\d'),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2, pattern: r'\d'),
  ],
  separator: '/',
  onChanged: handleDateChanged,
)

ObjectInput은 key로 필드 종류를 추론하지 않습니다. pattern이 없으면 어떤 문자든 maxLength까지 입력되고, 매칭 실패로 거부되는 문자는 양 플랫폼 모두 스크린 리더에 알려지지 않으므로 애초에 잘못된 문자를 못 치게 막는 편이 안전합니다.


❌ Don't#

필드 묶음이 하나의 의미 있는 입력으로 안내될 거라 기대하지 않기

// ❌ "생년월일" 같은 그룹 의미가 스크린 리더에 저절로 전달될 거라 기대
ObjectInput(
  fields: const [
    CoreObjectInputField(key: 'year', placeholder: 'YYYY', maxLength: 4),
    CoreObjectInputField(key: 'month', placeholder: 'MM', maxLength: 2),
    CoreObjectInputField(key: 'day', placeholder: 'DD', maxLength: 2),
  ],
  separator: '/',
  onChanged: handleDateChanged,
)

ObjectInput은 role도 접근 가능한 이름도, 필드 개수·순서 안내도, 자동 이동/Backspace 복귀도 제공하지 않습니다 — 각 필드는 서로 무관한 입력 세 개로 읽힙니다. 그룹 이름과 안내가 필요하면 소비자가 직접 부여해야 합니다.

접근성 (Accessibility)#

역할#

ObjectInput 자체는 role 도 그룹 semantics 도 내보내지 않습니다. Flutter 구현에는 Semantics 호출이 하나도 없고 루트는 Focus > Container > Row 이며, Web 루트는 호출자 passthrough 속성만 실은 맨 <div> 로 role / aria-* / <fieldset> / <legend> / <label> 어느 것도 붙지 않습니다. 필드별 wrapper 도 평범한 <div>, 구분자도 평범한 <span>(Web) / Text(Flutter) 입니다.

실제 semantics 는 전부 합성된 TextField 각각에서 나옵니다 — Web 은 native <input>, Flutter 는 TextField/EditableText 의 기본 textField semantics. role 이나 aria 를 얹으려면 Web 은 attributes: passthrough 로 직접 주입해야 하고, Flutter 에는 대응하는 통로가 없어 소비자가 바깥에서 Semantics 로 감싸야 합니다.

키보드#

ObjectInput 이 직접 처리하는 키가 없습니다. 타이핑·캐럿·선택은 모두 각 TextField 의 몫이고, 컴포넌트 차원의 maxLength 자동 이동, Backspace 로 이전 필드 복귀, 화살표 키 필드 이동은 어느 플랫폼에도 없습니다. Web 의 onKeyDown / onKeyUp 은 호출자 passthrough 프로퍼티일 뿐 소비자가 넘기지 않으면 아무 동작도 하지 않습니다.

포커스 관리#

포커스 링에 해당하는 시각 표현은 테두리 색 변경 하나이며, 그 상태는 보조기술에 노출되지 않습니다.

  • Flutter: Focus(onFocusChange:) 가 행 전체를 감싸고 그 결과로 테두리 색만 고릅니다. focusNode / canRequestFocus: false / skipTraversal: true 를 넘기지 않아 이 내부 노드는 Flutter 기본값(포커스 가능 + traversal 대상)으로 만들어지므로, 컨테이너가 필드들보다 앞선 탭 정지점 하나를 더 차지합니다.
  • Web: 루트 <div> 에 tabindex 가 없어 포커스 대상이 아닙니다. 테두리 색을 위해 focusin / focusout 만 듣고, 그 리스너는 enabled == false 일 때 아예 등록되지 않습니다.

포커스 트랩, 포커스 복원, autofocus 는 양쪽 모두 없습니다.

스크린 리더#

합성된 TextField 각각이 자기 hintText/placeholder 와 native input semantics 를 안내하고, 그 사이의 구분자 문자가 텍스트 노드로 함께 읽힙니다. ObjectInput 은 여기에 그룹 이름도, 필드 개수·위치("3개 중 2번째")도, 필드 간 관계도 더하지 않으며, 포커스 링이 바뀌어도 아무것도 안내되지 않습니다.

알려진 제약#

  • 필드 묶음에 role 도 접근 가능한 이름도 없습니다 — 연/월/일 세 필드가 서로 무관한 입력 세 개로 읽힙니다. 하나의 입력으로 인지시키려면 소비자가 직접 이름과 그룹을 부여해야 합니다.
  • 다중 파트 입력의 관례적 키보드 모델이 없습니다 — 자동 이동, Backspace 복귀, 화살표 이동 중 어느 것도 구현되어 있지 않습니다.
  • 구분자 텍스트가 리더에서 감춰지지 않습니다 — aria-hidden 이 붙지 않아 필드 사이에서 그대로 읽힙니다.
  • traversal 이 플랫폼마다 다릅니다 — Flutter 의 Focus 가 만드는 컨테이너 탭 정지점이 Web 에는 없어 탭 정지점 개수가 어긋납니다.
  • Web 은 enabled: false 일 때 자체 포커스 추적을 중단합니다.
  • pattern / maxLength 로 입력이 거부되어도 리더에 알려지지 않습니다 — 양 플랫폼 공통입니다.

포커스 링 색 대비 등 전역 축은 전역 접근성 축 을 참고하세요.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명ObjectInputObjectInput
필드 TextField (plain · lg, 자기 테두리와 면 없음) TextField (plain · lg, 자기 테두리와 면 없음)
포커스 링 Focus + 테두리 색 변경 focus / blur 이벤트
호버 테두리 MouseRegion 이 호버를 감지하고 resolver 가 만든 호버 컨테이너 스타일을 고름 토큰이면 hover:border-* 클래스(포커스 중에는 클래스를 내지 않음), 토큰이 아닌 색은 :hover scoped 규칙