2026-02-05 09:38:42 +09:00
|
|
|
|
/**
|
2026-02-06 10:43:16 +09:00
|
|
|
|
* @file features/auth/constants.ts
|
|
|
|
|
|
* @description 인증 모듈 전반에서 사용되는 상수, 에러 메시지, 설정값 정의
|
|
|
|
|
|
* @remarks
|
|
|
|
|
|
* - [레이어] Core/Constants
|
|
|
|
|
|
* - [사용자 행동] 로그인/회원가입/비밀번호 찾기 등 인증 전반
|
|
|
|
|
|
* - [데이터 흐름] UI/Service -> Constants -> UI (메시지 표시)
|
|
|
|
|
|
* - [주의사항] 환경 변수(SESSION_TIMEOUT_MINUTES)에 의존하는 상수 포함
|
2026-02-03 10:51:22 +09:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
// 에러 메시지 상수
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 인증 에러 메시지 매핑
|
|
|
|
|
|
* Supabase의 영문 에러를 한글로 변환하기 위한 매핑 테이블
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const AUTH_ERROR_MESSAGES = {
|
|
|
|
|
|
// === 로그인/회원가입 관련 ===
|
|
|
|
|
|
INVALID_CREDENTIALS: "이메일 또는 비밀번호가 일치하지 않습니다.",
|
|
|
|
|
|
USER_EXISTS: "이미 가입된 이메일 주소입니다.",
|
|
|
|
|
|
EMAIL_NOT_CONFIRMED: "이메일 인증이 완료되지 않았습니다.",
|
|
|
|
|
|
|
|
|
|
|
|
// === 입력값 검증 ===
|
|
|
|
|
|
EMPTY_FIELDS: "이메일과 비밀번호를 모두 입력해 주세요.",
|
|
|
|
|
|
EMPTY_EMAIL: "이메일을 입력해 주세요.",
|
|
|
|
|
|
INVALID_EMAIL: "올바른 이메일 형식이 아닙니다.",
|
|
|
|
|
|
|
|
|
|
|
|
// === 비밀번호 관련 ===
|
2026-02-04 09:35:15 +09:00
|
|
|
|
PASSWORD_TOO_SHORT: "비밀번호는 최소 8자 이상이어야 합니다.",
|
2026-02-03 10:51:22 +09:00
|
|
|
|
PASSWORD_TOO_WEAK:
|
2026-02-04 09:35:15 +09:00
|
|
|
|
"비밀번호는 최소 8자 이상, 대문자, 소문자, 숫자, 특수문자를 각각 1개 이상 포함해야 합니다.",
|
|
|
|
|
|
PASSWORD_MISMATCH: "비밀번호가 일치하지 않습니다.",
|
2026-02-03 10:51:22 +09:00
|
|
|
|
PASSWORD_SAME_AS_OLD: "새 비밀번호는 기존 비밀번호와 달라야 합니다.",
|
|
|
|
|
|
|
|
|
|
|
|
// === 비밀번호 재설정 ===
|
|
|
|
|
|
PASSWORD_RESET_SENT: "비밀번호 재설정 링크를 이메일로 발송했습니다.",
|
|
|
|
|
|
PASSWORD_RESET_SUCCESS: "비밀번호가 성공적으로 변경되었습니다.",
|
|
|
|
|
|
PASSWORD_RESET_FAILED: "비밀번호 변경에 실패했습니다.",
|
|
|
|
|
|
|
|
|
|
|
|
// === 인증 링크 ===
|
|
|
|
|
|
INVALID_AUTH_LINK: "인증 링크가 만료되었거나 유효하지 않습니다.",
|
2026-02-05 13:19:53 +09:00
|
|
|
|
EMAIL_VERIFIED_SUCCESS: "이메일 인증이 완료되었습니다. 로그인해 주세요.",
|
2026-02-03 10:51:22 +09:00
|
|
|
|
|
2026-02-04 09:35:15 +09:00
|
|
|
|
// === 소셜 로그인 (OAuth) 관련 ===
|
|
|
|
|
|
OAUTH_ACCESS_DENIED: "로그인이 취소되었습니다.",
|
|
|
|
|
|
OAUTH_SERVER_ERROR:
|
|
|
|
|
|
"인증 서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
|
|
|
|
|
OAUTH_INVALID_SCOPE:
|
|
|
|
|
|
"필요한 권한이 설정되지 않았습니다. 개발자 설정 확인이 필요합니다.",
|
|
|
|
|
|
OAUTH_UNAUTHORIZED_CLIENT:
|
|
|
|
|
|
"인증 앱 설정(Client ID/Secret)에 문제가 있습니다.",
|
|
|
|
|
|
OAUTH_UNKNOWN_ERROR: "소셜 로그인 중 알 수 없는 오류가 발생했습니다.",
|
|
|
|
|
|
|
2026-02-03 10:51:22 +09:00
|
|
|
|
// === Rate Limit ===
|
|
|
|
|
|
EMAIL_RATE_LIMIT:
|
|
|
|
|
|
"이메일 발송 제한을 초과했습니다. 잠시 후 다시 시도해 주세요.",
|
|
|
|
|
|
EMAIL_RATE_LIMIT_DETAILED:
|
|
|
|
|
|
"이메일 발송 제한을 초과했습니다. Supabase 무료 플랜은 시간당 이메일 발송 횟수가 제한됩니다. 약 1시간 후에 다시 시도해 주세요.",
|
|
|
|
|
|
|
|
|
|
|
|
// === 기타 ===
|
|
|
|
|
|
DEFAULT: "요청을 처리하는 중 오류가 발생했습니다.",
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
2026-02-05 09:38:42 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// Supabase Auth 에러 코드 매핑
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
export const AUTH_ERROR_CODE_MESSAGES = {
|
|
|
|
|
|
anonymous_provider_disabled: "익명 로그인은 비활성화되어 있습니다.",
|
|
|
|
|
|
bad_code_verifier: "PKCE code verifier가 일치하지 않습니다.",
|
|
|
|
|
|
bad_json: "요청 본문이 올바른 JSON이 아닙니다.",
|
|
|
|
|
|
bad_jwt: "Authorization 헤더의 JWT가 유효하지 않습니다.",
|
|
|
|
|
|
bad_oauth_callback: "OAuth 콜백에 필요한 값(state)이 없습니다.",
|
|
|
|
|
|
bad_oauth_state: "OAuth state 형식이 올바르지 않습니다.",
|
|
|
|
|
|
captcha_failed: "CAPTCHA 검증에 실패했습니다.",
|
|
|
|
|
|
conflict: "요청 충돌이 발생했습니다. 잠시 후 다시 시도해주세요.",
|
|
|
|
|
|
email_address_invalid: "예시/테스트 도메인은 사용할 수 없습니다.",
|
|
|
|
|
|
email_address_not_authorized:
|
|
|
|
|
|
"기본 SMTP 사용 시 허용되지 않은 이메일 주소입니다.",
|
|
|
|
|
|
email_conflict_identity_not_deletable:
|
|
|
|
|
|
"이메일 충돌로 이 아이덴티티를 삭제할 수 없습니다.",
|
|
|
|
|
|
email_exists: "이미 가입된 이메일 주소입니다.",
|
|
|
|
|
|
email_not_confirmed: "이메일 인증이 완료되지 않았습니다.",
|
|
|
|
|
|
email_provider_disabled: "이메일/비밀번호 가입이 비활성화되어 있습니다.",
|
|
|
|
|
|
flow_state_expired: "로그인 흐름이 만료되었습니다. 다시 시도해주세요.",
|
|
|
|
|
|
flow_state_not_found: "로그인 흐름을 찾을 수 없습니다. 다시 시도해주세요.",
|
|
|
|
|
|
hook_payload_invalid_content_type:
|
|
|
|
|
|
"훅 페이로드의 Content-Type이 올바르지 않습니다.",
|
|
|
|
|
|
hook_payload_over_size_limit: "훅 페이로드가 최대 크기를 초과했습니다.",
|
|
|
|
|
|
hook_timeout: "훅 요청 시간이 초과되었습니다.",
|
|
|
|
|
|
hook_timeout_after_retry: "훅 요청 재시도 후에도 시간이 초과되었습니다.",
|
|
|
|
|
|
identity_already_exists: "이미 연결된 아이덴티티입니다.",
|
|
|
|
|
|
identity_not_found: "아이덴티티를 찾을 수 없습니다.",
|
|
|
|
|
|
insufficient_aal: "추가 인증이 필요합니다.",
|
|
|
|
|
|
invalid_credentials: "이메일 또는 비밀번호가 일치하지 않습니다.",
|
|
|
|
|
|
invite_not_found: "초대 링크가 만료되었거나 이미 사용되었습니다.",
|
|
|
|
|
|
manual_linking_disabled: "계정 연결 기능이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_challenge_expired: "MFA 인증 시간이 초과되었습니다.",
|
|
|
|
|
|
mfa_factor_name_conflict: "MFA 요인 이름이 중복되었습니다.",
|
|
|
|
|
|
mfa_factor_not_found: "MFA 요인을 찾을 수 없습니다.",
|
|
|
|
|
|
mfa_ip_address_mismatch: "MFA 등록 시작/종료 IP가 일치하지 않습니다.",
|
|
|
|
|
|
mfa_phone_enroll_not_enabled: "전화 MFA 등록이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_phone_verify_not_enabled: "전화 MFA 검증이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_totp_enroll_not_enabled: "TOTP MFA 등록이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_totp_verify_not_enabled: "TOTP MFA 검증이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_verification_failed: "MFA 검증에 실패했습니다.",
|
|
|
|
|
|
mfa_verification_rejected: "MFA 검증이 거부되었습니다.",
|
|
|
|
|
|
mfa_verified_factor_exists: "이미 검증된 전화 MFA가 존재합니다.",
|
2026-02-05 13:19:53 +09:00
|
|
|
|
mfa_web_authn_enroll_not_enabled:
|
|
|
|
|
|
"WebAuthn MFA 등록이 비활성화되어 있습니다.",
|
|
|
|
|
|
mfa_web_authn_verify_not_enabled:
|
|
|
|
|
|
"WebAuthn MFA 검증이 비활성화되어 있습니다.",
|
2026-02-05 09:38:42 +09:00
|
|
|
|
no_authorization: "Authorization 헤더가 필요합니다.",
|
|
|
|
|
|
not_admin: "관리자 권한이 없습니다.",
|
|
|
|
|
|
oauth_provider_not_supported: "OAuth 제공자가 비활성화되어 있습니다.",
|
|
|
|
|
|
otp_disabled: "OTP 로그인이 비활성화되어 있습니다.",
|
|
|
|
|
|
otp_expired: "OTP가 만료되었습니다.",
|
|
|
|
|
|
over_email_send_rate_limit: "이메일 발송 제한을 초과했습니다.",
|
|
|
|
|
|
over_request_rate_limit: "요청 제한을 초과했습니다.",
|
|
|
|
|
|
over_sms_send_rate_limit: "SMS 발송 제한을 초과했습니다.",
|
|
|
|
|
|
phone_exists: "이미 가입된 전화번호입니다.",
|
|
|
|
|
|
phone_not_confirmed: "전화번호 인증이 완료되지 않았습니다.",
|
|
|
|
|
|
phone_provider_disabled: "전화번호 가입이 비활성화되어 있습니다.",
|
|
|
|
|
|
provider_disabled: "OAuth 제공자가 비활성화되어 있습니다.",
|
|
|
|
|
|
provider_email_needs_verification: "OAuth 이메일 인증이 필요합니다.",
|
|
|
|
|
|
reauthentication_needed: "비밀번호 변경을 위해 재인증이 필요합니다.",
|
|
|
|
|
|
reauthentication_not_valid: "재인증 코드가 유효하지 않습니다.",
|
|
|
|
|
|
refresh_token_already_used: "세션이 만료되었습니다. 다시 로그인해주세요.",
|
|
|
|
|
|
refresh_token_not_found: "세션을 찾을 수 없습니다.",
|
|
|
|
|
|
request_timeout: "요청 시간이 초과되었습니다.",
|
|
|
|
|
|
same_password: "새 비밀번호는 기존 비밀번호와 달라야 합니다.",
|
|
|
|
|
|
saml_assertion_no_email: "SAML 응답에 이메일이 없습니다.",
|
|
|
|
|
|
saml_assertion_no_user_id: "SAML 응답에 사용자 ID가 없습니다.",
|
|
|
|
|
|
saml_entity_id_mismatch: "SAML 엔티티 ID가 일치하지 않습니다.",
|
|
|
|
|
|
saml_idp_already_exists: "SAML IdP가 이미 등록되어 있습니다.",
|
|
|
|
|
|
saml_idp_not_found: "SAML IdP를 찾을 수 없습니다.",
|
|
|
|
|
|
saml_metadata_fetch_failed: "SAML 메타데이터를 불러오지 못했습니다.",
|
|
|
|
|
|
saml_provider_disabled: "SAML SSO가 비활성화되어 있습니다.",
|
|
|
|
|
|
saml_relay_state_expired: "SAML relay state가 만료되었습니다.",
|
|
|
|
|
|
saml_relay_state_not_found: "SAML relay state를 찾을 수 없습니다.",
|
|
|
|
|
|
session_expired: "세션이 만료되었습니다.",
|
|
|
|
|
|
session_not_found: "세션을 찾을 수 없습니다.",
|
|
|
|
|
|
signup_disabled: "회원가입이 비활성화되어 있습니다.",
|
|
|
|
|
|
single_identity_not_deletable: "유일한 아이덴티티는 삭제할 수 없습니다.",
|
|
|
|
|
|
sms_send_failed: "SMS 발송에 실패했습니다.",
|
|
|
|
|
|
sso_domain_already_exists: "SSO 도메인이 이미 등록되어 있습니다.",
|
|
|
|
|
|
sso_provider_not_found: "SSO 제공자를 찾을 수 없습니다.",
|
|
|
|
|
|
too_many_enrolled_mfa_factors: "등록 가능한 MFA 요인 수를 초과했습니다.",
|
|
|
|
|
|
unexpected_audience: "토큰 audience가 일치하지 않습니다.",
|
|
|
|
|
|
unexpected_failure: "인증 서버 오류가 발생했습니다.",
|
|
|
|
|
|
user_already_exists: "이미 존재하는 사용자입니다.",
|
|
|
|
|
|
user_banned: "계정이 일시적으로 차단되었습니다.",
|
|
|
|
|
|
user_not_found: "사용자를 찾을 수 없습니다.",
|
|
|
|
|
|
user_sso_managed: "SSO 사용자 정보는 수정할 수 없습니다.",
|
|
|
|
|
|
validation_failed: "요청 값 형식이 올바르지 않습니다.",
|
|
|
|
|
|
weak_password: "비밀번호가 정책을 만족하지 않습니다.",
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
export const AUTH_ERROR_STATUS_MESSAGES = {
|
|
|
|
|
|
403: "요청한 기능을 사용할 수 없습니다.",
|
|
|
|
|
|
422: "요청을 처리할 수 없는 상태입니다.",
|
|
|
|
|
|
429: "요청이 너무 많습니다. 잠시 후 다시 시도해주세요.",
|
|
|
|
|
|
500: "인증 서버 오류가 발생했습니다.",
|
|
|
|
|
|
501: "요청한 기능이 활성화되어 있지 않습니다.",
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
2026-02-03 10:51:22 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 라우트 경로 상수
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 인증 관련 라우트 경로
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const AUTH_ROUTES = {
|
|
|
|
|
|
LOGIN: "/login",
|
|
|
|
|
|
SIGNUP: "/signup",
|
|
|
|
|
|
FORGOT_PASSWORD: "/forgot-password",
|
|
|
|
|
|
RESET_PASSWORD: "/reset-password",
|
|
|
|
|
|
AUTH_CONFIRM: "/auth/confirm",
|
|
|
|
|
|
AUTH_CALLBACK: "/auth/callback",
|
|
|
|
|
|
HOME: "/",
|
2026-02-05 16:36:42 +09:00
|
|
|
|
DASHBOARD: "/dashboard",
|
2026-02-03 10:51:22 +09:00
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 로그인 없이 접근 가능한 페이지 목록
|
|
|
|
|
|
* 미들웨어에서 라우트 보호에 사용
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const PUBLIC_AUTH_PAGES = [
|
|
|
|
|
|
AUTH_ROUTES.LOGIN,
|
|
|
|
|
|
AUTH_ROUTES.SIGNUP,
|
|
|
|
|
|
AUTH_ROUTES.FORGOT_PASSWORD,
|
|
|
|
|
|
AUTH_ROUTES.RESET_PASSWORD,
|
|
|
|
|
|
AUTH_ROUTES.AUTH_CONFIRM,
|
|
|
|
|
|
AUTH_ROUTES.AUTH_CALLBACK,
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
2026-02-05 09:38:42 +09:00
|
|
|
|
// 복구 플로우 전용 쿠키 (비밀번호 재설정 화면 외 접근 차단에 사용)
|
|
|
|
|
|
export const RECOVERY_COOKIE_NAME = "sb-recovery";
|
|
|
|
|
|
export const RECOVERY_COOKIE_MAX_AGE_SECONDS = 10 * 60;
|
|
|
|
|
|
|
2026-02-03 10:51:22 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 검증 규칙 상수
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 비밀번호 검증 규칙
|
2026-02-04 09:35:15 +09:00
|
|
|
|
* - 최소 8자 이상
|
|
|
|
|
|
* - 대문자 1개 이상
|
|
|
|
|
|
* - 소문자 1개 이상
|
|
|
|
|
|
* - 숫자 1개 이상
|
|
|
|
|
|
* - 특수문자 1개 이상
|
2026-02-03 10:51:22 +09:00
|
|
|
|
*/
|
|
|
|
|
|
export const PASSWORD_RULES = {
|
2026-02-04 09:35:15 +09:00
|
|
|
|
MIN_LENGTH: 8,
|
|
|
|
|
|
REQUIRE_UPPERCASE: true,
|
|
|
|
|
|
REQUIRE_LOWERCASE: true,
|
2026-02-03 10:51:22 +09:00
|
|
|
|
REQUIRE_NUMBER: true,
|
|
|
|
|
|
REQUIRE_SPECIAL_CHAR: true,
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
2026-02-06 10:43:16 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 세션 관련 상수
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 세션 타임아웃 시간 (밀리초)
|
|
|
|
|
|
* 환경 변수에서 분 단위를 가져와 밀리초로 변환합니다.
|
|
|
|
|
|
* 기본값: 30분
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const SESSION_TIMEOUT_MS =
|
|
|
|
|
|
(Number(process.env.NEXT_PUBLIC_SESSION_TIMEOUT_MINUTES) || 30) * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
// 경고 표시 시간 (타임아웃 1분 전)
|
|
|
|
|
|
export const SESSION_WARNING_MS = 60 * 1000;
|
|
|
|
|
|
|
2026-02-03 10:51:22 +09:00
|
|
|
|
// ========================================
|
|
|
|
|
|
// 타입 정의
|
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 인증 폼 데이터 타입
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type AuthFormData = {
|
|
|
|
|
|
email: string;
|
|
|
|
|
|
password: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 인증 에러 타입
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type AuthError = {
|
|
|
|
|
|
message: string;
|
|
|
|
|
|
type: "validation" | "auth" | "unknown";
|
|
|
|
|
|
};
|