회원가입 폼 (RegisterForm)
기관과 첫 계정을 함께 만드는 2단계 가입 폼입니다. 상단 BI 락업을 무엇이 정하는지, 검증이 어느 단계에서 걸리는지 확인해 보세요.
Alpine 컴포넌트 · Web · 진행 중(WIP) · 문서 포맷 시범 적용
Interactive playground
signup-form.html 마크업 그대로예요. 입력에 클래스가 w-full 뿐인 것도 원본 그대로입니다 — 텍스트필드 룩은 design-system.css 의 맨몸 input 규칙이 줍니다. <!-- 넘길 옵션이 없다 — 상단 락업은 앱이 아니라 스토어의 브랜드 패밀리가 정한다.
여기에 브랜드를 글자로 박으면 다른 패밀리가 조용히 새어 나간다 -->
<div class="auth-form" x-data="registerForm">
<!-- include "../../packages/components/html/signup-form.html" -->
</div>
<!-- 락업을 고르는 값은 앱이 SDK 플러그인에 넘긴다 -->
createAlpinePlugin({
brand: "hereby Medical",
brandFamily: "medical", // 이 앱의 패밀리
// linkedBrandFamily 없음 → 락업은 자기 패밀리, 앱 표기는 생략된다
})1단계 · 계정 정보 — 연동 없는 앱. 락업 하나뿐이고 앱 표기는 생략됩니다(락업과 같은 브랜드라서)
1단계 · 상위 서비스가 따로 있는 앱(직원 앱) — 락업은 상위(Medical), 그 아래에 이 앱 표기
1단계 · 자기 패밀리가 works 인 앱 — 락업만 바뀝니다
1단계 · 검증 오류 — 필드마다 한 줄, border-red-400 로 테두리가 붉어집니다
2단계 · 기관 정보 — 여기서만 「이전」 버튼이 나옵니다
2단계 · 제출 실패 — 필드 오류와 달리 .ds-notice--danger 한 덩어리
2단계 · 처리 중 — 버튼 안 아이콘 스피너
이 컴포넌트는 넘길 옵션이 거의 없습니다(title · onSuccess 둘뿐). 그래서 축은 대부분 상태입니다 — 단계(1↔2) · 브랜드 구성 · 검증 오류 · 제출 실패 · 처리 중. 코드에 바뀌는 것은 폼 호출이 아니라 앱이 SDK 플러그인에 넘긴 브랜드 값이고, 그것이 상단 락업을 정합니다.
규칙 (강제) · 코딩 에이전트용
RegisterForm 강제 규칙. 상세는 아래 본문.
마크업
- 호스트가
x-data="registerForm"으로 감싸고packages/components/html/signup-form.html을 include 합니다. 가입 폼 마크업을 손으로 짓지 마세요. x-html로 폼을 주입하지 마세요 — Alpine이 주입된 HTML의 디렉티브를 재스캔하지 않습니다.
⚠️ 트랩 ① 브랜드를 문자열로 박지 마세요 (실제 사고)signup-form.html은 여러 앱이 함께 인라인합니다. 여기에 브랜드를 글자로 두면 다른 패밀리가 조용히 새어 나갑니다 — 실제로 직원 앱 가입 화면에 hereby Medical이 뜬 적이 있습니다. 그래서 컴포넌트의 title 폴백도 브랜드가 아니라 빈 문자열입니다. 상단 락업은 brandLockup getter가 스토어의 패밀리로 고릅니다.
⚠️ 트랩 ② 가입 상단은 락업, 로그인 상단은 텍스트입니다 가입 화면만 BI 락업(심볼 + 워드마크 통짜)을 씁니다. 로그인은 텍스트 제목이 정본이고 심볼은 opt-in입니다 — 두 화면의 규칙이 다릅니다. 가입에 심볼만, 로그인에 락업을 놓는 식으로 섞지 마세요.
⚠️ 트랩 ③ 락업은 「상위 서비스」의 것입니다brandLockup은 linkedBrandFamily || brandFamily로 고릅니다. (2026-10-01 이전) 직원 앱(Works)은 병원의 Medical과 연동되므로 Medical 락업이 올라가고, 그 아래 한 줄에 이 앱 표기(hereby Works)가 붙습니다. 연동 대상이 없는 앱은 자기 락업을 쓰고, 그때는 앱 표기를 생략합니다 — 위 락업과 같은 브랜드라 두 번 적히기 때문입니다.
⚠️ 트랩 ④ 오류가 두 종류이고 섞으면 안 됩니다 필드 검증 오류는 입력 아래 붉은 한 줄(text-xs text-red-500)이고, 입력 테두리는 border-red-400이 붉게 만듭니다(그 클래스는 design-system.css가 --ds-color-danger로 받아 둔 것입니다). 제출 실패만 .ds-notice--danger 한 덩어리입니다. 필드 오류를 notice로 올리거나 제출 실패를 필드 밑에 넣지 마세요.
⚠️ 트랩 ⑤ 검증은 지금 단계 것만 돕니다validate()는 step으로 분기해 그 단계 필드만 검사합니다. 2단계에서 1단계 필드가 비어도 잡히지 않으므로, 단계를 건너뛰는 진입점을 만들지 마세요.
- 다크 모드(2026-10-19) — 공용
signup-form.html의 팔레트 유틸 24곳 중 21곳을 시멘틱으로 옮겼다. 남은 3곳은 단계 표시의 브랜드 주황(bg-orange-500) · 진행선(bg-neutral-300)으로 테마 불변 자리다. 바깥 상자는 로그인과 같다. 확인: Playground 「미리보기 테마 → 다크」.
무엇을 만드나
기관과 그 기관의 첫 계정을 한 번에 만듭니다. 기존 기관에 직원을 추가하는 화면이 아닙니다 — 그건 AddEmployeePanel입니다.
그래서 2단계입니다. 1단계는 사람(이름 · 이메일 · 전화 · 비밀번호), 2단계는 기관(기관명 · 기관 코드 · 약관 동의)입니다.
브랜드
이 파일에 브랜드를 글자로 두지 마세요
signup-form.html은 여러 앱이 함께 인라인합니다. 문자열을 박으면 다른 패밀리가 조용히 새어 나갑니다 — 실제로 직원 앱 가입 화면에 hereby Medical이 뜬 적이 있습니다.
상단은 BI 락업(심볼 + 워드마크 통짜)이고, 무엇을 쓸지는 이렇게 정해집니다.
| 앱의 브랜드 설정 | 상단 락업 | 락업 아래 앱 표기 |
|---|---|---|
brandFamily만 있음 | 자기 패밀리 락업 | 생략 (락업과 같은 브랜드라 두 번 적힘) |
brandFamily + linkedBrandFamily | 상위 서비스 락업 | 이 앱의 brand 한 줄 |
직원 앱이 두 번째 경우입니다. Works는 병원의 Medical과 연동되므로 Medical 락업이 위에 오고, 아래에 hereby Works가 붙습니다.
락업 SVG는 viewBox로 비율만 고정돼 있습니다 — 높이만 정하면 폭은 비율대로 따라갑니다([&>svg]:h-8). 폭을 눈으로 맞추지 마세요.
접근성
autocomplete는 필드마다 다릅니다 — 이메일email, 전화tel, 비밀번호와 확인 둘 다new-password(current-password가 아닙니다).- 비밀번호 표시 토글은
aria-label이 상태에 따라 바뀝니다. - 약관 동의는
<label>이 체크박스를 감싸고 있어 문구를 눌러도 토글됩니다.
두 단계
| 단계 | 필드 | 버튼 |
|---|---|---|
| 1 · 계정 정보 | 성 · 이름 · 이메일 · 전화번호 · 비밀번호 · 비밀번호 확인 | 「다음」 하나 |
| 2 · 기관 정보 | 기관명 · 기관 코드 · 약관 동의 | 「이전」 + 「회원가입」 |
상단 단계 표시는 step >= n으로 칠해집니다 — 2단계에 가면 1번 원도 계속 주황으로 남습니다(지나온 단계 표시).
nextStep()은 검증을 통과해야만 2단계로 넘어갑니다. prevStep()은 검증 없이 1단계로 돌아가면서 errors를 비웁니다 — 되돌아갈 때 붉은 줄이 남지 않습니다.
검증 규칙
validate()가 step으로 갈라 지금 단계 필드만 검사합니다.
1단계
| 필드 | 규칙 |
|---|---|
| 성 · 이름 | 공백 제외 1자 이상 |
| 이메일 | /^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
| 전화번호 | 하이픈을 뺀 뒤 /^01[016789]\d{7,8}$/ |
| 비밀번호 | 기본 규칙 — 8자 이상 그리고 대문자 · 소문자 · 숫자 · 특수문자(영문·숫자가 아닌 모든 문자) 각 1개 이상. 아직 병원이 없는 계정이라 병원 설정 대신 기본값을 쓰며, 입력칸 안내(describeUserPasswordPolicy())·검사(userPasswordPolicyError())·서버(auth.register)가 같은 함수(@hereby/core/constants)를 거친다 |
| 비밀번호 확인 | 위와 일치 |
비밀번호 칸 아래에는 .ds-form-help 로 「비밀번호 규칙은 이후 설정에서 변경할 수 있어요.」(auth.password_rule_change_later)를 항상 보인다 — 가입한 사람이 병원 관리자가 되어 병원 설정에서 규칙을 바꿀 수 있기 때문이다.
2단계
| 필드 | 규칙 |
|---|---|
| 기관명 | 공백 제외 1자 이상 |
| 기관 코드 | /^[a-zA-Z][a-zA-Z0-9_-]{2,49}$/ — 영문으로 시작, 총 3~50자, 영문·숫자·하이픈·밑줄 |
| 약관 동의 | 체크 필수 |
전화번호는 입력하는 대로 정리되고, 보낼 때 한 번 더 바뀝니다
formatPhone()이 입력할 때마다 010-1234-5678 꼴로 하이픈을 넣고, 제출 직전 phoneToE164()가 앞의 0을 82로 바꿔 821012345678로 보냅니다. 화면에 보이는 값과 서버로 가는 값이 다릅니다.
오류 표시
두 종류이고 자리가 다릅니다.
| 종류 | 모양 | 자리 |
|---|---|---|
| 필드 검증 | text-xs text-red-500 한 줄 + 입력에 border-red-400 | 그 입력 바로 아래 |
| 제출 실패 | .ds-notice ds-notice--danger | 2단계 버튼 줄 위 |
border-red-400은 Tailwind 클래스처럼 보이지만 design-system.css가 직접 받아 둔 것입니다 — input.border-red-400 { border-color: var(--ds-color-danger) !important }. 그래서 색이 토큰을 따라갑니다.
호출 패턴
<div class="auth-form" x-data="registerForm">
<!-- include "../../packages/components/html/signup-form.html" -->
</div>넘길 것이 없다는 것이 규격입니다. 상단 락업을 정하는 값은 폼이 아니라 앱의 SDK 플러그인 설정에서 옵니다.
Alpine.plugin(createAlpinePlugin({
appId: "employee-app",
brand: "hereby Works", // 락업 아래 한 줄로 뜨는 이 앱 표기
brandFamily: "works", // 이 앱의 패밀리
linkedBrandFamily: "medical", // 연동되는 상위 서비스 — 락업은 이쪽이 된다
}));함께 필요한 것
- 라우터 스토어 — 하단 「로그인」 링크가
$store.router.navigate('/signin')을 부릅니다. hereby스토어 — 문구(t()),sdk.auth.register(),brand·brandFamily·linkedBrandFamily.- 아이콘 레지스트리 —
logo-lockup-hereby-medical/-works·ic-eye·ic-eye-off·ic-spinner·ic-exclamation-circle. .auth-form래퍼 — employee-app · hr-system의main.css에 있는 앱별 폭·여백 규칙입니다. 공용 CSS가 아니므로 새 앱에 그대로 기대하지 마세요.
입력의 룩은 어디서 오나
signup-form.html의 입력은 로그인과 같은 ds-input--48 w-full 하나입니다(2026-10-01 게이트 화면 통일 — 그전엔 ds-input--40 + 손으로 붙인 테두리·라운드 유틸). 그전에는 클래스가 w-full 뿐이었는데도 DS 텍스트필드로 보였는데, design-system.css에 맨몸 input 규칙이 있기 때문입니다 — input:not(.cell-input):not(.ds-input-plain):not([type="checkbox"])… 처럼 긴 :not() 사슬로 대부분의 입력을 잡습니다.
그 :not() 사슬은 명시도가 높습니다
사슬이 길어서 input.ds-input--error 같은 평범한 선택자는 집니다. 그래서 DS는 오류 상태를 별도로, 같은 사슬 + .ds-input--error 형태로 다시 적어 두고 있습니다. 이 폼의 입력에 새 스타일을 얹을 때 한 줄로 안 먹는다면 이것이 원인입니다.
API
registerForm Alpine.data factory. 옵션 객체 없이 x-data="registerForm"으로 씁니다.
RegisterFormOptions (모두 optional)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
title | string | "" | 락업 대신 글자를 쓰고 싶은 예외 화면만 넘깁니다. 브랜드 폴백을 두지 않은 것이 의도입니다 |
onSuccess | () => void | — | 가입 성공 후 호출 |
노출하는 상태
| 멤버 | 설명 |
|---|---|
brandLockup | linkedBrandFamily || brandFamily 로 고른 락업 SVG (getter) |
appBrand | 상위 서비스가 있을 때만 이 앱의 brand, 아니면 "" (getter) |
step | 1 계정 정보 / 2 기관 정보 |
email · phone · password · confirmPassword · firstName · lastName · organizationName · organizationCode · acceptTerms | 입력값 |
showPassword · isLoading | 비밀번호 표시 토글 · 제출 중 |
errors | 필드별 오류 맵. 제출 실패는 errors.submit |
메서드
| 멤버 | 설명 |
|---|---|
formatPhone() | 입력할 때마다 010-1234-5678 꼴로 하이픈 삽입(11자리에서 자름) |
phoneToE164() | 앞 0을 82로 바꿔 821012345678 반환 — 제출용 값 |
validate() | 지금 step의 필드만 검사. errors 채우고 통과 여부 반환 |
nextStep() | 통과했을 때만 step = 2 |
prevStep() | step = 1 + errors 비우기 (검증 없음) |
submit() | sdk.auth.register() 호출 |
가입 성공 뒤에 일어나는 일
register()는 SDK가 토큰을 자동으로 세팅합니다. 그래서 컴포넌트는 이어서 store.authenticated = true를 두고 getCurrentUser()로 사용자 정보를 채웁니다 — 이 조회는 실패해도 삼킵니다(가입 자체는 이미 끝났으므로). 그 뒤 onSuccess()가 불립니다.
사용처
| 앱 | 비고 |
|---|---|
| employee-app | linkedBrandFamily 없음 — 락업은 Works 하나(2026-10-01 변경: 로그인과 같은 브랜드로. 그전엔 Medical 락업 + 아래 hereby Works) |
| hr-system | 브랜드 설정을 안 넘겨 기본 패밀리(medical), 앱 표기 생략 |
admin 의 가입 화면은 이 컴포넌트가 아닙니다
admin은 앱 자체 signupForm(apps/admin/src/signup.ts)을 쓰고 공용 partial을 인라인하지 않습니다. 이메일 인증(signup-verify)과 초대 수락(invite)이 붙은 별도 흐름이라 그렇습니다 — admin 가입 화면을 고칠 때 이 페이지를 보면 안 됩니다.
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/RegisterForm.ts |
| HTML partial | packages/components/html/signup-form.html |
| 락업 자산 | packages/components/src/icons.ts (logo-lockup-hereby-*) |
| 브랜드 값 | packages/sdk/src/alpine/plugin.ts (brand · brandFamily · linkedBrandFamily) |
| Alpine 등록 | registerForm (plugin.ts) |
검토 필요 사항
- 컴포넌트 주석의 기관 코드 설명이 코드와 다릅니다.
RegisterForm.ts머리 주석은 「organizationCode (3-20 chars, alphanumeric + hyphens)」인데 실제 정규식은/^[a-zA-Z][a-zA-Z0-9_-]{2,49}$/— 3~50자이고 밑줄도 허용합니다. 로케일 문구(auth.org_code_invalid)는 정규식 쪽과 맞습니다. 주석만 낡은 것으로 보이지만, 어느 쪽이 의도인지는 확인이 필요합니다. - 가입 화면이 두 벌입니다. 공용
registerForm(employee-app · hr-system)과 admin 자체signupForm이 따로 있습니다. admin 쪽에만 이메일 인증·초대 수락이 붙어 있어 단순 중복은 아니지만, 두 화면의 검증 규칙과 브랜드 처리가 서로 어긋날 수 있습니다.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-09-08 | RegisterForm 페이지 신규 작성 — Playground(단계 × 브랜드 구성 3종 × 오류·로딩) + 모아보기 7상태 |