Skip to content

Workspaces & Team Collaboration — 설계 문서 (v2)

상태: 확정 (2026-06-26) — v2: UI/UX 픽스 + 라우팅 컨벤션 확정 범위: maker-link 전반의 조직(Workspace)·권한(Role/ACL)·시트 과금·초대·캔버스 공유 선행 결정 근거: 채팅 논의 결과 정리. 캔버스 협업 기능(Review Request 등)과 Enterprise(SAML/SCIM 등)는 본 사이클 범위 외. v2 변경 요약: §7 UX 흐름을 §14로 확장(상세 와이어), §8에 부트스트랩/멤버 검색 엔드포인트 추가, §15 라우팅 컨벤션 확정(/w/{slug}), §16 실행 단계(체크리스트) 추가.


0. TL;DR

  • Workspace 통합 모델: 개인 = type:'personal' 워크스페이스 of one. 팀 전환은 type:'team'으로 업그레이드.
  • 2-레이어 권한: Workspace Role(Owner/Admin/Member/Billing/Guest) + Resource ACL(Manager/Editor/Commenter/Viewer).
  • Seat 회계: Editor seat만 유료. Billing/Guest는 0 seat.
  • RBAC: CASL.
  • 실시간 협업(향후): Yjs + y-websocket self-host (현 사이클엔 ACL/구조만).
  • 결제: Nicepay(KR) + Stripe(글로벌) 병행. workspace.billing.provider로 분기.
  • 로드맵: Phase 1+2 (Team까지) 본 사이클. Phase 3(캔버스 협업 기능), Phase 4(Enterprise)는 보류.
  • Enterprise 대비: 스키마 필드 자리만 예약(sso/dataRegion/scimExternalId/provisioningSource/externalId).

1. 벤치마킹 채택안

출처채택한 요소
GitHubPersonal account = Org of one, Outside Collaborator
FigmaEditor seat 유료 + Viewer/Commenter 무료
VercelOwner/Admin/Member/Billing 4-role 명확 분리
Linear/Slack워크스페이스 전환기, 초대 수락 흐름
Google Drive"일반 액세스" + 개별 ACL 정신모형 (캔버스 공유 모달)

2. 핵심 개념 다이어그램

Account (User)  ──belongs to *──▶  Membership  ──belongs to──▶  Workspace

                                                                    ├── Invitation (pending)
                                                                    ├── Subscription (seats, plan, billing)
                                                                    ├── AuditLog
                                                                    └── Project / ArchitectureSession 등 리소스

                                                                              └── ResourceAcl (캔버스 단위 공유)
  • Personal Account도 워크스페이스를 1개 자동 보유 (UI에선 "워크스페이스" 단어 숨김).
  • 모든 도메인 리소스(ArchitectureSession 등)는 workspaceId + createdBy로 귀속.

3. Role 정의

3-1. Workspace Role (사람 분류)

Role권한 요지Seat
Owner워크스페이스 삭제·소유권 이전·결제 포함 모든 권한1 (editor)
Admin멤버 초대/제거/역할 변경, 워크스페이스 설정, 결제 제외1 (editor)
Member캔버스/세션 생성·편집, 워크스페이스 내 리소스 사용1 (editor)
Billing Manager결제 정보·청구서·플랜만, 콘텐츠 접근 X0 (free)
Guest (Outside Collaborator)명시적으로 ACL에 추가된 리소스만 접근0 (free)

3-2. Resource ACL Role (캔버스/세션 단위)

Role허용
ManagerACL 변경·공유링크·삭제 (워크스페이스 Owner/Admin은 항상 Manager)
Editor노드 추가/이동, 산출물 수정, 라이브룸 트리거
Commenter핀 코멘트, @멘션 (수정 X)
Viewer보기·줌·팬·산출물 열람 (다운로드는 plan별)

3-3. 상속 규칙

  • 캔버스 생성 시: 생성자 = Manager, 워크스페이스 Owner/Admin = Manager.
  • "일반 액세스" 정책 3종:
    • workspace-members: 워크스페이스 멤버 전원에게 기본 role 부여(기본: Commenter)
    • invited-only: ACL entries에 명시된 사람만
    • link: 토큰 링크 소유자에게 기본 role 부여 (Viewer/Commenter)

4. Seat 회계 규칙

상황Seat 변화
초대 메일 발송 (pending)변동 없음
초대 수락 → Member/Admin/Owner 가입+1
Member ↔ Billing/Guest 강등−1 (다음 주기 prorated 환급)
멤버 제거−1
Seat 한도 초과 초대 시도차단 + "Seat 추가 구매" 모달
결제 실패7일 유예 → seat 초과분 read-only 전환(제거는 X)

prorated 변경은 Stripe·Nicepay 모두 즉시 반영.


5. 과금 모델 (현 사이클 확정 범위)

Personal (무료)
  - Editor 1 (본인), 캔버스 무제한 보기 링크
  - 팀 초대 불가

Team (Editor seat 단위 / 월)
  - Editor 1 이상부터
  - Billing Manager / Guest 무료
  - 캔버스 공유(워크스페이스/링크/개별) 전부 가능
  • Phase 3 도래 시 Team plan에 Review Request·버전·타임라인 등 캔버스 협업 기능 추가.
  • Business/Enterprise plan은 enum/필드만 예약, 영업 발생 시 활성화.

6. 데이터 모델 (Mongoose)

신규 5개 + 기존 1개 마이그레이션.

Workspace

ts
{
  _id, slug: string (unique), name, logoUrl?,
  type: 'personal' | 'team',
  plan: 'personal' | 'team',            // enum 예약: 'business' | 'enterprise'
  ownerId: ObjectId<User>,
  seatLimit: number,
  seatUsage: number,
  billing: {
    provider: 'nicepay' | 'stripe' | null,
    customerId?: string,
    subscriptionId?: string,
    company?: { name, businessNumber, taxEmail }
  },
  // Enterprise 예약 필드 (null/기본값)
  sso: null,                            // { provider, domain, enforced }
  dataRegion: 'kr',                     // 'global' 추가 예정
  scimExternalId: null,
  createdAt, updatedAt
}

Membership

ts
{
  _id, workspaceId, userId,
  role: 'owner' | 'admin' | 'member' | 'billing' | 'guest',
  seatType: 'editor' | 'free',
  invitedBy?: ObjectId<User>,
  joinedAt,
  // 예약 필드
  provisioningSource: 'manual' | 'scim' | 'domain-capture',  // 기본 'manual'
  externalId: null
}
// 인덱스: { workspaceId, userId } unique

Invitation

ts
{
  _id, workspaceId, email (lowercase),
  role, seatType,
  tokenHash,
  expiresAt (default +7d),
  invitedBy,
  status: 'pending' | 'accepted' | 'expired' | 'revoked',
  acceptedAt?, acceptedUserId?
}
// 인덱스: { workspaceId, email, status }

ResourceAcl

ts
{
  _id,
  resourceType: 'session',                // 향후 'project' 등 확장
  resourceId,
  workspaceId,
  generalAccess: 'workspace-members' | 'invited-only' | 'link',
  generalAccessRole: 'viewer' | 'commenter' | null,
  entries: [
    { subjectType: 'user'|'guest'|'team', subjectId, role: 'viewer'|'commenter'|'editor'|'manager' }
  ],
  publicLink: { tokenHash, role: 'viewer'|'commenter', expiresAt } | null,
  updatedAt
}
// 인덱스: { resourceType, resourceId } unique

AuditLog

ts
{
  _id, workspaceId, actorId, action,
  // workspace.created, member.invited, member.joined, member.removed,
  // seat.changed, billing.subscribed, billing.failed, plan.changed,
  // acl.changed, session.shared, link.created, link.revoked ...
  resource?: { type, id },
  before?, after?,
  ip?, ua?, at
}
// 인덱스: { workspaceId, at: -1 }

ArchitectureSession (마이그레이션)

  • 기존 user: ObjectId<User>{ workspaceId: ObjectId<Workspace>, createdBy: ObjectId<User> }
  • 모든 도큐먼트 백필: 사용자별 personal workspace 생성 → 매핑.

7. UX 흐름 (Step 1~4)

Step 1 — 팀 생성

  • "팀원 초대" / "팀으로 업그레이드" 버튼 어디서나 진입
  • 모달: 팀 이름, slug(자동제안), 로고(선택)
  • 본인이 Owner로 즉시 가입 (seat 1)

Step 2 — 결제

  • "몇 명이서 쓰실 건가요?" 슬라이더 (총 seat)
  • 회사정보(사업자번호/세금계산서 이메일) 입력 — 선택
  • Nicepay(KR) / Stripe(글로벌) 카드 등록 → 즉시 결제
  • workspace.seatLimit 확정

Step 3 — 멤버 초대

  • 이메일 다건 입력 + 각 row에 Role 선택 (Member/Admin/Billing)
  • "초대 메일 보내기" → 7일 유효 토큰 메일
  • 수신자: 비가입자 → 가입 후 자동 수락 / 가입자 → 워크스페이스 전환기에 추가
  • 수락 시점에 seat 차감

Step 4 — 캔버스 단위 공유 (구글 드라이브 패턴)

  • 캔버스 우상단 "공유" 모달:
    • 워크스페이스 멤버 추가 (역할 선택)
    • 게스트 이메일 추가 (외부)
    • 일반 액세스: workspace-members / invited-only / link
    • 공개 링크 발급/회전

화면별 와이어/컴포넌트 분해는 §14 UI/UX 상세 (Personal → Team) 에서 다룬다.


8. API 엔드포인트 (Phase 1+2 최소셋)

GET    /me/workspaces                    부트스트랩: 내 멤버십·pending 초대 카운트
GET    /workspaces/:id/members?q=        멤버 검색(공유 모달 autocomplete)

POST   /workspaces                       팀 생성
PATCH  /workspaces/:id                   설정 변경
POST   /workspaces/:id/subscription      결제·seat 구매
PATCH  /workspaces/:id/subscription      seat 증감, plan 변경
DELETE /workspaces/:id/subscription      해지

POST   /workspaces/:id/invitations       초대 발송(배열)
GET    /workspaces/:id/invitations       pending 목록
DELETE /workspaces/:id/invitations/:iid  취소
POST   /invitations/accept               { token } 수락

GET    /workspaces/:id/members
PATCH  /workspaces/:id/members/:uid      role/seatType 변경
DELETE /workspaces/:id/members/:uid      제거

POST   /sessions/:id/acl                 캔버스 ACL 설정
GET    /sessions/:id/acl
POST   /sessions/:id/share-link          공개 링크 발급/회전

GET    /workspaces/:id/audit-logs        감사 로그(기록은 항상, 노출은 plan별)

모든 라우트는 ability.can(action, subject) 게이트(CASL).


9. 초대 코너 케이스 정책

  1. 비가입자 → 가입(또는 OAuth) → 자동 수락
  2. 기존 다른 워크스페이스 멤버 → 워크스페이스 전환기에 추가 (Slack 패턴)
  3. 이미 같은 워크스페이스 멤버 → "이미 멤버" + 초대 무효
  4. 이메일 정규화: lowercase. +alias는 동일 인물로 취급하지 않음(악용 방지).
  5. 토큰 7일 만료, 만료 시 재발송 버튼
  6. 취소 토큰 즉시 무효
  7. Seat 한도 초과 상태 → 수락 차단 + 관리자 알림

10. 로드맵 & PR 분할

Phase 1 — Foundation (현 사이클)

#PR산출물
1모델 + 백필Workspace/Membership/Invitation/ResourceAcl/AuditLog 스키마, User → personal workspace 백필, ArchitectureSession 마이그레이션
2CASL 도입 + 권한 게이트buildAbilityFor, 미들웨어, 기존 라우트 ability.can 교체
3팀 생성 + 결제 (Step 1~2)/workspaces POST, 결제 UI(Nicepay 분기, Stripe 자리), seat 회계, 회사정보
4초대 + 수락 (Step 3)Invitation 모델 + 메일 발송 + 수락 페이지 + 워크스페이스 전환기
5캔버스 ACL 공유 모달 (Step 4)ResourceAcl API + 공유 모달 UI + 공개 링크
6워크스페이스 설정 페이지멤버/역할/결제/감사 로그 뷰

각 PR은 FEATURE_WORKSPACES 플래그로 점진 노출.

Phase 2 — Team Billing 안정화 (Phase 1과 일부 병행)

  • Stripe 평행 결제 연결, prorated upgrade/downgrade, 결제 실패 grace period.

Phase 3 — 캔버스 협업 기능 (보류)

  • Review Request, 버전 스냅샷, 활동 타임라인, Yjs presence/실시간.

Phase 4 — Enterprise (보류)

  • SAML/SCIM, 데이터 리전, 도메인 캡처, 온프레 Helm, BYOK.

11. Enterprise 대비 예약 자리 (지금 비워둘 것)

  • Workspace.plan enum에 'business' | 'enterprise' 등록 (사용 X)
  • Workspace.sso 필드 (null)
  • Workspace.dataRegion ('kr' default)
  • Workspace.scimExternalId (null)
  • Membership.provisioningSource ('manual' default)
  • Membership.externalId (null)
  • AuditLogPhase 1부터 실제 기록 시작 (영업 자산)

12. 진행 상황 & 다음 작업

완료

  • PR #1 — 모델 + 백필 (머지 대기)
    • 신규 모델: Workspace, Membership, Invitation, ResourceAcl, WorkspaceAuditLog
    • ArchitectureSessionworkspaceId / createdBy 옵셔널 필드 추가 (기존 user 유지)
    • 백필 스크립트폐기. 운영에 실유저가 없어 마이그레이션 대신 lazy 자동 보장으로 전환 (§12 참조).
  • PR #2 — CASL 권한 게이트 인프라 (머지 대기)
    • @casl/ability ^7.0.0 의존성 추가
    • apps/api/services/ability.jsbuildAbilityFor, subjectOnSession, subjectOnWorkspace, canActOnSession
    • apps/api/middleware/workspace.jswithWorkspace, requireAbility
    • 이관 가이드: docs/AUTHZ_CASL_GUIDE.md
    • 기존 라우트는 의도적으로 미교체 — 신규 라우트(PR #3~#5)부터 새 패턴 사용.

다음 — PR #3부터 재개

PR #3 — 팀 생성 + 결제 (Step 1~2) 범위:

  • POST /workspaces (팀 생성 + Owner Membership 트랜잭션)
  • PATCH /workspaces/:id (이름/로고/회사정보)
  • POST /workspaces/:id/subscription (Nicepay 분기, Stripe 자리만)
  • PATCH /workspaces/:id/subscription (seat 증감 prorated)
  • seat 회계 트랜잭션 (Membership 변경 ↔ workspace.seatUsage 일관성)
  • WorkspaceAuditLog 기록 시작 (workspace.created, seat.changed, billing.subscribed 등)
  • 프론트: 팀 생성 모달 + 결제 폼 (/settings/team/new)

운영 적용 절차

운영에 실유저가 없는 시점이므로 마이그레이션 스크립트는 폐기. 대신 workspaceService.ensurePersonalWorkspace(userId) 가 멱등 보장:

  • GET /api/me/workspaces 진입 시 personal 워크스페이스 없으면 즉시 생성
  • POST /api/architecture/session 등 리소스 생성 시 personal 워크스페이스에 자동 attach
  • 기존 어드민/테스트 계정도 다음 로그인 시점에 자동으로 personal 워크스페이스가 만들어짐

마스터 플래그 FEATURE_WORKSPACES=true 가 켜진 환경의 첫 요청에서 자연스럽게 부트스트랩됨.


14. UI/UX 상세 (Personal → Team, Enterprise 제외)

§7 Step 1~4를 화면 단위로 분해. Phase 1+2(팀까지) 범위. 백엔드 PR #3~#6 코드 진입 전 픽스되어야 하는 시각/상호작용 사양.

14-0. 설계 원칙 (UI 톤)

  1. "워크스페이스" 단어는 Personal 사용자에게 노출 X. Personal은 "내 작업"으로만 표기. 팀 가입/생성 시점부터 자연스럽게 단어가 등장한다. (GitHub 패턴)
  2. 워크스페이스 스위처는 항상 좌상단 브랜드 옆 고정 위치. Personal 단독일 땐 숨김, 팀 멤버십 1개라도 생기면 노출.
  3. Seat·결제 UI는 Personal에 절대 등장 X. 팀 생성 후 /w/{slug}/settings/billing 에서만.
  4. 2-레이어 권한 UI를 화면 단위로 분리:
    • "사람의 워크스페이스 등급" → 멤버 관리 화면
    • "이 캔버스에 누가 접근?" → 캔버스 공유 모달(Google Drive 정신모형)
  5. 점진 노출: 플래그 매트릭스(§14-9)로 PR 단위 토글.

14-1. 글로벌 셸 (Header / Hamburger)

현재 AppHeader 좌측:

[maker-link ~/]   ────── [원격지셀렉터] [☰]

변경 후 (팀 멤버십 ≥1 시):

[maker-link ~/]  [⌄ Acme Inc. ▾]  ────── [원격지셀렉터] [☰]
                 ↑ WorkspaceSwitcher

WorkspaceSwitcher 드롭다운:

┌──────────────────────────────┐
│ 개인                          │
│   ● 내 작업              ✓   │
│ ──────────                   │
│ 팀                            │
│   ◯ Acme Inc.   (Owner)      │
│   ◯ Foo Studio  (Member)     │
│ ──────────                   │
│ + 팀 만들기                   │
│ 📨 초대 수락 (2)              │  ← pending invite > 0 시
│ ⚙ 워크스페이스 설정           │
└──────────────────────────────┘

선택 시 라우팅 컨텍스트 변경(§15). HamburgerMenu에는 "현재 컨텍스트" 섹션이 동적으로 추가됨.

14-2. Personal → Team 업그레이드 진입점

진입점동작
WorkspaceSwitcher "+ 팀 만들기"/settings/team/new (1차)
캔버스 공유 모달에서 Personal 사용자가 "사람 추가" 클릭인라인 업셀 + CTA
HamburgerMenu 워크스페이스 섹션동일 라우팅
/settings/pricing Team 카드"팀 만들기" CTA

Personal UI 내에서 "워크스페이스를 생성하세요"가 메인 카피가 되면 안 됨. "같이 작업하려면 팀이 필요해요" 가치로 묶어 노출.

14-3. /settings/team/new — Step 1+2 (생성 + 결제)

2-step 위저드. 완료 시 /w/{slug}/settings/members?onboarding=1 로 보내 Step 3 자동 오픈.

─ Step 1. 팀 정보 ─────────────────────────
  팀 이름        [Acme Inc.            ]
  팀 주소(slug)  [acme-inc] /w/_____ (중복 체크)
  로고(선택)     [업로드]
                                  [다음 →]

─ Step 2. 결제 ─────────────────────────────
  Editor seat 수: ●───────●  3 명
  (본인 포함, 즉시 결제 확정. Billing/Guest 무료)

  회사정보(선택): 사업자번호 / 세금계산서 이메일

  결제수단:
    ◉ 카드 (Nicepay·KR)
    ◯ Card (Stripe·Global) — 곧 출시

  월 ₩9,900 × 3 = ₩29,700 / 월
                                  [팀 만들기 ✓]

14-4. /w/{slug}/settings/* — 팀 설정 페이지

좌측 사이드 네비:

Acme Inc.
─────────
일반          (이름/로고/slug)
멤버          /w/{slug}/settings/members
초대          /w/{slug}/settings/invitations
역할 & 권한   /w/{slug}/settings/roles      ← 매트릭스 읽기 전용
결제 & Seat   /w/{slug}/settings/billing
감사 로그     /w/{slug}/settings/audit
위험 영역     /w/{slug}/settings/danger

14-4-1. 멤버 화면

[👤 검색]              [+ 멤버 초대]   Seat: 5 / 10
────────────────────────────────────────────────────
이름        이메일        역할     ▾     Seat   액션
김우영(나)  me@x.com      Owner          editor  —
박철수      park@x.com    Admin   ▾      editor  ⋯
이영희      lee@x.com     Member  ▾      editor  ⋯
회계팀      acc@x.com     Billing ▾      free    ⋯
External    ext@y.com     Guest   ▾      free    ⋯

역할 변경 시 Seat 영향 있으면 확인 모달: "Member → Billing 강등 시 Seat 1개 환급". ⋯ 메뉴: 제거 / 소유권 이전(Owner만). Owner 본인 행은 역할 변경 비활성.

14-4-2. 초대 화면 + 초대 모달

Pending 초대 (3)
이메일          역할      만료     액션
park@y.com      Member    +5일    재발송 · 취소
acc@y.com       Billing   +7일    재발송 · 취소

초대 모달:

┌─ 멤버 초대 ────────────────────────────┐
│ 이메일                  역할            │
│ [park@y.com,         ] [Member ▾]      │
│ [lee@y.com           ] [Admin  ▾]      │
│ [+ 추가]                                │
│ 메시지(선택) [                  ]       │
│ Seat 5/10 · 이번 초대 +2 → OK           │
│ ⚠ 한도 초과 시 차단 + 추가구매 CTA     │
│              [취소]  [N명 초대 보내기]  │
└────────────────────────────────────────┘

14-4-3. 역할 & 권한 매트릭스 (영업 자산, 읽기 전용)

                  Owner Admin Member Billing Guest
워크스페이스 삭제   ✓
소유권 이전        ✓
결제·청구서        ✓     —     —      ✓     —
멤버 초대/역할     ✓     ✓     —      —     —
설정 변경         ✓     ✓     —      —     —
캔버스 생성/편집   ✓     ✓     ✓      —     —
ACL 부여 캔버스만  —     —     —      —     ✓
Seat 소비          1     1     1      0     0

14-4-4. 결제 & Seat

플랜: Team — ₩9,900 / editor seat / 월
Seat 사용: 5 / 10
[ ─●─────── ] 슬라이더 (prorated 안내 hover)
다음 결제: 2026-07-25  ₩99,000
결제수단: Nicepay · **** 1234  [변경]
회사정보: Acme Inc. / 123-45-67890 [수정]
청구서 [목록 →]

결제 실패 grace 상태: 페이지 상단 빨간 배너 "7일 후 read-only 전환".

14-4-5. 감사 로그 테이블: 시각 · 행위자 · action · 대상 · diff 펼침. Team plan은 30일치 노출.

14-4-6. 위험 영역 소유권 이전(이메일 코드 확인) / 워크스페이스 삭제(이름 타이핑 컨펌, 활성 구독 자동 해지 워닝).

14-5. 초대 수락 페이지 — /invitations/accept?token=...

토큰 상태별 4종 화면:

상황표시
비로그인"Acme Inc.에 초대됨 (Member)" + [로그인 후 수락] / [가입 후 수락]
로그인 + 신규카드 + 역할/Seat 안내 + [수락]
이미 멤버"이미 멤버입니다" + [워크스페이스로]
만료/취소"유효하지 않은 초대" + [관리자에게 재요청]
Seat 초과"워크스페이스 가득 참" + 관리자 자동 알림 + [읽기 권한으로 이동]

수락 직후 → 스위처 펼침 상태로 /w/{slug}/dashboard 진입 (컨텍스트 변경 시각화, Slack 패턴).

14-6. 캔버스 공유 모달 (Step 4 · Google Drive 패턴)

architecture-designer/:id 우상단 "공유" 버튼 추가.

┌─ "마이크로서비스 v3" 공유 ───────────────────┐
│ 사람 또는 이메일 추가                         │
│ [@이영희, park@external.com,    ] [역할 ▾]   │
│                                              │
│ 접근 권한자                                  │
│ 👤 김우영(나)    Manager (소유자)            │
│ 👤 이영희        Editor    ▾   🗑           │
│ 👤 박철수        Commenter ▾   🗑           │
│ 🌐 lee@gmail.com Viewer    ▾   🗑           │ ← Guest
│                                              │
│ 일반 액세스                                  │
│  🏢 Acme Inc. 멤버 · Commenter ▾             │
│   ㄴ 옵션: 워크스페이스 멤버 / 초대된 사람만 │
│           / 링크 있는 누구나                 │
│                                              │
│ 🔗 https://maker-link.com/share/abc123           │
│    [복사] [재발급] [회수]  만료 30일         │
│                                  [완료]      │
└─────────────────────────────────────────────┘

UX 디테일:

  • Personal 사용자가 사람 추가 시도 → 인라인 업셀 "팀이 필요해요" + [팀 만들기]
  • 워크스페이스 Owner/Admin은 항상 Manager 표시, ACL에서 강등 불가
  • 외부 이메일 추가 시 미가입자면 자동 Invitation(role: guest, seat 0) 생성

캔버스 본문 변화: 우상단에 [공유] [👤👤👤 +2] 미니 뱃지(호버 시 ACL 요약).

14-7. Dashboard / 캔버스 리스트 변화

이름                   소속        업데이트    공유
"DB 마이그레이션"      Acme Inc.   2시간 전   👤+3
"개인 메모"            내 작업      어제       🔒
"외부와 작업"          Acme Inc.   3일 전     🔗

스위처에서 "내 작업" 선택 → personal workspace 필터, 팀 선택 → 팀 필터.

14-8. Pricing 페이지 변경

┌── Personal (무료) ──┐  ┌── Team ──────────────┐
│ Editor 1 (본인)     │  │ ₩9,900 / editor / 월 │
│ 캔버스 무제한       │  │ Seat 자유 증감       │
│ 보기 링크          │  │ Billing/Guest 무료    │
│ 팀 초대 불가       │  │ ACL · 공유 링크       │
│ [현재 플랜]        │  │ 감사 로그 30일        │
└────────────────────┘  │ [팀 만들기]          │
                        └──────────────────────┘
문의: Business/Enterprise → SAML, SCIM, 데이터 리전 (영업)

14-9. 점진 노출 / 플래그 매트릭스

PR 단위 토글:

플래그노출 UI켜지는 PR
WS_SWITCHER헤더 스위처, /w/{slug} 라우팅 prefixPR #3
WS_TEAM_CREATE/settings/team/new, 햄버거 진입PR #3
WS_BILLING결제 페이지, Seat 슬라이더PR #3
WS_INVITATIONS초대 모달, 수락 페이지PR #4
WS_CANVAS_SHAREShareDialog, ACL 뱃지PR #5
WS_AUDIT_LOG감사 로그 화면PR #6

14-10. 신규 디자인 시스템 컴포넌트

apps/web/src/components 아래 신규 컴포넌트:

  • WorkspaceSwitcher (헤더 + 햄버거 공용)
  • RoleBadge (Owner=amber, Admin=violet, Member=neutral, Billing=cyan, Guest=slate)
  • AclRoleSelect (Manager/Editor/Commenter/Viewer)
  • SeatMeter (n/m + prorated hover)
  • ShareDialog (캔버스 공유 모달)
  • MemberRow, InvitationRow
  • BillingProviderRadio (Nicepay/Stripe·disabled)
  • DangerZoneCard

기존 다크 톤(pv2-text-accent) 유지, 역할 컬러 토큰만 추가.


15. 라우팅 컨벤션 (확정)

결정: /w/{slug}/... 컨텍스트 prefix.

선정 근거:

  • withWorkspace 미들웨어가 URL param만으로 req.workspace 주입 → CASL buildAbilityFor 단순
  • 공유 링크/감사 로그/초대 수락 후 진입 URL이 모두 명시적
  • 멀티 워크스페이스 사용자의 "같은 리소스 ID, 다른 컨텍스트" 모호성 제거
영역PersonalTeam
대시보드/dashboard/w/{slug}/dashboard
설정 (계정)/settings/account, /settings/security동일(워크스페이스 무관)
설정 (팀)/w/{slug}/settings/{tab}
캔버스/architecture-designer/:id/w/{slug}/architecture-designer/:id
팀 생성/settings/team/new
초대 수락/invitations/accept?token=...동일, 수락 후 /w/{slug}/dashboard
공개 공유 링크/share/{token} (워크스페이스 무관, 토큰이 ACL 자체)동일

Personal 사용자에게는 URL에 slug 미노출 — 내부적으로 personal workspace는 존재하지만 라우팅상 prefix 없음 (UI 톤 §14-0 원칙 1과 일관).

미들웨어 계약 (백엔드):

withWorkspace(req)
  ├─ slug → workspaceId 해석 + Membership 조회
  ├─ req.workspace = { id, slug, plan, ... }
  ├─ req.membership = { role, seatType } | null
  └─ req.ability = buildAbilityFor(user, workspace, membership)

라우트는 항상 withWorkspacerequireAbility(action, subject) 로 게이트.


16. 실행 단계 (체크리스트)

각 단계는 직전 단계 완료를 가정. 백엔드는 PR #3부터 재개, 프론트는 UI/UX 픽스 후 PR 단위로 병행.

단계 0 — 사전 준비 (이 문서 머지 직후)

  • [x] 본 문서 v2 작성
  • [x] FEATURE_WORKSPACES 마스터 + §14-9 6개 서브플래그 환경변수 자리 추가 (모두 off)
    • apps/api/env.example, apps/web/.env.example
    • apps/api/utils/featureFlags.js, apps/web/src/lib/featureFlags.ts 유틸 추가
  • [x] 백필 운영 런북 → 폐기. 실유저 없음 + ensurePersonalWorkspace 멱등 자동 보장으로 대체.
  • [x] workspaceService.ensurePersonalWorkspace(userId) 추가 — /me/workspaces 부트스트랩과 POST /architecture/session 진입 시 호출되어 personal 워크스페이스를 즉시 보장.

단계 1 — 디자인 시스템 픽스 (코드 진입 직전)

  • [x] 역할별 컬러 토큰 globals.css 등록 (워크스페이스 5색 + ACL 4색)
  • [x] §14-10 8개 컴포넌트 실구현 apps/web/src/components/workspace/
    • WorkspaceSwitcher, RoleBadge, AclRoleSelect, SeatMeter, ShareDialog, MemberRow, InvitationRow, BillingProviderRadio, DangerZoneCard
  • [x] 공용 도메인 타입 types.ts + 단일 swap point useWorkspaces (PR #3에서 fetch 교체)
  • [x] AppHeaderWorkspaceSwitcher 배선 (플래그 off로 미노출)
  • [x] tsc --noEmit 통과

단계 2 — PR #3 (팀 생성 + 결제)

본 단계는 두 라운드로 분할 진행. 라운드 A: 팀 CRUD 코어 / 라운드 B: 결제 통합.

라운드 A — 팀 CRUD 코어 백엔드:

  • [x] GET /me/workspaces (부트스트랩 — pending 초대 카운트 포함)
  • [x] GET /workspaces/slug-check?slug= (위저드 가용성 체크)
  • [x] POST /workspaces (Owner Membership 트랜잭션 + workspace.created 감사로그)
  • [x] GET /workspaces/:idOrSlug (slug 또는 ObjectId)
  • [x] PATCH /workspaces/:id (workspace.updated 감사로그)
  • [x] services/auditLog.js 도입 — 이후 모든 변경 액션은 본 헬퍼 경유
  • [x] withWorkspace 미들웨어 활용 (라우트 prefix는 백엔드 무영향, 프론트 /w/:slug 만 적용)

프론트:

  • [x] useWorkspaces 훅 → 실 API(fetch) 호출, 401/플래그 off 안전 처리
  • [x] lib/workspaceApi.ts 클라이언트 모듈
  • [x] /settings/team/new 2-step 위저드 (slug 디바운스 체크, 결제 자리만)
  • [x] /w/[slug]/dashboard 임시 페이지 (PR #6 본 대시보드 진입 전 안전망)

라운드 B — 결제 통합 백엔드:

  • [x] POST /workspaces/:id/subscription (Nicepay 빌키 등록 + 첫 결제, billing.subscribed 감사)
  • [x] PATCH /workspaces/:id/subscription (seat 증감, seat.changed 감사 — 즉시 prorated 결제는 라운드 C)
  • [x] DELETE /workspaces/:id/subscription (해지, 빌키 폐기, billing.cancelled 감사)
  • [x] workspaceServicesubscribeWorkspace / changeWorkspaceSeats / cancelWorkspaceSubscription
  • [x] Workspace.billing.{customerId=bid, subscriptionId=tid, company} 저장 패턴 확정

프론트:

  • [x] /settings/team/new Step 2 실 결제 연동 (CardInputForm + Seat 슬라이더 + 회사정보)
  • [x] /w/[slug]/settings/billing 페이지 (구독 시작 / Seat 변경 / 해지)
  • [x] CardInputForm 컴포넌트 추가 (Nicepay 빌키용 공용)
  • [x] WorkspaceSummary.billing 타입 확장
  • [x] tsc --noEmit 통과

라운드 C — 후속 (다음 작업 후보)

  • [ ] Membership 변경 시 workspace.seatUsage 트랜잭션 자동 갱신 (PR #4 초대 흐름과 함께)
  • [ ] Seat 증감 즉시 prorated 결제 (Nicepay 차액 인보이스)
  • [ ] 결제 실패 7일 grace + read-only 전환
  • [ ] Pricing 카드 갱신 (Personal / Team 카드 §14-8)
  • [ ] 플래그 staging 활성화: WS_SWITCHER, WS_TEAM_CREATE, WS_BILLING

단계 3 — PR #4 (초대 + 수락)

백엔드:

  • [x] Invitation CRUD: POST/GET /workspaces/:id/invitations, DELETE /workspaces/:id/invitations/:iid
  • [x] GET /invitations/preview?token= (수락 페이지 4상태 결정용, optionalAuth)
  • [x] POST /invitations/accept — Membership 생성 + seat 회계 트랜잭션
  • [x] 멤버 관리: GET /workspaces/:id/members?q=, PATCH/DELETE /workspaces/:id/members/:uid
  • [x] Seat 한도 검증 (SEAT_LIMIT / SEAT_FULL 분리 응답)
  • [x] 이메일 발송: sendWorkspaceInviteEmail (Resend, RESEND_API_KEY 없으면 noop)
  • [x] 감사 액션 5종: member.invited, member.joined, member.role_changed, member.removed, invitation.revoked
  • [x] Seat 회계 자동화: 초대 수락 / 역할 변경 / 멤버 제거 시 workspace.seatUsage 트랜잭션 자동 갱신

프론트:

  • [x] /w/{slug}/settings/members — 멤버 + Pending 초대 동시 관리
  • [x] 초대 모달 — 다건 입력, 역할 선택, Seat 한도 인라인 경고
  • [x] /invitations/accept?token= 4+1 상태 페이지 (pending / already-member / accepted / expired / invalid / seat-full)
  • [x] Switcher pending 뱃지 (useWorkspaces 부트스트랩에 카운트 포함, 컴포넌트는 이미 표시)
  • [x] tsc --noEmit 통과
  • [ ] 플래그 staging 활성화: WS_INVITATIONS

단계 4 — PR #5 (캔버스 ACL 공유)

백엔드:

  • [x] services/aclService.js — getOrInit, serializeAcl, setGeneralAccess, addEntry/changeRole/remove, rotate/revoke public link
  • [x] GET/PATCH /sessions/:id/acl — 일반 액세스 변경
  • [x] POST/PATCH/DELETE /sessions/:id/acl/entries[/:subjectId] — 사람/게스트 ACL CRUD
  • [x] POST/DELETE /sessions/:id/share-link — 공개 링크 발급(회전)/회수, 원문 토큰 1회 노출
  • [x] GET /share/resolve?token= — 공개 링크 검증 (인증 불필요)
  • [x] GET /workspaces/:id/members?q= autocomplete (PR #4에서 이미 추가)
  • [x] 워크스페이스 Owner/Admin은 ACL에 명시 없어도 Manager 자동 노출
  • [x] 게스트 이메일 추가 시 Invitation(role=guest, seat=free) 자동 발급 + 메일

프론트:

  • [x] ShareDialog (PR #5 이전에 컴포넌트 작성, 본 라운드에서 API 연결)
  • [x] ShareSessionButton — sessionId만 받아 자체 fetch/모달 호스팅
  • [x] architecture-designer 우상단에 공유 버튼 1줄 추가
  • [x] /share/[token] 공개 링크 진입점 → 세션 페이지 리다이렉트
  • [x] tsc --noEmit 통과
  • [x] 대시보드 리스트 "소속" 칼럼 (architecture GET /sessions 응답에 workspace 메타 포함, PR #6에서 완료)
  • [ ] 플래그 staging 활성화: WS_CANVAS_SHARE

단계 5 — PR #6 (워크스페이스 설정 페이지 완성)

  • [x] GET /workspaces/:id/members (PR #4), GET /workspaces/:id/audit-logs (cursor pagination)
  • [x] POST /workspaces/:id/transfer-ownership, DELETE /workspaces/:id
  • [x] architecture GET /session/:id ACL read 게이트 + 응답에 workspace/myAclRole (공유 받은 세션 진입 가능)
  • [x] /w/{slug}/settings 공통 layout(사이드 네비)
  • [x] /w/{slug}/settings 일반(이름·회사정보)
  • [x] /w/{slug}/settings/roles 권한 매트릭스
  • [x] /w/{slug}/settings/audit 감사 로그(액션 라벨/펼치기 diff/더 보기)
  • [x] /w/{slug}/settings/danger 소유권 이전 + 워크스페이스 삭제(이름 타이핑 컨펌)
  • [ ] 플래그: WS_AUDIT_LOG 활성

단계 6 — Phase 2 안정화

  • [ ] Stripe 평행 결제
  • [ ] prorated upgrade/downgrade 회귀 테스트
  • [ ] 결제 실패 grace period 7일 read-only 전환 자동화

13. 변경 이력

  • 2026-06-25: v1 초안 확정 + PR #1, #2 완료. 다음 PR #3부터 재개.
  • 2026-06-26: v2 — UI/UX 상세(§14), 라우팅 컨벤션 /w/{slug} 확정(§15), 실행 단계 체크리스트(§16) 추가. §8에 GET /me/workspaces, GET /workspaces/:id/members?q= 추가.
  • 2026-06-26: v2.7 — 운영에 실유저가 없으므로 마이그레이션 스크립트(backfillWorkspaces.js)와 런북 폐기. 대신 workspaceService.ensurePersonalWorkspace(userId) 추가 (멱등, 트랜잭션). /me/workspaces 부트스트랩과 POST /architecture/session 시점에 자동 보장되어 기존 어드민/테스트 계정도 다음 로그인 시 자연스럽게 personal 워크스페이스가 생성됨.
  • 2026-06-26: v2.6 — PR #6 (워크스페이스 설정 페이지 완성) 코어 구현. 백엔드: POST /workspaces/:id/transfer-ownership, DELETE /workspaces/:id(활성 구독 자동 해지·관련 도큐먼트 cleanup), GET /workspaces/:id/audit-logs?cursor=&action=. architecture: GET /session/:id ACL read 게이트 + workspace/myAclRole 응답, GET /sessions 응답에 workspace 메타. 프론트: /w/[slug]/settings/{layout, page, roles, audit, danger} 추가 및 멤버/결제 페이지를 공통 layout으로 통합.

메이커링크 - 원격 터미널 플랫폼