Groo Design

Time Picker

시각을 고르는 12시간 휠 — 오전·오후 · 시 · 분. 시각 칸(Input Button)을 누르면 1280 미만은 아래 시트 · 이상은 칸 아래 팝오버로 열리고, "완료" 로 넣는다. 날짜와 함께 받으면 Date Picker 의 날짜 칸 옆에 시각 칸을 나란히 둔다. 시각을 치는 칸은 두지 않는다.

구조는 당근 SEED Time Picker(Apache-2.0)를 따른다 — Wheel Picker 위의 세 칼럼(오전·오후 → 시 → 분)과 가운데 띠. 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-03 사용자 결정). 옛 Time Picker(v72 — 치는 칸 · 24시간)를 대신한다.

수치 원본은 time-picker.yaml이다. 휠의 크기 · 색 · 띠 · 안개는 wheel-picker.yaml(medium · 5칸)을 따른다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다.

9:41
일정 추가
팀 회의
10월 15일 (목)
시작 시간
오후 3:00
시간 선택
desk.groo.app
일정 추가
10월 15일 (목)
시작 시간
오후 3:00
9:41
일정 추가
팀 회의
10월 15일 (목)
시작 시간
오후 3:00
시간 선택
desk.groo.app
일정 추가
10월 15일 (목)
시작 시간
오후 3:00
일정 시작 시각 — 시트 · 팝오버, 라이트 · 다크

직접 골라 보기

분 간격 · 여는 자리를 고르면 스펙대로 그린 휠과 그 코드가 바뀐다. 실제로 휠을 굴리고 "완료" 로 칸에 넣을 수 있다.

9:41
일정 추가
시간 선택
시간 선택

고르는 중: 오전 11:55 — 시를 11 ↔ 12 로 넘기거나 분을 55 → 00 으로 넘겨 보세요.

분 간격 minuteStep
여는 자리
모드
브랜드
import { TimePicker } from "@/components/ui/time-picker"

// 1280 미만 — BottomSheetContent 의 본문(useInputButtonSurface, input-button.md)
<TimePicker value={draft} onValueChange={setDraft} autoFocus />
<Button onClick={() => { setTime(draft); setOpen(false) }}>완료</Button>

Anatomy

ⓐ Period Column — 오전 · 오후ⓑ Hour Column — 1~12, 오른쪽 정렬ⓒ Minute Column — 2자리ⓓ Indicator — 가운데 띠
휠은 오전·오후 · 시 · 분 세 칼럼과 가운데 띠
부위설명
ⓐ Period Column오전 · 오후 — 두 칸에서 멈춘다.
ⓑ Hour Column시 — 1 · 2 … 12, 오른쪽 정렬. 돌아간다.
ⓒ Minute Column분 — 2자리, 분 간격을 따른다. 돌아간다.
ⓓ Indicator선택 띠 — 세 칼럼을 가로지르는 가운데 한 줄.

부위

부위설명
root휠 — Wheel Picker medium, 칼럼 셋
periodColumn오전 · 오후 — 반복하지 않는다
hourColumn시 — 1 · 2 … 12(0 을 채우지 않는다), 오른쪽 정렬, 반복
minuteColumn분 — 2자리(00 · 05 …), 분 간격을 따른다, 반복
indicator선택 띠 — 세 칼럼을 가로지르는 한 줄

Properties

칼럼

칼럼은 오전·오후 → 시 → 분(한국어 순서)이고, 시 · 분 글자 없이 숫자만 쓴다 — 단위는 칼럼 이름과 자리가 알린다("오후 3 00" = 오후 3시 00분). 시는 오른쪽에 맞춰 한 자리 · 두 자리 수의 끝을 맞춘다. 휠은 220(44 × 5), 글자 26 / 35 · 500 이다.

칼럼은 오전·오후 → 시 → 분, 칼럼 폭은 항목 글 폭 + 좌우 16(분홍) · 칼럼 묶음은 휠 가운데. 글자 26 / 35 · 500(글자 크기 설정을 따르지 않는 px). 띠는 좌우 16 들인 모서리 8 · 띠에 걸친 글자만 짙게, 안개는 min(휠 높이 × 40%, 항목 3칸) = 88.

칼럼 · 띠 · 안개

공통

부위속성값비고
root높이220px44 × 5 — Wheel Picker medium
root칼럼 순서오전·오후 → 시 → 분locale 순서(ko) — 단위 글자(시 · 분)는 쓰지 않는다. 단위는 칼럼 이름과 자리가 알린다
root값 형식24시간 {hour, minute}화면은 12시간, 값은 24시간. 비었으면 지금 시각을 분 간격에 맞춰 반올림한 자리를 짚기만 한다(고른 것처럼 넣지 않는다)
periodColumn정렬center
periodColumn항목오전 · 오후반복하지 않는다 — 두 칸에서 멈춘다
hourColumn정렬right한 자리 · 두 자리 수의 끝을 맞춘다
hourColumn항목1 ~ 12반복 — 11 ↔ 12 를 넘으면 오전 · 오후가 따라 바뀐다
minuteColumn정렬center
minuteColumn항목분 간격대로 00 ~ 59반복 — 55 → 00 을 넘어도 시는 그대로다

Minute Step

분 간격은 기본 5 이고, 자리에 따라 1 · 10 · 15 · 30 을 쓴다 — 넓을수록 빨리 고르고 좁을수록 정밀하다. 칼럼에는 그 간격의 분만 있다.

간격쓰는 곳
1분이 중요한 자리 — 방해 금지 시작 · 끝, 알림
5기본 — 일정 · 거래 · 반복 실행
10 · 15느슨한 시간대 · 15분 단위 예약
30근무 · 일정 시간표(HR 일정)

분 간격

minuteStep설명
1정확한 분 — 알람 · 방해 금지처럼 분이 중요한 자리(60칸)
5 (기본)기본 — 일정 · 거래 · 대부분(12칸)
10느슨한 시간대(6칸)
1515분 단위 예약(4칸)
3030분 단위 — 근무 · 일정 시간표(2칸)

Guidelines

칸 표기와 같은 말

칸에는 "오후 3:00"(International Design v106 — 오전 · 오후 12시간, 초 없음)이 들어간다. 휠도 같은 말이라 24시간 휠을 두지 않는다. 값은 24시간({hour, minute})으로 다룬다.

"완료" 로 넣는다

휠을 굴리는 동안 칸 값은 그대로다 — "완료" 를 누를 때 들어간다. 바깥 · 끌어내리기 · Esc 로 닫으면 버린다. 칸이 비었으면 지금 시각을 분 간격에 맞춰 반올림한 자리(9시 13분 → 9시 15분)를 짚어 열기만 한다.

어디에 놓나

시각 칸은 Input Button 이 연 시트(1280 미만) · 팝오버(이상) 안이다. 시각 고르기가 그 화면의 주된 일이면 화면에 바로 펼친 휠(Inline — 확정은 화면의 "다음" · "완료")을 둘 수 있다 — SEED 와 같게 두되 지금 쓰는 곳은 없다. 고르는 표는 Date Picker 의 "어디에 놓나" 한 곳이다(2026-10-22 사용자 결정 1C).

시 · 분이 넘어갈 때

시 휠이 11 ↔ 12 를 넘으면 오전 · 오후가 따라 바뀐다(오전 11 → 오후 12). 분 휠은 55 → 00 을 넘어도 시는 그대로다(10시 55분 → 10시 00분).

날짜와 함께

날짜와 시각을 함께 받으면 날짜 칸 옆에 시각 칸을 나란히 둔다 — 하나만 바꾸기 쉽고, 종일이면 시각 칸만 걷는다. 시각 칸은 144(large 에서 "오후 12:30" 이 잘리지 않는 폭), 날짜 칸은 글이 다 들어가는 폭 이상이다 — 한 줄에 둘이 들어가지 않으면 시각 칸이 다음 줄로 내려간다 — 다른 해 날짜("2027년 1월 3일 (일)") · 큰 글자 설정 · 좁은 폰에서. 올해 날짜는 360 폭 폰에서 거의 꽉 차므로(가장 긴 "12월 28일 (월)" 이 1px 남는다) 글꼴에 따라 내려갈 수 있다 — 어느 쪽이든 칸 글은 자르지 않는다. 칸마다 Field 로 감싸면 폭 클래스는 Field 에 준다. 칸 글을 말줄임으로 자르지 않는다. 한 칸에 날짜 · 시각을 같이 넣지 않는다.

폼에서는 이 두 칸 나란히다(사용자 결정). 날짜 고르기가 그 화면의 주된 단계라 달력을 화면에 펼쳤다면(Inline — Date Picker 의 "어디에 놓나"), 시각은 그 아래 칸 → 시트로 둔다(SEED — "날짜가 핵심 단계면 Date Picker 를 Inline 으로 펼치고, 시간은 보조 필드로 두어 Time Picker 를 Sheet 로"). 지금 그런 화면은 없다(2026-10-22 사용자 결정 2B — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L).

10월 15일 (목)
시작 시간
오후 3:00
2027년 1월 3일 (일)
종료 시간
오후 12:30
360 폰 — 한 줄에 들어가지 않으면(다른 해 날짜 2027년 1월 3일 (일)) 시각 칸이 다음 줄로. 라벨은 짝마다 하나
이렇게 — 날짜 칸 옆에 시각 칸 — 한 줄에 다 들어가지 않으면(다른 해 날짜 · 큰 글자 · 좁은 폰) 시각 칸이 다음 줄로 내려가고, 칸 글은 자르지 않는다
10월 15일 (목) 오후 3:00
2027년 1월 3일 (일) 오후 12:30
이렇게 하지 않는다 — 한 칸에 날짜 · 시각 — 시각만 바꿀 때도 날짜부터 지나고, 종일이어도 시각을 걷을 수 없다
일정 시작 · 종료 — 날짜 칸 + 시각 칸

코드

레시피 recipes/shadcn/components/ui/time-picker.tsx 를 Input Button 이 연 시트 · 팝오버 안에 둔다(펼친 휠은 화면에 바로 둔다 — 지금 쓰는 곳은 없다). 아래 미리보기는 스펙 값으로 그린 모습이다.

시각 — "완료" 로 넣는다

오후 3:00

이 창의 폭으로 연다 — 1280 미만은 아래 시트, 이상은 칸 아래 팝오버. 휠을 굴려도 칸은 "완료" 를 누를 때 바뀐다.

import { Clock } from "lucide-react"
import { TimePicker, formatTimeValue, roundToStep } from "@/components/ui/time-picker"

<InputButton value={formatTimeValue(time)} placeholder="시간 선택" suffixIcon={<Clock />} onClick={() => { setDraft(time ?? roundToStep(new Date(), 5)); setOpen(true) }} />

<TimePicker minuteStep={5} value={draft} onValueChange={setDraft} />
<Button onClick={() => { setTime(draft); setOpen(false) }}>완료</Button>

날짜 + 시각 — 두 칸 나란히

10월 2일 (금)
오후 12:30
2027년 1월 3일 (일)
오후 1:30

이 창의 폭으로 연다 — 1280 미만은 아래 시트, 이상은 칸 아래 팝오버. 휠을 굴려도 칸은 "완료" 를 누를 때 바뀐다.

{/* 한 줄에 다 들어가지 않으면 시각 칸이 다음 줄로 — 날짜 칸은 글이 다 들어가는 폭 이상 */}
<div className="flex flex-wrap gap-x2">
  <InputButton rootClassName="min-w-max flex-1" value={formatDateValue(date)} placeholder="날짜 선택" suffixIcon={<CalendarDays />} onClick={openDate} />
  <InputButton rootClassName="w-[144px] shrink-0" value={formatTimeValue(time)} placeholder="시간 선택" suffixIcon={<Clock />} onClick={openTime} />
</div>

Behavior

인터랙션동작
굴리기 · 끌기멈춘 뒤 가장 가까운 칸에 맞추고 값이 정해진다(wheel-picker.md)
보이는 항목 누르기그 항목을 가운데로 옮긴다
시 11 ↔ 12오전 · 오후가 따라 바뀐다
분 55 → 00시는 그대로다
오전 · 오후 바꾸기시 · 분은 그대로, 12시간을 더하거나 뺀다
열 때칸 값(비었으면 지금을 분 간격에 맞춰 반올림한 자리)이 가운데 — 초점은 시 칼럼으로

키보드

키동작
↑ ↓그 칼럼의 이전 · 다음 항목
Home End처음 · 끝 항목(시는 1 · 12) — 시 칼럼은 오전 · 오후를 바꾸지 않는다(지나간 칸을 세지 않고 그 시로 간다. 화살표로 11 ↔ 12 를 넘을 때만 따라 바뀐다)
Tab다음 칼럼(오전·오후 → 시 → 분 → "완료")

Accessibility

기준검증
WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1)고른 항목 fg-neutral 띠(bg-neutral-weak) 위 15.20 · 다크 10.32 ✓. 위아래 항목 fg-disabled 는 고를 값이 아닌 둘레 표시(안개로 흐려진다) — 고른 값은 띠 위 글자와 칸 값이 알린다
WCAG 2.1.1 Keyboard칼럼마다 Tab 자리 하나, ↑ ↓ 로 고른다 ✓
WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24)항목 44 높이 ✓
WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44)항목 44 높이 ✓ · 폭은 칼럼 글자 폭 + 32(64 이상) ✓
앱(Flutter 3.44.9)칼럼은 Wheel Picker 의 앱 줄대로 spinbutton 역할 대신 이름("오전/오후" · "시" · "분") + 값 + 올리기 · 내리기(Android 조절 막대 · iOS 조절 가능)다
ARIA휠 role="group"(이름 = 칸 라벨, 예: "시작 시각"), 칼럼 role="spinbutton" + 이름 "오전/오후" · "시" · "분", aria-valuetext "오후" · "3" · "00", aria-valuemin · aria-valuemax · aria-valuenow(번호). 항목은 보조 기술에 숨긴다

Do / Don't

✅ Do

  • 칸 표기와 같은 오전·오후 · 시 · 분 휠.
  • 분 간격은 기본 5, 분이 중요한 자리만 1.
  • 날짜 칸 옆에 시각 칸을 나란히.

❌ Don't

  • 시각을 치는 칸("HH:mm")으로 받기.
  • 24시간 휠 — 칸은 "오후 3:00" 인데 휠이 "15 00" 이면 표기가 갈린다.
  • 분 칼럼에 60칸을 기본으로 두기(일정 · 거래).
  • 휠을 굴리는 대로 칸 값을 바꾸기 — "완료" 로 넣는다.

Specification

Base

부위상태속성값
rootenabled높이height220px44 × 5 — Wheel Picker medium
칼럼 순서columnOrder오전·오후 → 시 → 분locale 순서(ko) — 단위 글자(시 · 분)는 쓰지 않는다. 단위는 칼럼 이름과 자리가 알린다
값 형식valueFormat24시간 {hour, minute}화면은 12시간, 값은 24시간. 비었으면 지금 시각을 분 간격에 맞춰 반올림한 자리를 짚기만 한다(고른 것처럼 넣지 않는다)
periodColumnenabled정렬aligncenter
항목items오전 · 오후반복하지 않는다 — 두 칸에서 멈춘다
hourColumnenabled정렬alignright한 자리 · 두 자리 수의 끝을 맞춘다
항목items1 ~ 12반복 — 11 ↔ 12 를 넘으면 오전 · 오후가 따라 바뀐다
minuteColumnenabled정렬aligncenter
항목items분 간격대로 00 ~ 59반복 — 55 → 00 을 넘어도 시는 그대로다

SEED 와 다른 점

  • 팝오버도 쓴다 — 1280 이상은 칸 아래 팝오버(Input Button 결정). SEED React 예제는 시트 · 바로 펼침뿐이다(디자인 문서에는 팝오버가 있다).
  • 열 때 초점은 시 칼럼 — SEED 예제는 시트 상자에 둔다.
  • 색은 groo 역할 짝 — 띠 bg-neutral-weak, 고른 글자 fg-neutral, 둘레 글자 fg-disabled(SEED 와 같은 역할).

Migration notes

2026-10-22 — 어디에 놓나 · 날짜가 주된 단계일 때

"어디에 놓나" 에 펼친 휠(Inline — 지금 쓰는 곳 없음, SEED 와 같게)을 적고 고르는 표는 Date Picker 한 곳을 가리킨다(1C). "날짜와 함께" 는 결정(폼은 두 칸 나란히) 그대로 + "날짜가 주된 단계면 달력은 펼치고 시각은 칸 → 시트"(2B). 사용자 결정 — https://claude.ai/artifact/Mag1tEv51MdQHdy1o8hY3L. 값 변경 없음.

2026-10-21 — 시 칼럼의 Home · End(03-C 3A)

오후 3 에서 End 가 "오전 12" 로 뒤집혔다 — 끝 항목 12 까지 휠이 지나간 칸 수(stepDelta)만큼 시를 더해서다. 사용자가 "오전 · 오후는 그대로, 1 · 12 로" 를 골랐다(2026-10-21 사용자 결정 — 03-C 3A, https://claude.ai/artifact/3nWUZCYpPh2Cg3uXc3DKtp). 레시피 Wheel Picker 가 onValueChange 에 jump(Home · End)를 싣고, Time Picker 는 jump 면 고른 값으로 시를 정한다. 앱 p_wheel_picker.dart · p_time_picker.dart 도 같다. 거절: 지금 그대로 · Home · End 받지 않음 · Home = 12 · End = 11.

2026-10-03 — SEED Time Picker 로 새로 둔다(옛 v72 Time Picker 를 대신)

사용자가 비교 페이지에서 정했다 — SEED Time Picker(12시간 · 오전·오후 → 시 → 분 · 44 × 5 · 26px), 분 간격은 기본 5 · 자리마다 1 · 10 · 15 · 30, 날짜와 시각은 두 칸 나란히. 옛 Time Picker(DESIGN.md v72 — <input type="time"> · "14:30" 24시간)는 걷었다.

제품은 앱 적용 단계에서 옮긴다(2026-10-02 조사).

  • Desk 웹 — InputTimePicker 6곳(치는 칸 + 팝오버 목록 2칼럼 · 5분). 시를 누르면 열린 채, 분을 누르면 닫힌다. 열면 초점이 "00" 이고 화살표가 안 된다. 덜 친 값("25:9")을 그대로 넘긴다. 방해 금지 칸은 키마다 저장한다.
  • Desk 앱 — PTimeInput 6곳(iOS 휠 · 1분 · [취소][확인]).
  • HR 웹 — InputTimePicker 2곳(셀렉트 2개 · 분 60개 · "확인", 셀렉트 이름 없음, Esc 뒤 고르던 값이 남는다), 일정 시 8 ~ 18 · 분 0/30 셀렉트.
  • 셋 다 24시간이다 — v106 표기(오전 · 오후)는 아직 어디에도 없다.

이 페이지