# 킨텍스 — 공개 인증 API 계약 (회원가입·비밀번호 찾기/초기화) > 작성: kintex-backend-dev · 근거: `docs/design.md`(SCR-01) 프론트 계약 정합 + `_workspace/01_backend_contracts.md`(§0 공통규약 준수) > 기존 인증(JWT/RBAC `auth`)·Phase B(`security`: TotpService·LoginAttempt·app_user otp/lock)는 **재사용·불변**. 본 3종은 순증. > 전부 **공개**(SecurityConfig `permitAll` 추가). 응답은 표준 `ApiResponse` 봉투. 비밀번호·재설정 코드·해시는 응답/로그 미노출(계약 §0-3). --- ## 1. POST /api/auth/register — 회원가입 (공개) 요청 ```json { "email": "a@x.com", "displayName": "홍길동", "password": "min8chars", "companyName": "지오인포(선택)", "inviteCode": "evt-2026(선택)" } ``` - 검증: email @Email, displayName 필수(≤120), password 8~100자. companyName·inviteCode 선택. 응답 200 ```json { "success": true, "data": { "userId": "usr-xxxxxxxxxxxxxxxxxxxx", "email": "a@x.com", "displayName": "홍길동", "joinedEvent": "evt-2026" // inviteCode 매핑 성공 시 eventId, 아니면 null }, "error": null } ``` - 생성 계정: `role_code='USER'`, `status='ACTIVE'`(즉시 로그인 가능), `verify_method='EMAIL'`, `hall_manager=false`. 비밀번호 BCrypt 해시 저장. - `inviteCode`: **행사 식별자(event.id)로 해석** → 활성 행사면 `event_member`(role `EXHIBITOR`) best-effort 매핑. 매칭 실패/에러는 무시(가입은 성립, joinedEvent=null). - `companyName`: `app_user.company_name`(V9 순증 컬럼)에 보존(선택). - 응답에 password_hash·otp 등 민감정보 제외. 오류 | 상황 | code | HTTP | |---|---|---| | email 중복 | `EMAIL_TAKEN` | 409 | | 검증 실패 | `VALIDATION` | 400 | --- ## 2. POST /api/auth/password/forgot — 비밀번호 찾기 (공개) 요청 `{ "email": "a@x.com" }` 응답 **항상 200**(사용자 열거 방지 — 존재 여부와 무관하게 동일 응답) ```json { "success": true, "data": { "message": "입력하신 이메일이 등록되어 있으면 비밀번호 재설정 안내를 보내드립니다." }, "error": null } ``` - 활성 계정 존재 시에만 6자리 재설정 코드 생성 → BCrypt 해시로 `password_reset` 저장(만료 10분·1회성). - 이메일 발송 인프라 부재 → **dev 로깅으로 대체**. 로그에는 코드 마스킹(`1****6`)·이메일 마스킹만. 응답에 코드 미포함. --- ## 3. POST /api/auth/password/reset — 비밀번호 초기화 (공개) 요청 `{ "email": "a@x.com", "code": "123456", "newPassword": "min8chars" }` 응답 200 `{ "success": true, "data": { "message": "비밀번호가 변경되었습니다. 새 비밀번호로 로그인해 주세요." }, "error": null }` - 검증: 최신 미사용 코드 대조(만료·1회성·시도제한 max 5). 성공 시 `password_hash` 갱신 + `failed_login_count=0`·`locked_until=NULL` 리셋 + 코드 `used=true`. - 실패(코드 불일치/만료/시도초과/없음): `OTP_INVALID` (401), 메시지 일반화("재설정 코드가 올바르지 않거나 만료되었습니다."). --- ## 4. 스키마 — 마이그레이션 V9 (V1~V8 불변·멱등·순증) `V9__public_auth_password_reset.sql` - `ALTER TABLE app_user ADD COLUMN IF NOT EXISTS company_name varchar(200);` - `CREATE TABLE IF NOT EXISTS password_reset (id bigserial PK, email, code_hash BCrypt, expires_at, used, attempts, created_at)` + `idx_password_reset_email(email, used, created_at DESC)`. - `code_hash`는 저장/검증 전용 — 어떤 조회에서도 응답 SELECT 금지. ## 5. 코드 산출물 - 컨트롤러 `auth/PublicAuthController` (신규, `/api/auth` 순증 — 기존 AuthController·TwoFactorController 불변) - 서비스 `auth/PublicAuthService` - DTO `auth/dto/{RegisterRequest,RegisterResponse,ForgotPasswordRequest,ResetPasswordRequest,MessageResponse}` - 매퍼 `auth/mapper/PublicAuthMapper` + `mybatis/mapper/PublicAuthMapper.xml` - `ErrorCode.EMAIL_TAKEN`(409) 추가 / `SecurityConfig` permitAll 3경로 추가 / `OTP_INVALID` 재사용 - **미변경:** M2~M5, 기존 auth 로그인, Phase B 2FA, 프론트. `./gradlew build -x test` BUILD SUCCESSFUL.