Skip to content

회원가입 폼 (RegisterForm) ​

기관과 첫 계정을 함께 만드는 2단계 가입 폼입니다. 상단 BI 락업을 무엇이 정하는지, 검증이 어느 단계에서 걸리는지 확인해 보세요.

Alpine 컴포넌트 · Web · 진행 중(WIP) · 문서 포맷 시범 적용

Interactive playground ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마
새 계정을 만드세요
1계정 정보
2기관 정보
이미 계정이 있으신가요?
실제 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 없음 → 락업은 자기 패밀리, 앱 표기는 생략된다
})

이 컴포넌트는 넘길 옵션이 거의 없습니다(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--danger2단계 버튼 줄 위

border-red-400은 Tailwind 클래스처럼 보이지만 design-system.css가 직접 받아 둔 것입니다 — input.border-red-400 { border-color: var(--ds-color-danger) !important }. 그래서 색이 토큰을 따라갑니다.

호출 패턴 ​

html
<div class="auth-form" x-data="registerForm">
  <!-- include "../../packages/components/html/signup-form.html" -->
</div>

넘길 것이 없다는 것이 규격입니다. 상단 락업을 정하는 값은 폼이 아니라 앱의 SDK 플러그인 설정에서 옵니다.

ts
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타입기본값설명
titlestring""락업 대신 글자를 쓰고 싶은 예외 화면만 넘깁니다. 브랜드 폴백을 두지 않은 것이 의도입니다
onSuccess() => void—가입 성공 후 호출

노출하는 상태 ​

멤버설명
brandLockuplinkedBrandFamily || brandFamily 로 고른 락업 SVG (getter)
appBrand상위 서비스가 있을 때만 이 앱의 brand, 아니면 "" (getter)
step1 계정 정보 / 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-applinkedBrandFamily 없음 — 락업은 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 partialpackages/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-08RegisterForm 페이지 신규 작성 — Playground(단계 × 브랜드 구성 3종 × 오류·로딩) + 모아보기 7상태