상세 설계 문서

로그인/권한 정책

로그인/권한 정책 v1

1. 목표

부동산 계약/고객/매물 SaaS MVP에서 본사관리자, 부동산 오너 관리자, 중개인, 중개보조인의 로그인 영역과 데이터 접근 범위를 명확히 분리한다. 모든 API와 DB 조회는 이 문서의 권한 체크 규칙을 기본값으로 사용한다.

2. 인증 영역

2.1 본사관리자 인증

  • 로그인 대상: headquarters_users
  • 로그인 경로: 본사관리자 전용 로그인 화면
  • 세션 주체: actorType = HEADQUARTERS_USER, headquartersUserId
  • 허용 역할: SUPER_ADMIN, ADMIN, BILLING_MANAGER, SUPPORT
  • 접근 범위: 전체 사무소 운영/과금/기능 설정. 단, 개인정보 상세 조회는 감사 로그 대상이다.

본사관리자는 office_users 계정으로 취급하지 않는다. 사무소 업무 화면에 진입해야 할 때도 본사 세션으로 접근하며, 대상 officeId를 명시하고 감사 로그를 남긴다.

2.2 부동산 사용자 인증

  • 로그인 대상: office_users
  • 로그인 경로: 부동산 사용자 전용 로그인 화면
  • 세션 주체: actorType = OFFICE_USER, officeUserId, officeId, role, permissionGroupId
  • 허용 역할: OWNER_ADMIN, AGENT, ASSISTANT
  • 접근 범위: 자기 officeId 안의 데이터. 역할과 권한 그룹에 따라 사무소 전체 또는 본인 담당 데이터로 제한한다.

부동산 사용자는 다른 사무소의 데이터에 접근할 수 없다. 공동중개 공개 매물처럼 공유 정책이 있는 데이터만 별도 규칙으로 조회할 수 있다.

3. 역할별 기본 범위

역할 기본 데이터 범위 사용자 관리 과금/기능 비고
본사관리자 전체 사무소 운영 데이터 전체 사용자 관리 전체 과금/기능 설정 개인정보 상세 조회는 로그 필요
부동산 오너 관리자 자기 사무소 전체 데이터 자기 사무소 사용자 관리 자기 사무소 이용 현황 조회 본사 설정값 변경 불가
중개인 자기 작성/담당 데이터 본인 정보 중심 조회 불가 기능 권한에 따라 일부 확장 가능
중개보조인 자기 작성/담당 데이터 본인 정보 중심 조회 불가 기본은 중개인보다 제한적으로 운영

4. 공통 권한 체크 순서

모든 API는 아래 순서로 검사한다.

  1. 인증 여부 확인
  2. 사용자 상태 확인
    • 본사: headquarters_users.status = ACTIVE
    • 사무소: office_users.status = ACTIVE, offices.status = ACTIVE
  3. 요청 리소스의 officeId 확인
  4. actor 타입별 접근 범위 확인
  5. 역할 기본 권한 확인
  6. permission_groups.permissionsJson의 기능별 권한 확인
  7. 사무소 기능 플래그 확인
    • 예: 공동중개, 고객 예약 팝업, 고급 PDF, 대량 엑셀
  8. 개인정보/내보내기/권한/과금 변경이면 audit_logs 기록

권한 실패 응답 원칙:

  • 인증 없음: 401
  • 인증은 되었으나 범위/권한 없음: 403
  • 다른 사무소 리소스 접근 시 존재 여부를 숨겨야 하는 API: 404 또는 403 중 보안 정책에 맞춰 통일

5. 데이터별 조회 규칙

5.1 계약

테이블: contracts

역할 조회 조건
본사관리자 제한 없음. 필요 시 officeId 필터
부동산 오너 관리자 contracts.officeId = session.officeId
중개인 contracts.officeId = session.officeId AND (writerUserId = session.userId OR managerUserId = session.userId)
중개보조인 contracts.officeId = session.officeId AND (writerUserId = session.userId OR managerUserId = session.userId)

생성:

  • 사무소 사용자만 생성 가능
  • officeId는 클라이언트 입력값을 신뢰하지 않고 세션의 officeId로 설정한다.
  • writerUserId는 세션 사용자로 설정한다.
  • managerUserId는 기본적으로 세션 사용자이며, 오너 관리자는 같은 사무소의 다른 활성 사용자로 지정할 수 있다.

수정:

  • 오너 관리자: 자기 사무소 계약 수정 가능
  • 중개인/중개보조인: 본인이 작성/담당한 계약만 수정 가능
  • 완료/취소/아카이브 상태 변경은 contracts.updateStatus 권한이 필요하다.

삭제:

  • MVP에서는 물리 삭제 금지
  • 오너 관리자 또는 작성자가 deletedAt 설정 가능하되, 권한 그룹에서 delete가 true여야 한다.

계약 버전:

  • 계약 수정 시 기존 contract_versions를 덮어쓰지 않고 새 버전을 추가한다.
  • PDF 재출력은 해당 계약을 조회할 수 있는 사용자만 가능하다.

5.2 고객

테이블: customers

역할 조회 조건
본사관리자 제한 없음. 개인정보 상세 조회 시 로그
부동산 오너 관리자 customers.officeId = session.officeId
중개인 customers.officeId = session.officeId AND managerUserId = session.userId
중개보조인 customers.officeId = session.officeId AND managerUserId = session.userId

생성:

  • 사무소 사용자만 생성 가능
  • officeId = session.officeId
  • createdByUserId = session.userId
  • managerUserId 기본값은 세션 사용자
  • 오너 관리자는 같은 사무소의 다른 활성 사용자를 담당자로 지정 가능

수정:

  • 오너 관리자: 자기 사무소 고객 수정 가능
  • 중개인/중개보조인: 본인 담당 고객만 수정 가능
  • 담당자 변경은 오너 관리자 또는 customers.assign 권한을 가진 사용자만 가능

엑셀 업로드/다운로드:

  • BULK_EXCEL_UPLOAD 기능이 office_features.enabled = true이거나 본사관리자일 때 허용
  • 다운로드는 개인정보 내보내기이므로 audit_logsEXPORT 기록

5.3 매물

테이블: listings

역할 조회 조건
본사관리자 제한 없음. 필요 시 officeId 필터
부동산 오너 관리자 listings.officeId = session.officeId
중개인 listings.officeId = session.officeId AND (managerUserId = session.userId OR createdByUserId = session.userId)
중개보조인 listings.officeId = session.officeId AND (managerUserId = session.userId OR createdByUserId = session.userId)

사무소 내부 공개:

  • visibility = OFFICE인 매물은 같은 사무소 사용자가 목록에서 볼 수 있다.
  • 단, 수정/삭제는 오너 관리자 또는 담당자/등록자만 가능하다.

비공개:

  • visibility = PRIVATE인 매물은 오너 관리자, 담당자, 등록자만 조회 가능하다.

공동중개 공개:

  • 기본 조건: visibility = EXCHANGE AND exchangeEnabled = true AND status = ACTIVE
  • 사무소가 LISTING_EXCHANGE 기능을 사용할 수 있어야 한다.
  • listing_exchanges.sharedToOfficeId is null AND status = APPROVED이면 전체 공개 후보
  • listing_exchanges.sharedToOfficeId = session.officeId AND status = APPROVED이면 특정 사무소 공개
  • 외부 사무소 사용자는 소유 사무소의 내부 메모, 담당자 개인 정보, 비공개 가격 메모를 볼 수 없다.

수정:

  • 오너 관리자: 자기 사무소 매물 수정 가능
  • 중개인/중개보조인: 본인 담당/등록 매물만 수정 가능
  • 공동중개 공개 상태 변경은 오너 관리자 또는 listings.exchange 권한 필요

5.4 사용자

테이블: office_users

역할 조회 조건
본사관리자 전체 조회 가능
부동산 오너 관리자 office_users.officeId = session.officeId
중개인 자기 사용자 정보만
중개보조인 자기 사용자 정보만

생성:

  • 본사관리자: 사무소 오너 관리자 생성 가능
  • 부동산 오너 관리자: 자기 사무소의 중개인/중개보조인 생성 가능
  • 중개인/중개보조인: 생성 불가

수정:

  • 오너 관리자는 자기 사무소 사용자의 이름, 연락처, 역할, 상태, 권한 그룹, 과금 제외 여부를 관리할 수 있다.
  • 오너 관리자가 자기 자신의 OWNER_ADMIN 역할을 제거하거나 비활성화하려면 사무소에 다른 활성 오너 관리자가 있어야 한다.
  • maxUserLimit 초과 생성은 기본적으로 차단한다. 운영 정책상 초과 허용이 필요하면 생성은 허용하되 billing_monthly_usages에서 초과 인원으로 계산한다.

5.5 과금

테이블: office_billing_policies, office_features, billing_monthly_usages

역할 허용 범위
본사관리자 전체 과금 정책 생성/수정/확정
부동산 오너 관리자 자기 사무소 이용 현황과 예상 금액 조회
중개인 조회 불가
중개보조인 조회 불가

정책 변경:

  • 본사관리자만 가능
  • 과거 정책을 덮어쓰지 않고 기존 row의 effectiveTo를 닫고 새 row를 생성한다.
  • 변경 시 audit_logsBILLING_CHANGE 기록

월 과금 계산:

billableUserCount = office_users where officeId = targetOfficeId
  and status = ACTIVE
  and billable = true
  and role in (OWNER_ADMIN, AGENT, ASSISTANT)

extraUserCount = max(0, billableUserCount - includedUserLimit)
extraUserAmount = extraUserCount * extraUserFee
featureAmount = sum(enabled office_features.monthlyFee)
totalAmount = baseMonthlyFee + extraUserAmount + featureAmount

billingEnabled = false이면 totalAmount는 예상 금액으로 표시하고 실제 청구 대상으로 처리하지 않는다.

5.6 기능 권한

테이블: features, office_features, permission_groups

기능 사용 가능 여부는 두 단계를 모두 통과해야 한다.

  1. 사무소에 기능이 켜져 있음: office_features.enabled = true
  2. 사용자 권한 그룹에서 해당 기능 동작이 허용됨: permissionsJson

예외:

  • 본사관리자는 사무소 기능 설정 화면에서 기능 플래그와 금액을 관리할 수 있다.
  • 오너 관리자는 기능 요청은 할 수 있으나 office_features를 직접 변경할 수 없다.

6. 역할별 메뉴 접근

메뉴 본사관리자 오너 관리자 중개인 중개보조인
본사 대시보드 Y N N N
부동산 등록/관리 Y N N N
전체 사용자 관리 Y N N N
과금 설정 Y N N N
기능 권한 설정 Y N N N
업무 대시보드 N Y, 사무소 전체 Y, 본인 기준 Y, 본인 기준
계약하기 N Y Y 권한 그룹에 따름
계약관리 운영 조회 Y, 사무소 전체 Y, 본인 기준 Y, 본인 기준
고객관리 운영 조회 Y, 사무소 전체 Y, 본인 기준 Y, 본인 기준
매물관리 운영 조회 Y, 사무소 전체 Y, 본인 기준 Y, 본인 기준
사용자관리 Y Y, 자기 사무소 본인 정보 본인 정보
사무소 이용 현황 Y Y, 자기 사무소 N N

7. API 필터 패턴

7.1 사무소 사용자 목록 조회

function officeScopeWhere(session) {
  if (session.actorType === "HEADQUARTERS_USER") return {};

  if (session.role === "OWNER_ADMIN") {
    return { officeId: session.officeId };
  }

  throw new ForbiddenError();
}

7.2 계약 목록 조회

function contractReadWhere(session) {
  if (session.actorType === "HEADQUARTERS_USER") return {};

  const base = { officeId: session.officeId, deletedAt: null };

  if (session.role === "OWNER_ADMIN") return base;

  return {
    ...base,
    OR: [
      { writerUserId: session.userId },
      { managerUserId: session.userId }
    ]
  };
}

7.3 고객 목록 조회

function customerReadWhere(session) {
  if (session.actorType === "HEADQUARTERS_USER") return {};

  const base = { officeId: session.officeId, deletedAt: null };

  if (session.role === "OWNER_ADMIN") return base;

  return { ...base, managerUserId: session.userId };
}

7.4 매물 목록 조회

function listingReadWhere(session) {
  if (session.actorType === "HEADQUARTERS_USER") return {};

  const base = { officeId: session.officeId, deletedAt: null };

  if (session.role === "OWNER_ADMIN") return base;

  return {
    ...base,
    OR: [
      { managerUserId: session.userId },
      { createdByUserId: session.userId },
      { visibility: "OFFICE" }
    ]
  };
}

visibility = OFFICE로 조회된 매물은 수정 권한과 별개다. 수정 API는 반드시 담당자/등록자/오너 관리자 조건을 다시 검사한다.

8. 권한 그룹 기본값

8.1 오너 관리자 기본

{
  "contracts": { "read": "office", "create": true, "update": "office", "delete": true, "export": true },
  "customers": { "read": "office", "create": true, "update": "office", "delete": true, "assign": true, "export": true },
  "listings": { "read": "office", "create": true, "update": "office", "delete": true, "exchange": true, "export": true },
  "users": { "read": "office", "create": true, "update": true, "deactivate": true },
  "billing": { "read": "office" },
  "features": { "request": true, "manage": false }
}

8.2 중개인 기본

{
  "contracts": { "read": "own", "create": true, "update": "own", "delete": false, "export": false },
  "customers": { "read": "own", "create": true, "update": "own", "delete": false, "assign": false, "export": false },
  "listings": { "read": "own_or_office_visible", "create": true, "update": "own", "delete": false, "exchange": false, "export": false },
  "users": { "read": "self", "create": false, "update": false },
  "billing": { "read": false },
  "features": { "request": true, "manage": false }
}

8.3 중개보조인 기본

{
  "contracts": { "read": "own", "create": true, "update": "own", "delete": false, "export": false },
  "customers": { "read": "own", "create": true, "update": "own", "delete": false, "assign": false, "export": false },
  "listings": { "read": "own_or_office_visible", "create": true, "update": "own", "delete": false, "exchange": false, "export": false },
  "users": { "read": "self", "create": false, "update": false },
  "billing": { "read": false },
  "features": { "request": false, "manage": false }
}

8.4 조회 전용

{
  "contracts": { "read": "own", "create": false, "update": false, "delete": false, "export": false },
  "customers": { "read": "own", "create": false, "update": false, "delete": false, "export": false },
  "listings": { "read": "own_or_office_visible", "create": false, "update": false, "delete": false, "exchange": false, "export": false },
  "users": { "read": "self", "create": false, "update": false },
  "billing": { "read": false },
  "features": { "request": false, "manage": false }
}

9. 감사 로그 대상

아래 작업은 audit_logs에 기록한다.

  • 로그인 성공/실패
  • 본사관리자의 사무소 생성/수정/정지
  • 본사관리자의 사무소 사용자 생성/역할 변경/상태 변경
  • 오너 관리자의 사무소 사용자 생성/역할 변경/상태 변경
  • 권한 그룹 생성/수정/삭제
  • 과금 정책 변경, 월 과금 확정
  • 기능 권한 부여/회수/금액 변경
  • 고객 개인정보 상세 조회
  • 계약 당사자 개인정보 상세 조회
  • 계약 PDF 생성/인쇄
  • 계약/고객/매물 엑셀 다운로드
  • 공동중개 공개 승인/철회

최소 기록 필드:

  • actor type/id
  • target officeId
  • action
  • resource type/id
  • IP, User-Agent
  • 변경 전후 요약 또는 metadata

10. 구현 체크리스트

  • 모든 사무소 업무 테이블 조회에 officeId 조건을 넣는다.
  • 클라이언트가 보낸 officeId, writerUserId, createdByUserId는 생성 API에서 신뢰하지 않는다.
  • 수정/삭제 API는 목록 조회 권한과 별도로 resource 단건을 읽고 권한을 다시 검사한다.
  • 본사관리자와 사무소 사용자 세션 타입을 구분한다.
  • 오너 관리자 권한으로도 다른 사무소 ID를 요청할 수 없게 한다.
  • 기능 버튼 노출은 office_featurespermission_groups를 모두 확인한다.
  • 개인정보가 포함된 응답은 필요한 필드만 반환하고, 상세 조회/내보내기는 로그를 남긴다.
  • 비활성 사용자와 정지 사무소는 로그인과 API 접근을 차단한다.
  • 과금 정책과 기능 과금은 update가 아니라 이력 추가 방식으로 변경한다.