Groo Design

Input OTP

메일 · 문자로 받은 일회용 인증 코드(지금은 숫자 6자리)를 넣는 칸 — Input 한 칸이다. 칸을 자릿수만큼 나누지 않는다. 칸 아래 다시 받기 단추가 남은 초를 보이고, 설명 줄이 유효 시간 · 틀릴 수 있는 횟수를 늘 알린다.

SEED 에는 이 컴포넌트가 없다 — Text Input 문서가 "주민등록번호나 전화번호처럼 입력 형식이 정해진 경우, Input을 나누지 말고 값에 포맷을 자동으로 적용해서 추가 인터랙션 없이 한 번에 입력할 수 있도록 해주세요." 라고 적었고, one-time-code · inputmode · 붙여넣기 · 다시 받기 규칙은 출처에 없다(당근 SEED, Apache-2.0). groo 는 코드 칸을 Input 한 칸으로 두고(2026-10-09 사용자 결정) 숫자만 뽑기 · 자동 채우기 · 다시 받기 · 설명 줄을 정했다. 파일 이름(input-otp)은 그대로 두고, 옛 스펙의 칸 6 × 40(숨은 입력 하나)과 DESIGN.md v69 의 "칸마다 입력" 을 대신한다.

수치 원본은 input-otp.yaml이고, 칸의 상자 · 글자 · 상태는 input.yaml, 라벨 · 설명 · 오류는 field.yaml이 원본이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.

9:41
설정
계정
이용 해지

가입한 메일로 받은 인증 코드를 입력해주세요.

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
desk.groo.app
이용 해지

가입한 메일로 받은 인증 코드를 입력해주세요.

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
9:41
설정
계정
이용 해지

가입한 메일로 받은 인증 코드를 입력해주세요.

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
desk.groo.app
이용 해지

가입한 메일로 받은 인증 코드를 입력해주세요.

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
이용 해지 > 본인 확인 — 인증 코드 칸 · 설명 · 다시 받기(52초), 라이트 · 다크

직접 골라 보기

크기(large · medium) · 상태(포커스 · 오류 · 막힘) · 다시 받기의 때(처음 · 남은 초 · 다시 받기)를 고르면 스펙대로 그린 칸과 그 코드가 바뀐다. 칸에 "123 456" · "인증 코드: 123456" 을 붙여 보면 숫자만 남고, 시계를 빨리 돌리면 다시 받기가 풀린다.

10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
붙여 보기 — 칸에 붙인 것처럼 숫자만 뽑는다
진짜 클립보드로 붙여도 같다 — maxLength 로 먼저 자르지 않는다
크기 size
상태
다시 받기의 때
시계
모드
import { Field } from "@/components/ui/field"
import { InputOTP, InputOTPResend } from "@/components/ui/input-otp"

<Field label="인증 코드" description="10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.">
  <InputOTP id="withdraw-code" size="large" value={code} onValueChange={setCode} />
</Field>
{/* 보낸 때 — 60초 안이면 "다시 받기(남은 초)" + 막힘 */}
<InputOTPResend className="mt-x3" sentAt={sentAt} codeInputId="withdraw-code" onResend={sendCode} />

Anatomy

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
ⓐ Label — "인증 코드"ⓑ Field — Input 한 칸(large 52)ⓒ Description — 늘 보이는 설명 줄ⓓ Resend — 칸 아래 12 · 보낸 뒤 60초 막힘
라벨 · 코드 칸 · 설명 줄 · 다시 받기 단추
부위설명
ⓐ Label칸 이름 — Field 의 라벨 "인증 코드".
ⓑ Field코드 칸 — Input 상자형 한 칸, 숫자만.
ⓒ Description설명 줄 — "10분 안에 입력해주세요 · 5번 틀리면 다시 받아요." 늘 보인다. 오류가 나면 오류 글이 대신한다.
ⓓ Resend다시 받기 — 칸 아래 왼쪽, 보낸 뒤 60초 동안 남은 초를 붙이고 막힌다.

메일로 보내는 코드는 예외로 다시 받기 아래에 스팸함 안내 한 줄을 둘 수 있다 — 아래 "설명 줄".

부위

부위설명
field코드 칸 — Input 상자형 한 칸. 이름은 Field 라벨("인증 코드")
value입력 — 숫자만, 최대 6자리
placeholder빈 칸의 글 — "6자리 숫자"
description설명 — Field 꼬리, 유효 시간 · 틀릴 수 있는 횟수
resend다시 받기 — Button neutralWeak medium, 칸 아래. 남은 초를 붙이고 그동안 막힌다

Properties

한 칸

코드 칸은 Input 상자형 한 칸이다 — large 52(폰 · 앱) · medium 40(1280 이상 데스크톱 웹), 웹의 기본은 responsive. 상자 · 글자 · 포커스 · 오류 · 막힘은 Input 그대로이고(막힌 칸의 자리 글만 다르다 — 아래 "SEED 와 다른 점"), 숫자는 고정폭 숫자(tabular-nums)로 다른 칸처럼 왼쪽에 쓴다 — 고정폭 글꼴 · 글자 사이 띄움 · 가운데 맞춤을 두지 않는다. 자릿수는 placeholder("6자리 숫자")가 알린다.

123456
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
large 52폰 · 앱 — 모서리 12 · 좌우 16
123456
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
medium 401280 이상 데스크톱 웹 — 모서리 8 · 좌우 14
크기 — large 52(폰 · 앱) · medium 40(1280 이상 데스크톱 웹)

크기

sizefield 최소 높이field 좌우 여백field 모서리field 바뀌는 폭값 글자설명
large52pxspacing-x4 16pxradius-r3 12px—text-t5 16px / 400 / 22px52 — 폰 · 앱
medium40pxspacing-x3_5 14pxradius-r2 8px—text-t4 14px / 400 / 19px40 — 1280 이상 데스크톱 웹(마우스)
responsive (기본)———1280px — 미만은 large, 이상은 medium 의 값(Input) — 앱은 늘 large—폭에 따라 — 1280 미만 large · 이상 medium. 웹의 기본

칸

속성값비고
모양outlineInput 상자형(input.yaml) — 크기 · 모서리 · 테두리 · 포커스 · 오류 · 막힘은 Input 그대로. 칸을 자릿수만큼 나누지 않는다(사용자 결정 10A — 6칸 · 6칸 모양은 고르지 않았다)
너비놓인 자리 폭Field 의 입력 자리를 채운다
배경transparent
테두리color-stroke-neutral-weak #E5E8EF · 다크 #404757안쪽 1px(Input)

숫자만 받는다

치기 · 붙여넣기 · 자동 채우기 모두 글에서 숫자만 뽑아 앞 6자리를 쓴다 — "123 456" · "123-456" · " 123456" · "인증 코드: 123456" 이 모두 123456 이 된다. 붙인 글의 숫자가 6자리 이상이면 칸의 값을 그 코드로 바꾸고(칸에 "12" 가 있어도 "인증 코드: 654321" → 654321), 그보다 짧으면 커서 자리에 넣고 숫자만 남긴다. 전각 숫자(123)는 보통 숫자로 바꾼다(NFKC). maxLength 를 걸지 않는다 — 브라우저가 먼저 자르면 붙인 글의 숫자를 잃는다(지금 웹 "123 456" → "12345"). 숫자 키보드(inputmode="numeric")를 띄우고, 받은 코드를 키보드 위에 제안하도록 autocomplete="one-time-code"(앱 AutofillHints.oneTimeCode)를 둔다. 코드가 메일로 오면 OS 가 제안하지 않을 수 있어 붙여넣기가 늘 되는 길이다.

붙인 글“123 456”
123456
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
붙인 글“인증 코드: 123456”
123456
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
이렇게 — 치기 · 붙여넣기 · 자동 채우기 모두 숫자만 뽑아 앞 6자리 — 메일의 띄어 쓴 코드 · 앞에 붙은 글도 그대로 붙는다
붙인 글“123 456”
12345
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
붙인 글“인증 코드: 123456”
6자리 숫자
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
이렇게 하지 않는다 — maxLength 로 먼저 자르기 — 브라우저가 붙인 글을 6글자로 자른 뒤에 숫자만 남겨 '12345' · 빈 칸이 된다(지금 웹)
붙여넣기 — "123 456" · "인증 코드: 123456" 이 123456 으로

입력

속성값비고
값 글자색color-fg-neutral #1A1F2E · 다크 #F5F6FA
숫자 폭tabular-nums숫자 폭을 고르게 — 고정폭 글꼴(mono) · 글자 사이 띄움은 쓰지 않는다
글 맞춤start다른 입력칸과 같다
키보드 종류numeric숫자 키보드 — type="number" 는 쓰지 않는다(Input)
자동 채우기one-time-code웹 autocomplete — iOS · 안드로이드가 받은 코드를 키보드 위에 제안한다. 앱은 AutofillHints.oneTimeCode
받는 글자숫자만 · 앞 6자리치기 · 붙여넣기 · 자동 채우기 모두 — 글에서 숫자만 뽑아 앞 6자리를 쓴다("123 456" · "123-456" · "인증 코드: 123456" → 123456). maxLength 를 걸지 않는다 — 브라우저가 먼저 잘라 숫자를 잃는다
저절로 보내기없음6자리를 채워도 보내지 않는다 — 확인 단추를 누른다(고칠 틈을 둔다)

다시 받기

칸 아래 12 에 Button neutralWeak · medium(40)을 왼쪽에 둔다. 보내기 전에는 "코드 받기", 보낸 뒤 60초 동안은 "다시 받기(52초)" 처럼 남은 초를 붙이고 막는다(전용 색 — 흐리게 하지 않는다), 60초가 지나면 "다시 받기" 다(사용자 결정 11B — 서버의 다시 받기 60초). 칸 옆에 두지 않는다 — 칸(52 · 40)과 단추 높이가 맞지 않는다. 코드를 보내면 초점을 칸으로 옮긴다 — 막히는 단추에 초점이 남아 본문으로 빠지지 않게.

코드 받기처음 — 보내기 전
다시 받기(52초)보낸 뒤 60초 동안 — 남은 초 · 막힘
다시 받기60초가 지나면
다시 받기 — 코드 받기 · 다시 받기(52초) 막힘 · 다시 받기

다시 받기의 때

resendresend 배경resend 글자·아이콘resend 커서단추 글설명
first———코드 받기 — 처음 — 보내기 전아직 안 보냄 — "코드 받기"
ready (기본)———다시 받기보낸 지 60초가 지남 — "다시 받기"
cooldowncolor-bg-disabled #F5F6FA · 다크 #353B4D — Button 막힘 — 흐리게 하지 않는다(v106)color-fg-disabled #8A91A0 · 다크 #838997not-allowed다시 받기({n}초) — 남은 초를 괄호에 붙인다(앞 글자에 붙여 쓴다 — International Design). 0 이 되면 "다시 받기"보낸 지 60초 안 — "다시 받기(52초)", 막힘

다시 받기

속성값비고
모양neutralWeakButton — 옅은 회색 채움
높이40pxButton medium — 칸 옆이 아니라 아래에 둬 칸(52 · 40)과 높이를 맞출 일이 없다(지금 웹은 칸 40 옆 36)
위 바깥 여백spacing-x3 12pxField 꼬리(설명 · 오류) ↔ 단추 12. 왼쪽 맞춤
막는 시간60초보낸 뒤 60초 동안 막는다(서버 COOLDOWN 60) — 남은 초를 1초마다 줄여 보인다
초점보내면 칸으로단추가 막히며 초점이 본문으로 빠지지 않게 — 코드를 보내면 초점을 코드 칸으로 옮긴다

설명 줄

설명은 늘 "10분 안에 입력해주세요 · 5번 틀리면 다시 받아요." 다 — 서버 규칙(유효 10분 · 5번 틀리면 다시 받기)이 메일에만 있고 화면에는 없던 것을 칸 아래에 둔다(사용자 결정 11B). 남은 시간을 시계로 세지 않는다. 서버 규칙이 바뀌면 이 글도 바꾼다. 틀렸을 때는 Field 의 규칙대로 오류 글이 이 줄을 대신하고, 칸을 고치기 시작하면 돌아온다(사용자 결정 2026-10-09 18A — 오류 글과 설명 줄을 함께 보이는 예외는 고르지 않았다).

메일로 보내는 코드는 [코드 받기] 아래에 스팸함 안내 한 줄("메일함에 코드가 안 보이면 스팸함도 확인해주세요.")을 둘 수 있다 — 설명 줄과 같은 글자 · 색이고, 코드를 보낸 뒤에만 보인다. 설명 줄은 하나라 유효 시간 · 횟수 글이 차지하기 때문이다(2026-10-24 사용자 결정 — 05-2 Q1 A).

설명

속성값비고
내용10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.늘 보인다(사용자 결정 11B) — 서버 규칙(유효 600초 · 틀림 5번 · 다시 받기 60초 — porest-sso-back ReauthServiceImpl)이 바뀌면 글도 바꾼다. 오류가 나면 Field 규칙대로 오류 글이 이 자리를 대신하고, 칸을 고치면 돌아온다
글자text-t4 14px / 400 / 19pxField 설명 그대로
글자·아이콘color-fg-neutral-subtle #62697A · 다크 #A2A8B7

State

상태모습
enabled투명 바탕 · 안쪽 1px stroke-neutral-weak(Input)
focused안쪽 2px stroke-neutral-contrast — 마우스 · 터치로 눌러도
invalid안쪽 2px stroke-critical-solid · 오류 글이 설명 줄을 대신한다(Field)
disabled바탕 bg-disabled · 글자 fg-disabled — 흐리게 하지 않는다
기본
6자리 숫자
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
포커스
4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
오류
4821
코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요.
막힘
4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
라이트포커스 · 오류는 안쪽 2px(내용이 밀리지 않는다) · 오류 글이 설명 줄을 대신한다 · 막힘은 전용 색
기본
6자리 숫자
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
포커스
4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
오류
4821
코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요.
막힘
4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
다크포커스 · 오류는 안쪽 2px(내용이 밀리지 않는다) · 오류 글이 설명 줄을 대신한다 · 막힘은 전용 색
상태 — 기본 · 포커스 · 오류 · 막힘

상태

상태field 배경field 테두리값 글자색placeholder 색description 글자·아이콘description 내용
enabled 기본transparentcolor-stroke-neutral-weak #E5E8EF · 다크 #404757 — 안쪽 1px(Input)color-fg-neutral #1A1F2E · 다크 #F5F6FAcolor-fg-placeholder #767C8B · 다크 #838997color-fg-neutral-subtle #62697A · 다크 #A2A8B710분 안에 입력해주세요 · 5번 틀리면 다시 받아요. — 늘 보인다(사용자 결정 11B) — 서버 규칙(유효 600초 · 틀림 5번 · 다시 받기 60초 — porest-sso-back ReauthServiceImpl)이 바뀌면 글도 바꾼다. 오류가 나면 Field 규칙대로 오류 글이 이 자리를 대신하고, 칸을 고치면 돌아온다
focused 칸에 포커스 — 마우스 · 터치로 눌러도(Input)transparentcolor-stroke-neutral-contrast #1A1F2E · 다크 #F5F6FA — 안쪽 2px(Input) — 내용이 밀리지 않는다color-fg-neutral #1A1F2E · 다크 #F5F6FAcolor-fg-placeholder #767C8B · 다크 #838997color-fg-neutral-subtle #62697A · 다크 #A2A8B710분 안에 입력해주세요 · 5번 틀리면 다시 받아요. — 늘 보인다(사용자 결정 11B) — 서버 규칙(유효 600초 · 틀림 5번 · 다시 받기 60초 — porest-sso-back ReauthServiceImpl)이 바뀌면 글도 바꾼다. 오류가 나면 Field 규칙대로 오류 글이 이 자리를 대신하고, 칸을 고치면 돌아온다
invalid 오류 — Field 의 invalid(aria-invalid). 오류 글이 설명 자리를 대신한다transparentcolor-stroke-critical-solid #D72323 · 다크 #FF8477 — 안쪽 2px(Input) — 포커스해도 그대로color-fg-neutral #1A1F2E · 다크 #F5F6FAcolor-fg-placeholder #767C8B · 다크 #838997color-fg-critical #D72323 · 다크 #FF8477코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요. — Field 오류(14 · fg-critical + 아이콘 16)가 설명을 대신한다(18A) — 칸을 고치기 시작하면 설명 줄이 돌아온다
disabled 비활성 — 값을 바꿀 수 없다color-bg-disabled #F5F6FA · 다크 #353B4D — Input 그대로 — 흐리게 하지 않는다(v106)color-stroke-neutral-weak #E5E8EF · 다크 #404757 — 안쪽 1px(Input)color-fg-disabled #8A91A0 · 다크 #838997color-fg-placeholder #767C8B · 다크 #838997 — 막혀도 자리 글은 그대로 — Input · Textarea · SEED text-input(fg-disabled)과 다른 자리다. 막힌 칸은 바탕(bg-disabled) · 커서로 갈린다(2026-10-17 사용자 결정 01T-013 — 추천 fg-disabled 는 고르지 않았다)color-fg-neutral-subtle #62697A · 다크 #A2A8B710분 안에 입력해주세요 · 5번 틀리면 다시 받아요. — 늘 보인다(사용자 결정 11B) — 서버 규칙(유효 600초 · 틀림 5번 · 다시 받기 60초 — porest-sso-back ReauthServiceImpl)이 바뀌면 글도 바꾼다. 오류가 나면 Field 규칙대로 오류 글이 이 자리를 대신하고, 칸을 고치면 돌아온다

Guidelines

칸을 나누지 않는다

코드를 자릿수만큼 칸으로 나누지 않는다 — 한 칸이라 붙여넣기 · 지우기 · 고치기가 보통 글 칸처럼 되고, 칸 크기 · 포커스 · 막힘 규칙을 따로 지킬 일이 없다(사용자 결정 10A — SEED Text Input 의 "Input을 나누지 말고"). 6칸 모양을 한 칸 위에 그리지도 않는다.

4821
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
이렇게 — 한 칸 — 붙여넣기 · 지우기 · 고치기가 보통 글 칸처럼 되고, 크기 · 포커스 · 막힘 규칙을 따로 지킬 일이 없다
인증 코드
4821
이렇게 하지 않는다 — 자릿수만큼 나눈 칸 · 한 칸 위에 그린 6칸 모양 — 지우기 · 고치기가 칸마다 끊기고 붙여넣기가 한 칸에만 들어간다
한 칸 — 붙여넣기 · 지우기가 글 칸처럼 · 6칸으로 나눈 칸(쓰지 않는다)

저절로 보내지 않는다

6자리를 채워도 바로 확인하지 않는다 — 확인 단추를 누른다. 확인 단추는 켜 두고, 6자리가 안 되면 누를 때 오류("인증 코드 6자리를 입력해주세요.")로 알린다(Field 의 "제출과 검증").

오류는 칸 아래

틀린 코드 · 지난 코드는 Field 의 오류로 칸 아래에 알린다(aria-invalid · aria-describedby · 알림 — 기초 Inclusive Design). 서버가 보낸 글을 그대로 보이지 않고 화면 글로 바꾼다 — 무엇이 안 됐는지와 할 일("코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요."). 다시 받기는 60초 동안 막혀 있으므로 "코드를 방금 보냈어요" 오류는 생기지 않는다.

482193
코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요.
이렇게 — 칸 2px 빨강 · 오류 글이 설명 줄을 대신한다(무엇이 안 됐는지와 할 일) · 다시 받기는 그대로
482193
10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.
코드를 방금 보냈어요. 잠시 뒤에 다시 받아 주세요
이렇게 하지 않는다 — 서버가 보낸 글을 그대로 칸 아래 빨갛게 — 칸에는 오류 표시가 없고, 설명 줄과 겹쳐 무엇을 할지 모른다(지금 웹)
오류 — 칸 2px 빨강 · 오류 글이 설명을 대신 · 다시 받기는 그대로

글

라벨은 "인증 코드" — 띄어 쓴 한 꼴로(서버 글 "인증코드" 도 화면에서는 "인증 코드"). placeholder 는 "6자리 숫자", 단추는 "코드 받기" · "다시 받기" · "다시 받기(52초)" — 괄호는 앞 글자에 붙인다(International Design). 보냈다는 알림은 Snackbar "메일로 코드를 보냈어요." 다.

코드

레시피 recipes/shadcn/components/ui/input-otp.tsx 를 Field 안에 둔다 — 칸은 레시피 input.tsx(Input) 위에 짠다. 아래 미리보기는 스펙 값으로 그린 모습이다.

  • InputOTP — 코드 칸. Input 의 속성(size · disabled · id · placeholder …)에 value · defaultValue(숫자 글) · onValueChange(value)(숫자만 · 최대 length 자 — 붙여넣기도 이 값으로) · length(기본 6)를 받는다. inputMode="numeric" · autoComplete="one-time-code" · placeholder="6자리 숫자"(기본) · 고정폭 숫자를 스스로 건다 — type · maxLength · inputMode · autoComplete · onChange 는 받지 않는다. Field 안이면 라벨 · 설명 · 오류 · 막힘을 Input 처럼 받는다.
  • InputOTPResend — 다시 받기 단추(Button neutralWeak medium). sentAt(마지막으로 보낸 때 — Date.now() 의 수, 아직 안 보냈으면 null) · onResend() · cooldownSeconds(기본 60) · codeInputId(보낸 뒤 초점을 옮길 코드 칸의 id) · className. null 이면 "코드 받기", 보낸 뒤 cooldownSeconds 안이면 "다시 받기({n}초)" + 막힘, 지나면 "다시 받기" — 남은 초는 단추가 스스로 센다. 누르는 즉시 초점이 코드 칸으로 가고, onResend 가 Promise 를 돌려주면 끝날 때까지 다시 누를 수 없다. Button 의 속성(disabled …)을 받는다.

본인 확인 — 메일로 코드 받기

본인 확인

가입한 메일로 받은 인증 코드를 입력해주세요.

10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.

미리보기의 맞는 코드는 123456 — 6자리를 채워도 저절로 보내지 않아요.

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Field } from "@/components/ui/field"
import { InputOTP, InputOTPResend } from "@/components/ui/input-otp"

const [code, setCode] = useState("")
const [sentAt, setSentAt] = useState<number | null>(null)
const [error, setError] = useState<string | null>(null)

<Field label="인증 코드" description="10분 안에 입력해주세요 · 5번 틀리면 다시 받아요." invalid={error !== null} errorMessage={error}>
  <InputOTP
    id="withdraw-code"
    value={code}
    onValueChange={(value) => {
      setCode(value)
      setError(null) // 고치기 시작하면 설명 줄로 돌아온다
    }}
  />
</Field>
<InputOTPResend className="mt-x3" sentAt={sentAt} codeInputId="withdraw-code" onResend={() => sendCode().then(() => setSentAt(Date.now()))} />
<Button size="large" onClick={confirm}>확인</Button>

Behavior

인터랙션동작
치기숫자만 들어간다 — 6자리에서 멈춘다
붙여넣기 · 자동 채우기붙인 글에서 숫자만 뽑아 앞 6자리 — "인증 코드: 123456" → 123456
6자리를 채움아무것도 보내지 않는다 — 확인 단추를 누른다
"코드 받기" · "다시 받기"코드를 보내고 초점을 칸으로 옮긴다. 단추는 60초 동안 "다시 받기({n}초)" 로 막힌다 · 보냈다고 Snackbar
남은 초1초마다 줄고, 0 이 되면 "다시 받기" 로 풀린다
확인 — 6자리가 안 됨Field 오류 "인증 코드 6자리를 입력해주세요." · 초점은 칸으로
확인 — 틀림 · 지남Field 오류(칸 2px 빨강 · 오류 글이 설명 줄을 대신) · 화면 읽기 프로그램에 한 번 알린다
칸 고치기오류를 지우고 설명 줄로 돌아온다
Disabled포커스 · 입력 불가(Input)

Accessibility

기준검증
WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1)Input · Field 의 검증 그대로 — 값 fg-neutral 16.41 · 13.42, 설명 fg-neutral-subtle 5.50 · 6.09(시트 다크 5.27), 오류 fg-critical 5.06 · 6.08 ✓. placeholder fg-placeholder 4.18 · 4.13(시트 다크 3.58) — 4.5:1 밑, 사용자가 대가를 알고 SEED 값을 골랐다(2026-10-16 글자 역할 D7, DESIGN v119). 다시 받기 글 fg-neutral · bg-neutral-weak 위 15.20 · 10.32 ✓ — 막힌 동안 fg-disabled 는 기준 밖
WCAG 1.3.5 Identify input purposeautocomplete="one-time-code" ✓
WCAG 3.3.1 Error identification틀린 코드를 글로 알리고 칸에 aria-invalid ✓
WCAG 2.2.1 Timing adjustable유효 시간(10분)을 설명 줄로 미리 알리고, 지나면 다시 받을 수 있다 ✓
WCAG 2.4.3 Focus order보낸 뒤 초점을 칸으로 — 막힌 단추에 남지 않는다 ✓
WCAG 4.1.3 Status messages오류는 Field 의 알림 자리(polite), 보냈다는 알림은 Snackbar(role="status") ✓
WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24)칸 52 · 40 · 단추 40 ✓
WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44)칸 large 52 ✓ · medium 40 ⚠(1280 이상 데스크톱 웹만 — Input 과 같다). 단추는 누르는 영역 44 ✓
ARIA<input type="text" inputmode="numeric" autocomplete="one-time-code"> 하나 — 이름은 Field 라벨(<label for>), 설명 · 오류는 aria-describedby. 다시 받기의 남은 초는 단추 이름에 들어 있어 초점이 오면 읽힌다(1초마다 알리지 않는다)

Do / Don't

✅ Do

  • Input 한 칸으로 받는다 — 라벨 "인증 코드" · placeholder "6자리 숫자".
  • 붙여넣기 · 자동 채우기에서 숫자만 뽑는다 · one-time-code · 숫자 키보드.
  • 다시 받기 단추에 남은 초를 붙이고 그동안 막는다 — 보내면 초점을 칸으로.
  • 유효 시간 · 틀릴 수 있는 횟수를 설명 줄에 늘 둔다.
  • 오류는 Field 로 칸 아래에, 화면 글로.

❌ Don't

  • 칸을 자릿수만큼 나누기 · 6칸 모양을 그리기.
  • maxLength 로 먼저 자르기.
  • 6자리를 채우면 저절로 확인하기.
  • 10:00 시계로 남은 시간을 세기 · 아무 표시 없이 두기.
  • 서버가 보낸 글을 그대로 보이기.
  • 칸 옆에 높이가 다른 단추 두기.

Specification

input-otp.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 인증 코드 칸을 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다. 칸의 나머지 값은 input.yaml, 라벨 · 설명 · 오류는 field.yaml 이다.

Base

부위상태속성값
fieldenabled너비width놓인 자리 폭Field 의 입력 자리를 채운다
배경backgroundtransparent
테두리borderColorstroke-neutral-weak#E5E8EF다크 #404757안쪽 1px(Input)
모양variantoutlineInput 상자형(input.yaml) — 크기 · 모서리 · 테두리 · 포커스 · 오류 · 막힘은 Input 그대로. 칸을 자릿수만큼 나누지 않는다(사용자 결정 10A — 6칸 · 6칸 모양은 고르지 않았다)
valueenabled값 글자색foregroundfg-neutral#1A1F2E다크 #F5F6FA
글 맞춤textAlignstart다른 입력칸과 같다
숫자 폭numeralstabular-nums숫자 폭을 고르게 — 고정폭 글꼴(mono) · 글자 사이 띄움은 쓰지 않는다
키보드 종류inputModenumeric숫자 키보드 — type="number" 는 쓰지 않는다(Input)
자동 채우기autoCompleteone-time-code웹 autocomplete — iOS · 안드로이드가 받은 코드를 키보드 위에 제안한다. 앱은 AutofillHints.oneTimeCode
받는 글자filter숫자만 · 앞 6자리치기 · 붙여넣기 · 자동 채우기 모두 — 글에서 숫자만 뽑아 앞 6자리를 쓴다("123 456" · "123-456" · "인증 코드: 123456" → 123456). maxLength 를 걸지 않는다 — 브라우저가 먼저 잘라 숫자를 잃는다
저절로 보내기autoSubmit없음6자리를 채워도 보내지 않는다 — 확인 단추를 누른다(고칠 틈을 둔다)
placeholderenabledplaceholder 색foregroundfg-placeholder#767C8B다크 #838997
내용content6자리 숫자자릿수를 알린다 — 보내는 코드가 바뀌면 같이
descriptionenabled글자·아이콘foregroundfg-neutral-subtle#62697A다크 #A2A8B7
글자typographytext-t414px / 19px / 400Field 설명 그대로
내용content10분 안에 입력해주세요 · 5번 틀리면 다시 받아요.늘 보인다(사용자 결정 11B) — 서버 규칙(유효 600초 · 틀림 5번 · 다시 받기 60초 — porest-sso-back ReauthServiceImpl)이 바뀌면 글도 바꾼다. 오류가 나면 Field 규칙대로 오류 글이 이 자리를 대신하고, 칸을 고치면 돌아온다
resendenabled높이height40pxButton medium — 칸 옆이 아니라 아래에 둬 칸(52 · 40)과 높이를 맞출 일이 없다(지금 웹은 칸 40 옆 36)
위 바깥 여백marginTopspacing-x312pxField 꼬리(설명 · 오류) ↔ 단추 12. 왼쪽 맞춤
모양variantneutralWeakButton — 옅은 회색 채움
막는 시간cooldown60초보낸 뒤 60초 동안 막는다(서버 COOLDOWN 60) — 남은 초를 1초마다 줄여 보인다
초점focus보내면 칸으로단추가 막히며 초점이 본문으로 빠지지 않게 — 코드를 보내면 초점을 코드 칸으로 옮긴다
fieldfocused테두리borderColorstroke-neutral-contrast#1A1F2E다크 #F5F6FA안쪽 2px(Input) — 내용이 밀리지 않는다
fieldinvalid테두리borderColorstroke-critical-solid#D72323다크 #FF8477안쪽 2px(Input) — 포커스해도 그대로
descriptioninvalid글자·아이콘foregroundfg-critical#D72323다크 #FF8477
내용content코드가 맞지 않거나 시간이 지났어요. 확인하거나 다시 받아주세요.Field 오류(14 · fg-critical + 아이콘 16)가 설명을 대신한다(18A) — 칸을 고치기 시작하면 설명 줄이 돌아온다
fielddisabled배경backgroundbg-disabled#F5F6FA다크 #353B4DInput 그대로 — 흐리게 하지 않는다(v106)
valuedisabled값 글자색foregroundfg-disabled#8A91A0다크 #838997
placeholderdisabledplaceholder 색foregroundfg-placeholder#767C8B다크 #838997막혀도 자리 글은 그대로 — Input · Textarea · SEED text-input(fg-disabled)과 다른 자리다. 막힌 칸은 바탕(bg-disabled) · 커서로 갈린다(2026-10-17 사용자 결정 01T-013 — 추천 fg-disabled 는 고르지 않았다)

size=large

부위상태속성값
fieldenabled최소 높이minHeight52px
좌우 여백paddingXspacing-x416px
모서리radiusradius-r312px
valueenabled값 글자typographytext-t516px / 22px / 400

size=medium

부위상태속성값
fieldenabled최소 높이minHeight40px
좌우 여백paddingXspacing-x3_514px
모서리radiusradius-r28px
valueenabled값 글자typographytext-t414px / 19px / 400

size=responsive

부위상태속성값
fieldenabled바뀌는 폭breakpoint1280px미만은 large, 이상은 medium 의 값(Input) — 앱은 늘 large

resend=first

부위상태속성값
resendenabled단추 글label코드 받기처음 — 보내기 전

resend=ready

부위상태속성값
resendenabled단추 글label다시 받기

resend=cooldown

부위상태속성값
resendenabled배경backgroundbg-disabled#F5F6FA다크 #353B4DButton 막힘 — 흐리게 하지 않는다(v106)
글자·아이콘foregroundfg-disabled#8A91A0다크 #838997
커서cursornot-allowed
단추 글label다시 받기({n}초)남은 초를 괄호에 붙인다(앞 글자에 붙여 쓴다 — International Design). 0 이 되면 "다시 받기"

SEED 와 다른 점

  • groo 가 정한 컴포넌트다 — SEED 에는 OTP 부품이 없다(Figma 에 비공개 PIN Field 가 상태만 있다). 칸은 SEED Text Input 그대로이고 "Input을 나누지 말고" 를 따랐다.
  • 막힌 칸의 자리 글은 fg-placeholder 그대로 — Input · Textarea · SEED Text Input 은 막히면 자리 글도 fg-disabled 지만, 코드 칸은 막혀도 "6자리 숫자" 를 켜진 칸과 같은 색으로 둔다. 막힌 칸은 바탕(bg-disabled) · 커서로 갈린다(2026-10-17 사용자 결정 — 01 기초 앱 · 글자 웹 비교 페이지 01T-013 — Input 그대로 fg-disabled 는 고르지 않았다). 다크는 두 색이 같은 값(#838997)이라 차이가 없고, 라이트만 4.18 ↔ 3.16 으로 갈린다.
  • 숫자만 뽑기 · one-time-code · 숫자 키보드 · 다시 받기 · 설명 줄 — SEED 에 규칙이 없다(문서 · 예제 · 코드에 inputMode · one-time-code 0회). SEED 에서 가장 가까운 것은 maxGraphemeCount 로 글자 수를 세는 예("6글자까지 입력 가능합니다")다.

Migration notes

2026-10-17 — 막힌 칸의 자리 글

사용자가 01 기초 앱 · 글자 웹 비교 페이지 01T-013 에서 정했다 — 막힌 코드 칸의 자리 글은 fg-placeholder 그대로다(input-otp.yaml disabled › placeholder). 전에는 스펙이 자리 글 색 하나(fg-placeholder)만 적고 막힘 줄은 값만 적어, 웹 ds 견본이 Input 규칙대로 fg-disabled(#8A91A0)로 그렸다. 레시피는 InputOTP 가 입력에 placeholder:text-fg-placeholder 를 덧붙여 Input 의 막힘 색을 덮는다. 웹 · 앱 ds 의 코드 칸도 이렇게 옮긴다.

2026-10-09 — Input 한 칸으로 다시 쓴다

사용자가 입력 비교 페이지에서 정했다 — 한 칸(10A — SEED "Input을 나누지 말고 … 한 번에", Text Field large 52 · medium 40, 붙여넣기는 숫자만 뽑는다), 다시 받기 단추에 남은 초 + 칸 아래 늘 설명 줄(11B — "다시 받기(60초)" 동안 막힘). 그리고 "따라오는 것" — 숫자만 뽑기 · one-time-code · inputmode="numeric" · Field 오류 연결 · 자동 확인 없음 · "인증 코드" 띄어쓰기 한 꼴 · 단추 높이는 칸에 맞춘다(칸 아래로 옮겨 맞출 일을 없앴다). 6칸(10B) · 6칸 모양 + 한 입력(10C) · 표시 없음(11A) · 10:00 시계(11C)는 고르지 않았다. 스펙을 쓰다 나온 것(같은 비교 페이지 18) — 틀리면 오류 글이 설명 줄을 대신하고 칸을 고치기 시작하면 돌아온다(18A — Field 규칙 그대로). 오류 글과 설명 줄을 함께 보이는 예외(18B)는 고르지 않았다. 설명 줄은 Writing v106 대로 보조 용언을 붙이고 마침표를 찍었다("입력해주세요 · … 받아요."). 옛 스펙은 input-otp.history/v-pre-seed-input.* 에 남겼다.

옛 Input OTP새 Input OTP
칸 6 × 40 · 사이 4 · 18 / 600 고정폭 글꼴 · 숨은 입력 하나(DESIGN v69 는 칸마다 입력 · maxlength="1")Input 한 칸 — large 52 · medium 40 · 고정폭 숫자
빈 칸 surface-input · 찬 칸 surface-default · 포커스 바깥 2px border-focus투명 바탕 · 안쪽 1px · 포커스 안쪽 2px stroke-neutral-contrast(Input)
오류 error 테두리 + 30% 링 · "코드가 올바르지 않습니다. 다시 입력해주세요."안쪽 2px stroke-critical-solid + Field 오류 · 해요체
막힘 불투명도 0.5bg-disabled · fg-disabled
3-3 구분 줄 · 만료 카운트다운 Do구분 없음 · 설명 줄(10분 · 5번) + 다시 받기 남은 초

제품은 앱 적용 단계에서 옮긴다(2026-10-09 조사 — Desk 웹은 크로미움에서 진짜 클립보드로 붙여 봤고, Desk 앱 · 서버는 코드로 봤다). 코드 칸은 Desk 이용 해지의 본인 확인 하나다 — "비밀번호가 없으신가요? 메일로 코드 받기". 옛 스펙의 6칸 부품은 네 제품 어디서도 쓰지 않는다(웹은 스펙 JSON shared/ds/spec/input-otp.json 만). HR 에는 없고, SSO 는 비밀번호 찾기가 임시 비밀번호 메일 · 가입이 초대 링크라 코드 칸이 없다. Desk 2단계 인증은 걷혔다.

  • 붙여넣기가 숫자를 잃는다 — 웹(features/user/ui/WithdrawDialog.tsx:295-301)은 maxLength={6} 을 브라우저가 먼저 적용하고 그 뒤에 숫자만 남겨 "123 456" · "123-456" · " 123456" → "12345", "인증 코드: 123456" → "" 다(D5). 메일에는 코드가 28 / 700 · 자간 6 한 덩어리로 찍힌다(porest-sso-back EmailServiceImpl.java:117-126). 앱(features/settings/presentation/withdrawal_sheet.dart:427-470)은 numbersOnly 가 길이 제한보다 먼저라 붙여넣기가 맞다(shared/widgets/p_text_input.dart:108-117).
  • 시간 · 다시 받기 · 시도 표시 0 — 서버는 유효 10분 · 5번 틀리면 다시 받기 · 다시 받기 60초(ReauthServiceImpl.java:41-46 · 69-75)인데 화면에는 아무것도 없고, 60초 안에 다시 받으면 서버 글 "코드를 방금 보냈어요. 잠시 뒤에 다시 받아 주세요" 가 칸 아래 빨갛게 뜬다(D15). one-time-code 0 · 오류의 aria-invalid · aria-describedby · 알림 0(웹), 앱 autofillHints 0.
  • 칸 40 옆 단추 36 — 웹 "코드 받기 · 다시 받기" 83 × 36 이 칸 40 옆이다(WithdrawDialog.tsx:293-312, D27). 칸 아래 Button medium 40 으로.
  • 글 — 화면 "인증 코드" ↔ 서버 "인증코드"(messages_ko.properties:18-21 — 합쇼 · 해요가 섞인 서버 글 셋). 화면 글은 "인증 코드" · 해요체로.
  • 6자리 전 확인 막힘 — 웹 "해지하기" 가 6자리 전에는 꺼져 있다. Field 규칙대로 켜 두고 누를 때 알린다.

이 페이지