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. 벤치마킹 채택안
| 출처 | 채택한 요소 |
|---|---|
| GitHub | Personal account = Org of one, Outside Collaborator |
| Figma | Editor seat 유료 + Viewer/Commenter 무료 |
| Vercel | Owner/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 | 결제 정보·청구서·플랜만, 콘텐츠 접근 X | 0 (free) |
| Guest (Outside Collaborator) | 명시적으로 ACL에 추가된 리소스만 접근 | 0 (free) |
3-2. Resource ACL Role (캔버스/세션 단위)
| Role | 허용 |
|---|---|
| Manager | ACL 변경·공유링크·삭제 (워크스페이스 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
{
_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
{
_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 } uniqueInvitation
{
_id, workspaceId, email (lowercase),
role, seatType,
tokenHash,
expiresAt (default +7d),
invitedBy,
status: 'pending' | 'accepted' | 'expired' | 'revoked',
acceptedAt?, acceptedUserId?
}
// 인덱스: { workspaceId, email, status }ResourceAcl
{
_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 } uniqueAuditLog
{
_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. 초대 코너 케이스 정책
- 비가입자 → 가입(또는 OAuth) → 자동 수락
- 기존 다른 워크스페이스 멤버 → 워크스페이스 전환기에 추가 (Slack 패턴)
- 이미 같은 워크스페이스 멤버 → "이미 멤버" + 초대 무효
- 이메일 정규화: lowercase.
+alias는 동일 인물로 취급하지 않음(악용 방지). - 토큰 7일 만료, 만료 시 재발송 버튼
- 취소 토큰 즉시 무효
- Seat 한도 초과 상태 → 수락 차단 + 관리자 알림
10. 로드맵 & PR 분할
Phase 1 — Foundation (현 사이클)
| # | PR | 산출물 |
|---|---|---|
| 1 | 모델 + 백필 | Workspace/Membership/Invitation/ResourceAcl/AuditLog 스키마, User → personal workspace 백필, ArchitectureSession 마이그레이션 |
| 2 | CASL 도입 + 권한 게이트 | 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.planenum에'business' | 'enterprise'등록 (사용 X)Workspace.sso필드 (null)Workspace.dataRegion('kr' default)Workspace.scimExternalId(null)Membership.provisioningSource('manual' default)Membership.externalId(null)AuditLog는 Phase 1부터 실제 기록 시작 (영업 자산)
12. 진행 상황 & 다음 작업
완료
- PR #1 — 모델 + 백필 (머지 대기)
- 신규 모델:
Workspace,Membership,Invitation,ResourceAcl,WorkspaceAuditLog ArchitectureSession에workspaceId/createdBy옵셔널 필드 추가 (기존user유지)백필 스크립트→ 폐기. 운영에 실유저가 없어 마이그레이션 대신 lazy 자동 보장으로 전환 (§12 참조).
- 신규 모델:
- PR #2 — CASL 권한 게이트 인프라 (머지 대기)
@casl/ability ^7.0.0의존성 추가apps/api/services/ability.js—buildAbilityFor,subjectOnSession,subjectOnWorkspace,canActOnSessionapps/api/middleware/workspace.js—withWorkspace,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 톤)
- "워크스페이스" 단어는 Personal 사용자에게 노출 X. Personal은 "내 작업"으로만 표기. 팀 가입/생성 시점부터 자연스럽게 단어가 등장한다. (GitHub 패턴)
- 워크스페이스 스위처는 항상 좌상단 브랜드 옆 고정 위치. Personal 단독일 땐 숨김, 팀 멤버십 1개라도 생기면 노출.
- Seat·결제 UI는 Personal에 절대 등장 X. 팀 생성 후
/w/{slug}/settings/billing에서만. - 2-레이어 권한 UI를 화면 단위로 분리:
- "사람의 워크스페이스 등급" → 멤버 관리 화면
- "이 캔버스에 누가 접근?" → 캔버스 공유 모달(Google Drive 정신모형)
- 점진 노출: 플래그 매트릭스(§14-9)로 PR 단위 토글.
14-1. 글로벌 셸 (Header / Hamburger)
현재 AppHeader 좌측:
[maker-link ~/] ────── [원격지셀렉터] [☰]변경 후 (팀 멤버십 ≥1 시):
[maker-link ~/] [⌄ Acme Inc. ▾] ────── [원격지셀렉터] [☰]
↑ WorkspaceSwitcherWorkspaceSwitcher 드롭다운:
┌──────────────────────────────┐
│ 개인 │
│ ● 내 작업 ✓ │
│ ────────── │
│ 팀 │
│ ◯ 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/danger14-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 014-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} 라우팅 prefix | PR #3 |
WS_TEAM_CREATE | /settings/team/new, 햄버거 진입 | PR #3 |
WS_BILLING | 결제 페이지, Seat 슬라이더 | PR #3 |
WS_INVITATIONS | 초대 모달, 수락 페이지 | PR #4 |
WS_CANVAS_SHARE | ShareDialog, 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,InvitationRowBillingProviderRadio(Nicepay/Stripe·disabled)DangerZoneCard
기존 다크 톤(pv2-text-accent) 유지, 역할 컬러 토큰만 추가.
15. 라우팅 컨벤션 (확정)
결정: /w/{slug}/... 컨텍스트 prefix.
선정 근거:
withWorkspace미들웨어가 URL param만으로req.workspace주입 → CASLbuildAbilityFor단순- 공유 링크/감사 로그/초대 수락 후 진입 URL이 모두 명시적
- 멀티 워크스페이스 사용자의 "같은 리소스 ID, 다른 컨텍스트" 모호성 제거
| 영역 | Personal | Team |
|---|---|---|
| 대시보드 | /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)라우트는 항상 withWorkspace 후 requireAbility(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.exampleapps/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 pointuseWorkspaces(PR #3에서 fetch 교체) - [x]
AppHeader에WorkspaceSwitcher배선 (플래그 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/new2-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]
workspaceService에subscribeWorkspace / changeWorkspaceSeats / cancelWorkspaceSubscription - [x]
Workspace.billing.{customerId=bid, subscriptionId=tid, company}저장 패턴 확정
프론트:
- [x]
/settings/team/newStep 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/:idACL 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/:idACL read 게이트 +workspace/myAclRole응답,GET /sessions응답에workspace메타. 프론트:/w/[slug]/settings/{layout, page, roles, audit, danger}추가 및 멤버/결제 페이지를 공통 layout으로 통합.