Input Types
텍스트·숫자·이메일·전화·날짜·선택·체크·라디오·텍스트영역 등 다양한 입력 컨트롤을 Kosmos DSL로 일관된 API와 접근성을 갖추어 구성합니다.
설계 원칙
- 의미 기반 타입: 가능한 한 올바른
type을 사용하여 모바일 키보드/자체 밸리데이션을 활용합니다. - 레이블·힌트·에러 표준화:
label,.form-text,.invalid-feedback를 통일합니다. - 값 포맷과 검증 분리: 마스킹/포맷은 보조일 뿐, 최종 검증은 서버가 담당합니다.
- 접근성 일급:
for↔id,aria-describedby,aria-invalid등을 지킵니다.
코드 심화: 공용 Input 유틸
public final class FormInputs {
private FormInputs() {}
public static HtmlComponent text(String id, String value, String placeholder) {
return El.input().css("form-control").attr("type","text")
.attr("id", id).attr("name", id)
.attr("placeholder", placeholder == null ? "" : placeholder)
.attr("value", value == null ? "" : value);
}
public static HtmlComponent email(String id, String value) {
return El.input().css("form-control").attr("type","email")
.attr("id", id).attr("name", id).attr("autocomplete","email")
.attr("inputmode","email").attr("value", value == null ? "" : value);
}
public static HtmlComponent tel(String id, String value) {
return El.input().css("form-control").attr("type","tel")
.attr("id", id).attr("name", id).attr("inputmode","tel")
.attr("value", value == null ? "" : value);
}
public static HtmlComponent number(String id, Number value, Integer min, Integer max, Integer step) {
var i = El.input().css("form-control").attr("type","number")
.attr("id", id).attr("name", id);
if (value != null) i.attr("value", String.valueOf(value));
if (min != null) i.attr("min", String.valueOf(min));
if (max != null) i.attr("max", String.valueOf(max));
if (step != null) i.attr("step", String.valueOf(step));
return i;
}
public static HtmlComponent date(String id, String value) {
return El.input().css("form-control").attr("type","date")
.attr("id", id).attr("name", id).attr("value", value == null ? "" : value);
}
public static HtmlComponent select(String id, String current, List<Option> options, boolean disabled) {
var s = El.select().css("form-select").attr("id", id).attr("name", id);
if (disabled) s.attr("disabled","disabled");
for (var o : options) {
var el = El.option().attr("value", o.value()).text(o.label());
if (Objects.equals(o.value(), current)) el.attr("selected","selected");
s.child(el);
}
return s;
}
public static HtmlComponent textarea(String id, String value, int rows) {
return El.textarea().css("form-control").attr("id", id).attr("name", id)
.attr("rows", String.valueOf(Math.max(rows,3))).text(value == null ? "" : value);
}
public record Option(String value, String label) {}
}
텍스트 / 이메일 / 전화
// 텍스트
new CompFormGroup("name", "이름",
FormInputs.text("name", dto.getName(), "예: 홍길동"),
"실명 기준으로 입력하세요.", null
);
// 이메일
new CompFormGroup("email", "이메일",
FormInputs.email("email", dto.getEmail()),
"비밀번호 찾기 등 안내 발송", null
);
// 전화
new CompFormGroup("phone", "전화번호",
FormInputs.tel("phone", dto.getPhone()),
"숫자만 입력(예: 01012345678)", null
);
숫자 / 날짜
// 숫자
new CompFormGroup("age", "나이",
FormInputs.number("age", dto.getAge(), 0, 120, 1),
"0~120 사이 정수", null
);
// 날짜
new CompFormGroup("birth", "생년월일",
FormInputs.date("birth", dto.getBirth()),
"YYYY-MM-DD", null
);
선택 (Select)
List<FormInputs.Option> roles = List.of(
new FormInputs.Option("", "역할 선택"),
new FormInputs.Option("USER", "일반 사용자"),
new FormInputs.Option("ADMIN", "관리자")
);
new CompFormGroup("role", "역할",
FormInputs.select("role", dto.getRole(), roles, false),
"권한에 따라 메뉴가 달라집니다.", null
);
체크박스 / 라디오
// 체크박스 (단일)
El.div().css("mb-3").children(
El.div().css("form-check").children(
El.input().css("form-check-input").attr("type","checkbox").attr("id","agree").attr("name","agree")
.attr("value","Y").attr(dto.isAgree() ? "checked" : null, dto.isAgree() ? "checked" : null),
El.label().css("form-check-label").attr("for","agree").text("약관에 동의합니다")
),
El.div().css("form-text").text("동의해야 진행됩니다.")
);
// 라디오 (그룹)
El.div().css("mb-3").children(
El.span().css("form-label d-block").text("알림 수신"),
El.div().css("form-check").children(
El.input().css("form-check-input").attr("type","radio").attr("name","notify").attr("id","notifyY").attr("value","Y")
.attr(dto.getNotify().equals("Y") ? "checked" : null, dto.getNotify().equals("Y") ? "checked" : null),
El.label().css("form-check-label").attr("for","notifyY").text("예")
),
El.div().css("form-check").children(
El.input().css("form-check-input").attr("type","radio").attr("name","notify").attr("id","notifyN").attr("value","N")
.attr(dto.getNotify().equals("N") ? "checked" : null, dto.getNotify().equals("N") ? "checked" : null),
El.label().css("form-check-label").attr("for","notifyN").text("아니오")
)
);
텍스트 영역 (Textarea)
new CompFormGroup("bio", "소개",
FormInputs.textarea("bio", dto.getBio(), 5),
"간단한 자기소개를 작성하세요. 최대 500자.", null
);
상태/검증 클래스 매핑
| 상태 | 컨트롤 클래스 | 피드백 영역 | 적용 시점 |
|---|---|---|---|
| 기본 | form-control 또는 form-select |
.form-text |
항상 |
| 유효 | is-valid |
.valid-feedback |
선택적 |
| 오류 | is-invalid |
.invalid-feedback |
서버 검증 실패 시 |
| 비활성 | disabled/readonly |
— | 제어 불가/보기 전용 |
형식 가이드: 포맷/마스킹
- 전화/숫자 포맷: 입력 도움은 UX 보조이며, 서버에서는 순수 숫자로 검증/저장하세요.
- 날짜:
type="date"로 기본 피커를 우선 사용하고, 브라우저 미지원 시 폴리필(선택)을 고려합니다. - 이메일:
type="email"은 기본 문법만 확인합니다. 도메인 검증 등은 서버에서 처리합니다.
접근성 체크리스트
- 레이블 연결:
label[for]↔input[id]일치 - 도움말/에러 연결:
aria-describedby="idHelp idError"처럼 다중 연결 가능 - 그룹 레이블: 라디오/체크 그룹에는 시각적 그룹 제목을 제공합니다.
- 키보드 내비게이션: 라디오는 ↑/↓, 체크는 Space 동작을 점검합니다.
보안 & 서버 팁
| 주제 | 권장 | 지양 |
|---|---|---|
| 검증 | 서버 단에서 Bean Validation + 도메인 검증 | 클라이언트 검증만 신뢰 |
| XSS | 출력 이스케이프 / 화이트리스트 | 사용자 HTML을 그대로 출력 |
| 화이트리스트 | 셀렉트 옵션은 서버에서 승인된 값만 | 임의 문자열을 그대로 저장 |
샘플: 에러 상태 주입
@SuppressWarnings("unchecked")
Map<String,String> errs = (Map<String,String>) ctx.model("errors");
HtmlComponent email = FormInputs.email("email", form.getEmail())
.css(errs != null && errs.containsKey("email") ? " is-invalid" : "");
El.div().css("mb-3").children(
El.label().css("form-label").attr("for","email").text("이메일"),
email,
errs != null && errs.get("email") != null
? El.div().css("invalid-feedback d-block").text(errs.get("email"))
: El.span().css("d-none").text("")
);
주의사항
placeholder 남용 금지: 레이블을 대체하지 않습니다. 레이블은 항상 필요합니다.
서버 검증 누락: 숫자/이메일/전화 형식은 최종적으로 서버에서 재검증해야 합니다.
옵션 동기화:
<select>의 옵션은 서버 권한/설정과 반드시 동기화하세요.다음으로
- File Upload — 업로드 컨트롤/보안/검증
- Validation — UI 상태/에러 요약/포커스
- Alerts — 성공/오류 피드백 패턴
- Buttons — 제출/취소/비동기 버튼 UX