로그인/권한 정책 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는 아래 순서로 검사한다.
- 인증 여부 확인
- 사용자 상태 확인
- 본사:
headquarters_users.status = ACTIVE - 사무소:
office_users.status = ACTIVE,offices.status = ACTIVE
- 본사:
- 요청 리소스의
officeId확인 - actor 타입별 접근 범위 확인
- 역할 기본 권한 확인
permission_groups.permissionsJson의 기능별 권한 확인- 사무소 기능 플래그 확인
- 예: 공동중개, 고객 예약 팝업, 고급 PDF, 대량 엑셀
- 개인정보/내보내기/권한/과금 변경이면
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.officeIdcreatedByUserId = session.userIdmanagerUserId기본값은 세션 사용자- 오너 관리자는 같은 사무소의 다른 활성 사용자를 담당자로 지정 가능
수정:
- 오너 관리자: 자기 사무소 고객 수정 가능
- 중개인/중개보조인: 본인 담당 고객만 수정 가능
- 담당자 변경은 오너 관리자 또는
customers.assign권한을 가진 사용자만 가능
엑셀 업로드/다운로드:
BULK_EXCEL_UPLOAD기능이office_features.enabled = true이거나 본사관리자일 때 허용- 다운로드는 개인정보 내보내기이므로
audit_logs에EXPORT기록
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_logs에BILLING_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
기능 사용 가능 여부는 두 단계를 모두 통과해야 한다.
- 사무소에 기능이 켜져 있음:
office_features.enabled = true - 사용자 권한 그룹에서 해당 기능 동작이 허용됨:
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_features와permission_groups를 모두 확인한다. - 개인정보가 포함된 응답은 필요한 필드만 반환하고, 상세 조회/내보내기는 로그를 남긴다.
- 비활성 사용자와 정지 사무소는 로그인과 API 접근을 차단한다.
- 과금 정책과 기능 과금은 update가 아니라 이력 추가 방식으로 변경한다.