Matrix · 계정 권한 구조

Matrix 계정 구조 개편안

지금은 권한을 브랜드 단위로만 줄 수 있어요. 개편 후에는 브랜드·에이전시·리테일이 각자 필요한 범위만 보도록 권한 단위를 나눠요. 앞부분은 개편 개요, 뒷부분은 개발팀용 권한 모델 명세예요.

리테일 권한 부여가 번거로움자기 리테일만 보려 해도 브랜드마다 하나씩 권한을 줘야 해요.
다른 리테일이 섞이면 권한 부여 불가브랜드 계정을 통째로 열면 다른 리테일 성과까지 보이기 때문이에요.
에이전시 대행 현황이 안 보임에이전시는 대행 계정을 한번에 관리 못 하고, 매드업도 에이전시별 대행 규모를 알기 어려워요.

등장하는 주체

권한은 이 네 주체를 기준으로 정해요.

매드업

어드민

  • 모든 계정 조회·편집
  • 에이전시별 대행 계정 수 집계
브랜드사

소유자 · 담당자

  • 소유자: 자기 브랜드 계정 전부 조회
  • 소유자: 에이전시 추가·제외, 담당자 배정
  • 담당자: 배정받은 브랜드 계정 조회
에이전시

소유자가 정한 권한

  • 대행 중인 브랜드 계정 전체 접근
  • 편집·조회 여부는 소유자가 지정
  • 이전 기간 성과까지 조회
리테일

뷰어 권한

  • 자기 리테일 광고 계정만 조회
  • 열람 동의 없이는 계정 생성 불가

누가 무엇을 보나

아래 버튼을 누르면 그 주체에게 보이는 계정만 강조돼요. 브랜드 A~D는 서로 별개인 브랜드 계정이에요.

전체 구조브랜드 → 브랜드 계정 → 리테일 광고 계정 순으로 종속돼요.
브랜드 A에이전시 X
카카오톡 선물하기
브랜드 B에이전시 Y
오늘의집
W컨셉
브랜드 C에이전시 X
카카오톡 선물하기
오늘의집
W컨셉
브랜드 D에이전시 Y
카카오톡 선물하기
W컨셉

권한 규칙

주체별로 보는 범위, 할 수 있는 일, 권한을 받는 방법이에요.

주체보는 범위권한권한을 받는 방법
어드민모든 브랜드 계정·광고 계정편집매드업 내부 계정
브랜드 소유자자기 브랜드 계정의 모든 광고 계정편집 + 관리브랜드 계정당 지정
브랜드 담당자배정받은 브랜드 계정의 모든 광고 계정소유자가 정함소유자가 배정
에이전시대행 중인 브랜드 계정 전체 (이전 기간 성과 포함)소유자가 정함소유자가 직접 추가·제외
리테일각 브랜드 계정 안의 자기 리테일 광고 계정뷰어계정 생성 시 열람 동의 (필수)

이럴 땐 이렇게

운영하면서 자주 생길 상황이에요.

한 브랜드를 리테일별로 다른 에이전시가 대행해요
에이전시별로 별개의 브랜드 계정을 만들어요(A-1, A-2). 둘은 서로 연결되지 않고, 에이전시 권한은 항상 브랜드 계정 하나를 통째로 줘요.
에이전시가 바뀌었어요
소유자가 이전 에이전시를 제외하고 새 에이전시를 추가해요. 새 에이전시는 이전 기간 성과까지 볼 수 있어요.
리테일이 자기 성과를 보고 싶어해요
계정을 만들 때 이미 열람에 동의했으니 별도 요청 없이 자기 리테일 광고 계정만 뷰어로 보여요.
매드업이 에이전시 대행 현황을 보고 싶어요
에이전시가 연결된 브랜드 계정 수를 바로 집계해요. 예시에서는 에이전시 X가 브랜드 2개(A, C)를 대행해요.

지금과 무엇이 달라지나

지금
개편 후
권한 단위는 브랜드 하나뿐
브랜드 계정 → 리테일 광고 계정, 두 단계로 권한을 줌
리테일에 브랜드마다 하나씩 권한 부여
열람 동의만 하면 자기 리테일 계정만 자동으로 보임
다른 리테일이 섞인 브랜드는 권한 부여 불가
리테일 광고 계정 단위로 보이므로 다른 리테일은 처음부터 안 보임
에이전시가 브랜드별로 따로 관리
대행 중인 브랜드 계정을 한곳에서 관리
에이전시별 대행 규모 파악이 어려움
에이전시–브랜드 계정 연결이 데이터로 남아 바로 집계
2부 · 개발 명세

권한 모델 상세

위 개편안을 개발 관점에서 정리했어요. 데이터 모델, 권한 판정 규칙, 지켜야 할 불변 조건, 개발팀이 확인해 줘야 할 질문이에요.

권한 경로는 3개브랜드 멤버십, 에이전시 위임, 리테일 매칭. 어드민은 모든 경로를 건너뛰어요.
위임 단위는 브랜드 계정에이전시는 브랜드 계정 통째로만 받아요. 광고 계정 하나만 따로 주는 경우는 없어요.
권한 값은 소유자가 정함담당자와 에이전시의 편집·조회 여부는 브랜드 소유자가 지정해요. 리테일은 항상 조회만 해요.

테이블·필드 이름은 이해를 돕기 위한 제안이에요. 현재 Matrix 스키마와 다를 수 있으니 실제 구현 이름으로 바꿔 읽어 주세요.

엔티티

권한 판정에 필요한 최소 단위만 적었어요. 성과 데이터는 ad_account에 붙어 있다고 가정해요.

엔티티뜻주요 필드
brand_account브랜드 계정. 서로 독립이며 상하 관계가 없어요.id, name, owner_user_id
retailer리테일 마스터 (카카오톡 선물하기, 오늘의집, W컨셉 …)id, name
ad_account리테일 광고 계정 = 브랜드 계정 × 리테일id, brand_account_id, retailer_id, retail_consented_at (NOT NULL)
agency에이전시 조직id, name
user로그인 사용자. 소속 유형이 하나로 정해져요.id, user_type (madup | brand | agency | retail), agency_id?, retailer_id?
brand_member브랜드 계정의 소유자·담당자brand_account_id, user_id, role (owner | member), permission (edit | view)
agency_grant에이전시 위임. 해제해도 행은 남겨요(이력·집계용).id, brand_account_id, agency_id, permission (edit | view), granted_by, granted_at, revoked_at?

관계도

브랜드 계정이 중심이고, 사용자는 세 경로 중 하나로 광고 계정에 닿아요.

erDiagram
  BRAND_ACCOUNT ||--o{ AD_ACCOUNT : "소유"
  RETAILER ||--o{ AD_ACCOUNT : "판매처"
  BRAND_ACCOUNT ||--o{ BRAND_MEMBER : "멤버"
  USER ||--o{ BRAND_MEMBER : "소속"
  BRAND_ACCOUNT ||--o{ AGENCY_GRANT : "위임"
  AGENCY ||--o{ AGENCY_GRANT : "위임받음"
  AGENCY ||--o{ USER : "직원"
  RETAILER ||--o{ USER : "직원"
  BRAND_ACCOUNT {
    bigint id PK
    string name
    bigint owner_user_id FK
  }
  AD_ACCOUNT {
    bigint id PK
    bigint brand_account_id FK
    bigint retailer_id FK
    timestamp retail_consented_at
  }
  BRAND_MEMBER {
    bigint brand_account_id FK
    bigint user_id FK
    string role
    string permission
  }
  AGENCY_GRANT {
    bigint id PK
    bigint brand_account_id FK
    bigint agency_id FK
    string permission
    timestamp granted_at
    timestamp revoked_at
  }
  USER {
    bigint id PK
    string user_type
    bigint agency_id FK
    bigint retailer_id FK
  }

권한 판정 로직

광고 계정 하나에 대해 사용자의 권한을 구해요. 결과는 edit, view, null(안 보임) 중 하나예요. edit은 view를 포함해요.

function access(user, ad):            // → 'edit' | 'view' | null
  if user.user_type == 'madup':  return 'edit'            // 어드민

  m = brand_member(ad.brand_account_id, user.id)
  if m:                                                 // ① 브랜드 멤버십
    return m.role == 'owner' ? 'edit' : m.permission

  if user.user_type == 'agency':                        // ② 에이전시 위임
    g = agency_grant(ad.brand_account_id, user.agency_id, revoked_at IS NULL)
    if g: return g.permission                             // 기간 필터 없음: 위임 전 성과도 보임

  if user.user_type == 'retail' and user.retailer_id == ad.retailer_id:
    return 'view'                                        // ③ 리테일 매칭 (동의는 생성 시 필수)

  return null

function canManage(user, brand_account_id):         // 에이전시 추가·제외, 담당자 배정, 권한 값 지정
  return user.user_type == 'madup'
      or brand_member(brand_account_id, user.id).role == 'owner'

권한 매트릭스

한 브랜드 계정을 기준으로 주체별로 할 수 있는 일이에요.

주체성과 조회광고 편집에이전시 추가·제외권한 값 지정담당자 배정
어드민가능가능가능가능가능
브랜드 소유자가능가능가능가능가능
브랜드 담당자가능소유자 지정불가불가불가
에이전시가능소유자 지정불가불가불가
리테일자기 리테일만불가불가불가불가
가능소유자 지정 소유자가 편집 또는 조회로 정함불가

불변 조건

어떤 경로로 데이터를 바꿔도 깨지면 안 되는 규칙이에요. DB 제약이나 서비스 검증으로 막아 주세요.

INV-1브랜드 계정마다 소유자는 정확히 1명이에요. 소유자가 없는 브랜드 계정은 만들 수 없어요.
INV-2광고 계정은 리테일 열람 동의가 있어야만 생성돼요. retail_consented_at은 NOT NULL이고, 동의 철회 흐름은 없어요.
INV-3브랜드 계정끼리는 상하 관계가 없어요(parent_id 없음). 같은 브랜드라도 운영이 나뉘면 별개 브랜드 계정으로 만들어요.
INV-4에이전시 위임 단위는 브랜드 계정 전체예요. 광고 계정 단위 위임은 없어요.
INV-5브랜드 계정당 활성 에이전시는 최대 1개예요. 리테일마다 대행사가 다르면 브랜드 계정을 나눠요. 활성 위임에 부분 UNIQUE (brand_account_id) WHERE revoked_at IS NULL을 걸어 주세요.
INV-6리테일 사용자는 자기 retailer_id의 광고 계정만, 항상 view로 봐요.
INV-7에이전시 접근에는 기간 필터가 없어요. 위임 전 성과까지 보이고, revoked_at이 찍히는 순간 접근이 끊겨요.
INV-8담당자와 에이전시의 permission 값은 소유자(또는 어드민)만 바꿀 수 있어요.

주요 시나리오와 데이터 변화

에이전시 교체 (X → Z)

  1. 소유자가 X의 agency_grant에 revoked_at 기록
  2. Z로 새 agency_grant 생성, 권한 값 지정
  3. X는 즉시 접근 불가, Z는 과거 성과까지 조회

리테일마다 대행사가 다름

  1. 브랜드 계정을 새로 만듦 (예: 브랜드 A-1, A-2)
  2. 각 브랜드 계정에 해당 리테일 광고 계정을 둠
  3. 각각 다른 에이전시에 위임

새 리테일 광고 계정 개설

  1. 리테일 열람 동의 확인 후 ad_account 생성
  2. 해당 리테일 사용자에게 자동으로 view
  3. 활성 에이전시가 있으면 자동으로 위임 범위에 포함

담당자 추가

  1. 소유자가 brand_member를 role = member로 추가
  2. permission을 edit 또는 view로 지정
  3. 담당자는 브랜드 계정 안 모든 광고 계정에 접근

참고 쿼리

목록 화면과 어드민 집계에 바로 쓸 수 있는 형태예요.

에이전시 사용자가 볼 수 있는 광고 계정

SELECT a.*, g.permission
FROM ad_account a
JOIN agency_grant g ON g.brand_account_id = a.brand_account_id
WHERE g.agency_id = :agency_id AND g.revoked_at IS NULL;

리테일 사용자가 볼 수 있는 광고 계정

SELECT * FROM ad_account WHERE retailer_id = :retailer_id;

에이전시별 대행 브랜드 계정 수 (어드민)

SELECT agency_id, COUNT(DISTINCT brand_account_id) AS brand_accounts
FROM agency_grant
WHERE revoked_at IS NULL
GROUP BY agency_id;

개발팀 확인 질문

기획에서 아직 정하지 않았거나, 구현 방식에 따라 달라지는 부분이에요.

  1. 에이전시·리테일 조직 안의 사용자 범위.위임이 그 조직 직원 전원에게 적용되는지, 조직 안에서 다시 사람별로 배정해야 하는지.
  2. 광고 계정 유일성.같은 브랜드 계정·리테일 조합에 광고 계정이 2개 이상 생길 수 있는지. 아니라면 (brand_account_id, retailer_id) UNIQUE.
  3. 광고 계정을 만드는 주체.소유자, 어드민, 에이전시 중 누가 생성할 수 있는지.
  4. 브랜드 계정 분리 시 기존 광고 계정 이동.A를 A-1, A-2로 나눌 때 광고 계정과 성과 이력을 옮기는 방법.
  5. 기존 권한 이관.지금 브랜드 단위로 준 권한을 소유자·담당자·에이전시 중 무엇으로 옮길지 매핑 기준.
  6. 권한 변경 이력.위임·해제·권한 값 변경을 감사 로그로 남길지.