# 레벨 스튜디오 재구현 설계 명세

작성일: 2026-10-26 · 설계 상태: 구현을 위한 제안 · 원본 내부 구조에 관한 주장: 없음

이 문서는 HighLevel의 공개 기능 분석을 바탕으로 레벨 스튜디오를 독립적으로 구현하기 위한 서버·권한·데이터·실행기 설계다. HighLevel의 비공개 서버, 원본 데이터베이스, 내부 큐, AI 프롬프트, 공급자 계약을 확보한 결과가 아니다. 아래 엔티티·상태·API는 **레벨 스튜디오 자체 설계**이며 현재 코드에 모두 구현됐다는 뜻도 아니다. 실제 구현·테스트 완료 상태는 별도 인계 문서와 실행 결과를 기준으로 판단한다.

## 1. 목표와 제품 경계

사용자의 반복 업무는 ‘유입 → 연락처 → 응대 → 예약/영업 → 청구 → 후속 관리 → 성과’로 이어진다. 각 메뉴를 서로 독립적인 데모로 만드는 대신 같은 사업장·연락처·이벤트를 공유하는 운영 시스템을 목표로 한다.

완료 판정은 화면 수가 아니라 데이터 연속성이다. 예를 들어 CSV에서 가져온 연락처가 검색되고, 그 연락처로 영업기회와 예약을 만들며, 대시보드 집계와 내보내기/복원 결과까지 일치해야 한다. 외부 메시지·결제·게시·AI는 공급자 연결과 실제 실행 결과를 검증하기 전까지 초안·시뮬레이션·미연결로 표시한다.

### 실행 프로필

| 프로필 | 저장·실행 | 용도 | 완성으로 간주하지 않는 범위 |
|---|---|---|---|
| 브라우저 독립 시제품 | 브라우저 저장소·로컬 파일 내보내기 | 화면·상호작용·업무 흐름 검토 | 다중 사용자·상주 예약·실제 채널 전송 |
| 로컬 운영 서버 | 같은 PC의 API·관계형 DB·파일 저장소·작업자 | 단일 운영자 또는 제한된 내부 시험 | PC 장애 내성·대외 공개 운영 |
| 호스팅 운영 서버 | 인증된 API·관계형 DB·객체 저장소·작업자 | 다중 사업장·실제 외부 연결 | 기능별 공급자 검증을 통과하지 않은 동작 |

프로필을 전환할 때 데이터 모델·이벤트 의미·내보내기 형식은 가능한 한 유지한다. 저장 어댑터 뒤로 CRUD를 모으고, UI가 직접 공급자 토큰이나 서버의 내부 파일 경로를 다루지 않게 한다.

## 2. 계층과 의존 방향

```text
UI / 접근성 / 화면 상태
  ↓ 명령·조회
Application Services
  ├─ 정책 검사 / 입력 검증 / 버전 검사
  ├─ 연락처·영업·예약·청구·콘텐츠 서비스
  └─ Import / Workflow / Agent 조정
  ↓ 트랜잭션 경계
Domain / 상태 전이 / 업무 불변조건
  ↓ 저장 인터페이스
Repositories / Unit of Work
  ├─ 관계형 데이터
  ├─ 파일·미디어 저장소
  └─ Outbox / Inbox / 실행 작업
  ↓ 별도 실행 프로세스
Workers / Scheduler / Connector Adapters
  ├─ 메시지 / 캘린더 / 결제 / 소셜 / 리뷰
  ├─ 보고서 생성·전달
  └─ AI 모델·지식 검색·도구 실행
```

- UI는 화면 로딩·선택·임시 입력을 갖지만 업무 상태의 최종 권한은 서버에 둔다.
- 도메인은 외부 SDK에 의존하지 않는다. `MessagingPort`, `PaymentPort`, `CalendarPort`, `ModelPort` 같은 인터페이스에 요청한다.
- API와 작업자는 같은 상태 전이 규칙·권한 검증을 재사용한다.
- 요청 트랜잭션 중 외부 네트워크 호출을 기다리지 않는다. 내부 상태와 Outbox를 함께 기록하고 외부 호출은 별도 작업자가 수행한다.
- 읽기 집계는 원본 데이터와 계산식을 명확히 한다. 시제품의 고정 예제 수치를 운영 데이터로 섞지 않는다.

## 3. 사업장 경계와 권한

### 3.1 식별자와 범위

`agencyId`는 운영사, `workspaceId`는 고객/사업장 운영 단위다. 사용자 계정의 전역 ID와 사업장 소속(`WorkspaceMembership`)을 분리한다. 동일한 사용자가 여러 사업장에서 다른 역할을 가질 수 있다.

모든 업무 레코드의 조회·생성·수정·삭제 키는 `workspaceId + recordId`다. URL/요청 본문에 들어온 workspace를 그대로 신뢰하지 않고, 인증된 사용자의 소속과 권한으로 검증한다. 사업장별 외부 연결·통화·시간대·중복 설정도 분리한다.

| 데이터 | 소유 경계 | 참조 허용 |
|---|---|---|
| Contact / Opportunity / Appointment | Workspace | 같은 Workspace |
| Message / Invoice / Review | Workspace | 같은 Workspace의 Contact·Connection |
| Media / Page / Form / Course | Workspace | 같은 Workspace의 자산 |
| Workflow / Agent / 실행 이력 | Workspace | 같은 Workspace의 정의·도구·지식 |
| SaaS Plan / SnapshotTemplate | Agency | 명시적으로 허용된 사업장에 복사 |
| Credential | Connection의 비밀 저장 범위 | 서버 어댑터만 복호화/조회 |

사업장 간 자산 공유는 참조를 몰래 연결하는 방식 대신 승인된 템플릿 복사로 처리한다. ID를 새로 발급하고 내부 참조를 다시 매핑한다.

### 3.2 정책 함수

`authorize(actor, workspace, module, action, record?)`는 최소한 다음을 함께 평가한다.

1. 계정/소속 활성 여부.
2. 해당 workspace 접근 허용.
3. 모듈 이용 가능 여부(요금제·설정).
4. 역할과 세부 행동 권한(`read`, `create`, `update`, `delete`, `export`, `execute`, `manage`).
5. 담당 데이터 범위와 해당 레코드 담당/팔로워 관계.
6. 실행별 추가 조건(실제 전송, 게시, 결제, 민감 설정 변경).

목록 조회는 정책을 쿼리 조건으로 적용한다. 레코드를 전부 읽은 뒤 UI에서 걸러내는 방식은 사용하지 않는다. 통계·검색·파일 URL 발급·CSV 내보내기도 같은 범위를 적용한다. 권한이 바뀌면 세션 권한 캐시와 공개된 작업 링크를 재검증한다.

### 3.3 감사 기록

`AuditEntry`: workspace, actor, actorType, action, entityType, entityId, changedFieldNames, safeBeforeAfter, requestId, occurredAt. 비밀번호·토큰·전체 메시지 본문·결제 카드 자료는 일반 감사 값에 복제하지 않는다. 실제 고객 자료를 포함한 로그는 코드 저장소와 구분한다.

## 4. 관계형 데이터 모델 제안

### 4.1 공통 컬럼·제약

업무 엔티티: `id`, `workspaceId`, `createdAt`, `updatedAt`, `version`, 필요시 `deletedAt`. 시각은 UTC로 저장하고 날짜 전용 값·표시 시간대는 별도로 구분한다. 숫자 표시를 위해 금액을 부동소수점으로 누적하지 않는다.

- 복합 키 또는 제약으로 `(workspaceId, id)`를 유일하게 만들고 관계도 같은 workspace 쌍을 참조한다.
- 태그·담당자·사용자 정의 필드는 참조 무결성을 유지한다.
- 낙관적 잠금은 `version` 비교로 처리하며 충돌 때 새 값과 사용자 변경을 함께 보여준다.
- 삭제 정책은 엔티티별로 정한다. 결제·전송·실행 원장은 일반 CRUD 삭제 대신 취소/정정 이벤트를 사용한다.
- API 응답의 ID는 추측하기 어려운 값으로 발급하지만 ID의 복잡성에 접근 통제를 의존하지 않는다.

### 4.2 CRM

| 엔티티 | 핵심 필드 | 주요 불변조건 |
|---|---|---|
| Contact | name, primaryEmail, primaryPhone, normalizedKeys, ownerId, source, dndByChannel | 빈 이메일/전화는 중복 키가 아님 |
| ContactChannelAddress | contactId, channel, value, normalizedValue, primary | 채널별 기본값 1개 |
| Tag / ContactTag | label / contactId, tagId | 중복 관계 금지 |
| SavedList | name, filterAST, columns, sort | 필터 스키마 버전 저장 |
| Note / Task | contactId, author/assignee, body, dueAt, status | 담당자와 연락처 같은 workspace |
| Company | name, domain, ownerId | 자동 연결 기준은 명시적 설정 |
| ObjectDefinition / FieldDefinition | objectKey, fieldKey, type, required, unique, options | 사용 중 타입 변경은 별도 마이그레이션 |
| ObjectRecord / Association | values / sourceType+id, relationType, targetType+id | 서로 다른 workspace 연결 금지 |

중복 정책은 별도 `DeduplicationPolicy`로 버전 관리한다. 자동 병합은 근거가 명확한 필드만 사용하고, ID/이메일/전화가 서로 다른 연락처와 일치하는 경우 사용자가 해결하도록 충돌 항목을 만든다. 기존 연락처가 무엇인지 모호할 때 임의 레코드를 덮어쓰지 않는다.

### 4.3 영업·예약

| 엔티티 | 핵심 필드 | 주요 불변조건 |
|---|---|---|
| Pipeline / Stage | name / pipelineId, name, position | Stage는 자신의 Pipeline에 속함 |
| Opportunity | contactId, pipelineId, stageId, status, amount, currency, ownerId, lostReason | 단계와 결과 상태는 독립 |
| OpportunityTransition | from/to stage·status, actor, eventId, at | 과거 변화는 불변 이력 |
| Calendar | type, timezone, rules, duration, buffers, capacity, assignmentPolicy | 설정 버전 보존 |
| AvailabilityException | calendar/user, date/range, blocked/override | 휴무·특별 가용 시간 명시 |
| Appointment | contactId, calendarId, startAt, endAt, timezone, status, version | end > start·수용 한도·충돌 검사 |
| AppointmentResource | appointmentId, resourceId, reservationRange | 확정된 슬롯만 점유 |
| BookingAttempt | idempotencyKey, requestHash, appointmentId, outcome | 동일 키에 다른 요청은 충돌 |

### 4.4 대화·상거래

| 엔티티 | 핵심 필드 | 주요 불변조건 |
|---|---|---|
| Conversation | contactId, channel, connectionId, assignee, inboxState | 채널·연결에 대응하는 수신자 필요 |
| Message | conversationId, direction, bodyRef, transportState, providerMessageId | providerMessageId 중복 제거 |
| MessageAttempt | messageId, attemptNo, providerResult, at | 시도와 최종 전달 상태 분리 |
| Product / Price | name / amount, currency, recurrence | 가격 버전은 과거 주문을 바꾸지 않음 |
| Invoice / InvoiceLine | contactId, status, currency, dueAt / snapshot, qty, amount | 발행 시 항목 스냅샷 유지 |
| Payment / Refund | invoice/order, providerId, externalId, amount, state | 외부 ID·이벤트 멱등 처리 |
| Subscription | providerId, priceSnapshot, period, state | 결제 원장과 접근권을 추적 연결 |

카드 번호·CVV 같은 원본 결제 정보는 레벨 스튜디오의 일반 DB에 저장하는 설계를 두지 않는다. 공급자가 제공하는 안전한 결제 표면과 식별자를 사용한다.

### 4.5 콘텐츠·AI·실행

| 엔티티 | 핵심 필드 | 주요 불변조건 |
|---|---|---|
| MediaAsset | storageKey, mime, size, sha256, visibility | 공개 URL과 저장 키 구분 |
| Page / PageVersion | slug, publishedVersionId / blockTree, assets | 게시 버전은 불변 |
| Form / Submission | schemaVersion / answers, contactId, dedupeKey | 검증한 스키마 버전 보존 |
| Course / Lesson / Enrollment | content / structure / grantSource, expiresAt | 수강권 없으면 비공개 콘텐츠 차단 |
| Review / ReviewRequest | source/externalId/rating / recipient, state | 원본 수집과 요청 전송 구분 |
| WorkflowDefinition / WorkflowVersion | draftGraph / immutableGraph, publishedAt | 실행은 고정 버전 참조 |
| WorkflowRun / StepRun | eventId, state / nodeId, attempt, nextAt | 단계 중복 실행 방지 |
| AgentVersion / AgentRun | prompt/tools/knowledge config / model, usage, result | 실제 도구 권한을 실행 때 재검사 |
| KnowledgeSource / IndexVersion | sourceRef, digest, state / sourceVersions | 원본 변경·인덱스 갱신 상태 구분 |

## 5. 상태 기계와 전이

다음 이름은 구현 제안이다. 원본의 실제 enum을 추정 복제한 것이 아니다.

### 5.1 Import

```text
selected → uploaded → mapped → validated → queued → processing
                                             ├─ completed
                                             ├─ completed_with_errors
                                             ├─ failed
                                             └─ cancelled
```

- `mapped` 이전에는 업무 레코드를 수정하지 않는다.
- 파일·매핑·모드·정책 중 하나라도 바뀌면 검증 결과를 폐기하고 다시 `mapped/validated`로 간다.
- `queued`부터 파일 해시·매핑·정책을 고정한다.
- 행 결과는 created / updated / skipped / invalid / conflict / failed로 분리한다.
- 작업 취소는 이미 커밋한 행을 자동으로 되돌리는 뜻이 아니다. 롤백을 제공하려면 import 이전 버전과 외부 후속 이벤트를 포함한 별도 정정 절차가 필요하다.

### 5.2 예약

```text
requested → confirmed → completed
     │          ├─ cancelled
     │          └─ no_show
     └─ rejected
confirmed → rescheduled(이력) → confirmed(새 구간)
```

변경 예약은 기존 예약 ID를 유지하고 버전을 올리거나 후속 예약으로 연결하는 정책 중 하나를 고정한다. 어느 방식을 쓰든 옛 슬롯 해제와 새 슬롯 확보, 이전 알림 취소와 새 알림 등록은 추적 가능해야 한다. 취소 후 도착한 오래된 공급자 이벤트가 다시 확정 상태로 되돌리지 못하게 한다.

### 5.3 메시지

```text
draft → queued → dispatching → accepted → delivered → read
          │          │            └─ delivery_failed
          │          └─ rejected / retry_wait
          └─ cancelled
```

`accepted`는 공급자가 요청을 수락했다는 뜻이고 `delivered`는 전달 확인 이벤트가 있어야 한다. 채널이 읽음 영수증을 제공하지 않으면 read를 만들어내지 않는다. 외부 호출 타임아웃은 ‘실패 확정’이 아니라 ‘결과 미확인’일 수 있으므로 공급자 조회/멱등 키로 조정한다.

### 5.4 청구·결제

청구서: `draft → issued → partially_paid → paid`, 별도 `void`, 만기일 기준 `overdue` 표시. 결제 시도: `initiated → pending → succeeded / failed / cancelled`, 환불: `requested → pending → succeeded / failed`. 취소·환불은 원래 원장을 수정해서 지우지 않고 반대 기록과 연결한다.

### 5.5 워크플로·에이전트

정의: `draft → published(version N) → archived`; 실행: `queued → running → waiting → running → completed`, 별도 `paused / cancelled / failed / needs_review`. AI 생성 성공과 도구 실행 성공을 분리한다. 에이전트가 생성한 계획은 실행 결과가 아니다.

## 6. 예약의 경쟁 조건과 멱등성

예약은 단순히 현재 목록을 읽고 빈 시간에 insert하는 방식으로 구현하지 않는다. 두 요청이 같은 슬롯을 동시에 읽으면 둘 다 성공할 수 있기 때문이다.

### 생성 절차

1. 클라이언트가 동일 작업 재시도에 재사용하는 `Idempotency-Key`를 발급한다.
2. 서버는 인증·workspace·예약 권한·입력·시간대를 검증한다.
3. `(workspaceId, operation, key)`를 고유하게 저장한다. 같은 키의 요청 해시가 다르면 conflict를 돌려준다.
4. 트랜잭션 안에서 필요한 담당자/리소스의 가용성 경계를 잠그거나 충돌을 원자적으로 거절할 제약을 사용한다.
5. 가용 요일·예외·수용 인원·외부 바쁜 구간 캐시·버퍼를 다시 계산한다.
6. Appointment·리소스 점유·변경 이력·`AppointmentCreated` Outbox를 함께 기록한다.
7. 커밋 후 성공 응답을 저장하고 돌려준다. 같은 키 재호출에는 같은 예약 ID와 결과를 반환한다.
8. 외부 캘린더 생성·고객 알림은 작업자가 처리한다. 외부 연동 실패가 내부 예약 생성과 별도 상태로 보여야 한다.

충돌 범위는 반열린 구간 `[start-bufferBefore, end+bufferAfter)`를 기준으로 설계한다. 예약 종료와 다음 예약 시작이 같은 순간인 경우 버퍼가 없으면 허용한다. 정원이 2 이상이면 건별 락이 아닌 해당 시간대의 총 점유 검증이 필요하다. 분산 서버에서 애플리케이션 프로세스의 메모리 잠금만 사용하지 않는다.

### 변경·취소

- 변경 요청은 `If-Match/version`으로 최신 예약에 적용되는지 확인한다.
- 기존 구간을 먼저 완전히 해제한 뒤 새 구간 저장에 실패해 예약을 잃는 순서를 피한다. 내부 점유 변경은 하나의 트랜잭션으로 처리한다.
- 변경 버전마다 알림 작업 키를 다르게 하고 오래된 버전의 알림은 실행 직전에 취소 여부를 재검사한다.
- 취소는 반복 요청해도 한 번 취소된 결과를 반환한다. 외부 삭제 실패는 재시도 큐에 남긴다.
- 외부 제공자의 이벤트가 늦게 도착하면 외부 수정 시각·버전·현재 로컬 상태를 비교하고 충돌을 운영 화면에 남긴다.

## 7. 웹훅·Outbox·Inbox

### 7.1 이벤트 봉투

```json
{
  "eventId": "unique-event-id",
  "workspaceId": "workspace-id",
  "eventType": "OpportunityStatusChanged",
  "schemaVersion": 1,
  "entityType": "Opportunity",
  "entityId": "opportunity-id",
  "entityVersion": 7,
  "occurredAt": "2026-10-26T03:00:00Z",
  "actor": {"type": "user", "id": "user-id"},
  "correlationId": "request-or-workflow-run-id",
  "causationId": "parent-event-id",
  "payload": {"from": "open", "to": "won"}
}
```

본문에는 비밀 토큰을 넣지 않는다. 로그나 이벤트를 다른 환경으로 옮길 때 실제 개인 데이터가 필요한지 별도 판단한다.

### 7.2 Outbox

업무 변경과 Outbox insert를 같은 DB 트랜잭션으로 커밋한다. 작업자는 `pending → leased → sent`를 전이하며 lease가 만료되면 다른 작업자가 이어받을 수 있다. 외부 수락 후 sent 표시 전에 작업자가 죽으면 다시 실행될 수 있으므로, 모든 소비자는 중복을 예상한다.

필드: eventId, destination, availableAt, attemptCount, leaseOwner, leaseUntil, lastErrorCode, deliveryState. 재시도 가능한 오류와 최종 실패를 분리하고 dead-letter 목록에서 원인·재시도 여부를 보여준다. 성공한 외부 작업을 재시도할 때 새 메시지나 결제를 만들지 않도록 대상별 멱등 키를 사용한다.

### 7.3 외부 웹훅 Inbox

1. 공급자 서명·허용 연결·본문 크기를 확인한다.
2. 외부 eventId가 있으면 `(provider, connectionId, eventId)`를 고유하게 기록한다. 없는 경우 문서화한 안정 키를 사용한다.
3. 원본 수신 사실을 저장한 뒤 빠르게 수락 응답을 보낸다.
4. 비동기 작업에서 형식·사업장 매핑·현재 버전·상태 전이를 검증한다.
5. 처리 완료/무시/실패/검토 필요와 원인을 기록한다.

수신 웹훅의 workspace 값만으로 사업장을 선택하지 않는다. 서버가 보관한 Connection과 외부 계정 ID의 매핑을 사용한다. 순서가 뒤집힌 이벤트가 올 수 있으므로 상태를 무조건 최신 수신 순으로 덮어쓰지 않는다. 누락 복구를 위해 공급자별 cursor 기반 정기 조정 작업을 둘 수 있다.

## 8. 워크플로 그래프 불변조건과 실행기

### 8.1 정의 검사

- 시작 트리거가 명확하고 모든 노드는 고유한 ID를 가진다.
- 연결선의 source·target 노드가 존재해야 한다.
- 분기별 포트와 조건 타입을 검사한다. 기본 경로 또는 미매칭 처리 정책이 필요하다.
- 접근 불가능한 노드, 종료 없는 경로, 잘못된 참조를 게시 전에 표시한다.
- 기본 그래프는 순환을 금지한다. 반복 기능은 명시적인 반복 노드와 횟수/기간 한도를 갖춘 별도 제어 구조로만 제공한다.
- 도구·필드·템플릿·파이프라인 참조는 같은 workspace에 존재하며 실행 권한과 연결 상태를 확인한다.
- 즉시 전송·게시 같은 외부 작업을 포함하는 그래프는 실제 연결과 실행 모드를 명시한다.
- 유효한 게시 버전은 불변이다. 편집할 때 새 초안/새 버전을 만든다.

### 8.2 실행 의미

`WorkflowRun`은 workspace·정의 버전·입력 이벤트를 고정한다. `StepRun`은 runId·nodeId·iteration·시도 번호를 갖는다. 이벤트 재전달에 같은 run이 여러 번 등록되지 않도록 등록 정책별 고유 키를 둔다. 태그 변경처럼 같은 연락처에 반복되는 이벤트는 ‘한 번만’과 ‘재등록 허용’을 사용자가 구분하게 한다.

각 단계는 가능한 한 다음 형태로 실행한다.

```text
lease 획득 → 현재 run/노드/권한/버전 확인 → 입력 해석
 → 순수 계산 또는 도메인 명령
 → 결과·다음 노드·Outbox를 트랜잭션으로 저장
 → lease 해제
```

외부 API 동작은 별도 작업 상태를 기다린다. Wait 노드는 프로세스를 sleep시키지 않고 `resumeAt`을 저장한다. 작업자가 종료돼도 기다림이 유지돼야 한다. 실행 도중 정의를 수정해도 기존 run의 그래프가 바뀌지 않는다.

### 8.3 실패와 재시도

| 오류 | 처리 |
|---|---|
| 잘못된 설정·없는 필드/템플릿 | needs_review, 설정 수정 후 명시적 재개 |
| 권한 회수·연결 해제 | 중단/검토 필요, 자동 우회 금지 |
| 공급자 429·일시 5xx | 지수 대기·지터·최대 시도/기간 |
| 외부 결과 미확인 타임아웃 | 멱등 키 조회·조정 후 재시도 |
| 연락처 삭제·수신거부 | 해당 동작 건너뜀/종료 정책 기록 |
| 그래프 예산 초과 | failed 또는 needs_review, 원인 경로 표시 |

실패 단계를 재개할 때 이전에 성공한 외부 작업을 다시 수행하지 않도록 step effect key를 보관한다. 실행 로그에서 성공·대기·건너뜀·실패·수동 개입을 구분한다.

## 9. Import 엔진의 원자성과 복구

파일 원본은 불변으로 보관하고 SHA-256과 크기를 저장한다. 파싱·매핑·행 검증·대상 매칭·적용은 분리한다. 화면은 처음 몇 행만 보여주더라도 검증 건수와 적용 건수는 전체 파일 기준이어야 한다.

- 파서는 quoted comma/newline, escaped quote, BOM을 처리한다. Excel 계열 내보내기에는 필요하면 수식으로 해석될 위험이 있는 값을 안전하게 표현한다.
- 파일 한 행의 Contact·Opportunity 연결은 하나의 일관된 단위로 적용한다. 어느 객체에 오류가 나면 해당 행을 부분 적용할지 전체 실패할지 정책을 명시한다.
- 대용량은 배치 트랜잭션을 사용하되 import job·row 번호·키를 고정해 재개한다.
- 동일 파일을 다시 올리는 것은 사용자가 의도한 별도 import일 수 있으므로 파일 해시만으로 무조건 거절하지 않는다. 마지막 확정 작업의 멱등 키와 구분한다.
- 중복 처리 정책·빈값 덮어쓰기·필드 스키마 버전을 작업에 저장한다. 작업 중 설정이 바뀌어도 이미 확정한 의미가 바뀌지 않게 한다.
- 오류 CSV는 행 번호·원본 키·안전한 오류 코드를 포함한다. 실제 고객 데이터가 들어가면 일반 소스 백업과 분리한다.
- 되돌리기 기능은 ‘이번 작업이 만든 레코드’와 ‘기존 레코드의 변경’을 구분한다. 이후 다른 작업이 수정한 데이터를 자동 덮어쓰지 않는다.

## 10. 자체 API 계약 제안

다음 경로는 HighLevel 경로가 아니다. 레벨 스튜디오 서버 구현 시 사용할 수 있는 계약 예시다.

| 작업 | 예시 | 주요 요구 |
|---|---|---|
| 연락처 목록 | `GET /api/workspaces/{w}/contacts` | cursor·필터 AST·권한 범위 |
| 연락처 생성/수정 | `POST /contacts`, `PATCH /contacts/{id}` | 입력 검증·version·감사 |
| import 준비 | `POST /imports` | 파일 메타·객체·모드 |
| 매핑 검증 | `POST /imports/{id}/validate` | 매핑/정책 고정과 결과 해시 |
| import 실행 | `POST /imports/{id}/execute` | Idempotency-Key·검증 버전 |
| 예약 | `POST /appointments` | 멱등·원자 충돌 검사 |
| 예약 변경 | `PATCH /appointments/{id}` | version·슬롯 교체 |
| 메시지 초안/실행 | `POST /messages`, `POST /messages/{id}/send` | 초안/외부 실행 구분 |
| 청구서 | `POST /invoices`, `POST /invoices/{id}/issue` | 가격 스냅샷·전이 검증 |
| 워크플로 게시 | `POST /workflows/{id}/publish` | 그래프 검사·고정 버전 |
| 실행 취소/재개 | `POST /runs/{id}/cancel`, `/resume` | 현재 상태·actor 기록 |
| 데이터 내보내기 | `POST /exports` | 범위·권한·암호화 정책 |
| 복원 검사 | `POST /restores/validate` | 스키마 버전·해시·참조 검사 |

응답 오류는 `code`, `message`, `fieldErrors`, `requestId`, `retryable`, 필요시 `currentVersion`을 반환한다. 비밀 연결값이나 DB stack trace를 사용자 오류로 돌려주지 않는다. 목록의 total·pagination 의미를 모든 모듈에서 통일한다.

## 11. 구현 단계별 통과 기준

### P0: 로컬 CRM과 데이터 이동

- Contact CRUD·검색·태그·필터·상세가 같은 저장 어댑터를 사용.
- CSV 업로드→매핑→검토→적용과 오류 로그가 실제 데이터에 작동.
- 새로고침 후 보존, JSON/CSV 내보내기, 다른 깨끗한 환경에 복원 검증.
- 빈값·중복·한글·인용문자·행 일부 실패 시험 통과.
- 브라우저 저장소의 삭제/용량 한계와 외부 미연결 상태를 인계에 기록.

### P1: 업무 연결

- 같은 연락처에 영업기회·예약·대화/청구 초안을 연결.
- Stage와 Status, 예약 시간대, 금액 계산을 분리.
- 대시보드 수치가 실제 레코드와 일치.
- 잘못된 참조·중복 예약·오래된 편집 버전 시험 통과.

### P2: 지속 저장·인증·실행 기반

- 서버 인증·사업장 경계·역할/담당 데이터 통제를 구현.
- Outbox/Inbox·작업 lease·재시도·이력을 도입.
- 프로세스 재시작 후 작업·기다림 상태를 복구.
- 서로 다른 workspace ID 접근, 중복 이벤트, 동시 예약 시험 통과.

### P3: 공급자 실제 연동

- 한 공급자씩 테스트 모드로 연결·권한·만료·웹훅 검증.
- 메시지/결제/예약/소셜 작업의 수락·전달·실패 상태를 확인.
- 외부 결과 미확인·중복 웹훅·순서 역전·재시도에서 중복 실행 방지.
- 앱 창을 닫고 재시작해도 예약 작업이 유지되는지 확인.

### P4: 고급 자동화·콘텐츠·AI·Agency

- 게시 버전 불변·실행 로그·사람 검토·도구 권한 재검사.
- 사이트/강의/커뮤니티의 비공개 접근을 서버에서 통제.
- 사용량/비용과 구독·접근권 원장을 연결.
- 템플릿 복사와 실제 데이터 백업을 구별하고 복원 시험 통과.

## 12. 검증 시나리오

| 영역 | 최소 의미 있는 시험 | 통과 조건 |
|---|---|---|
| Tenant | A 사용자로 B ID 조회/수정/파일/집계 요청 | 모든 통로에서 데이터 차단 |
| 권한 | 담당 데이터 제한 후 검색·출력·보고서 조회 | 같은 범위 적용 |
| Import | 동일 확정 작업을 2회 실행 | 결과/레코드 중복 없음 |
| Import | 여러 행 중 오류·충돌 혼합 | 행별 결과와 실제 적용 수 일치 |
| 예약 | 같은 슬롯 동시 20요청 | 정원만큼만 성공 |
| 예약 | 같은 멱등 키를 다른 요청 본문에 재사용 | conflict·기존 결과 보존 |
| 예약 | 확정 후 변경/취소와 오래된 알림 실행 | 옛 알림 전송 없음 |
| Outbox | 공급자 수락 직후 작업자 종료 | 재개 후 외부 중복 없음 |
| Webhook | 같은 이벤트 3회·역순 이벤트 | 원장/상태 중복·역행 없음 |
| Workflow | 대기 중 재시작·새 버전 게시 | 기존 실행은 고정 버전으로 재개 |
| Workflow | 순환·없는 노드·다른 workspace 참조 | 게시 전에 거절 |
| 결제 | 성공/환불 웹훅 중복·역순 | 납부·환불 원장 일치 |
| 보고서 | 0건·기간 경계·영업 재개 | 계산식과 원본 목록 일치 |
| 복원 | 새 clone과 빈 DB/파일 저장소 | 문서만으로 실행·데이터 관계 복구 |

UI의 세부 스타일만 바꾸는 낮은 위험 작업에 불필요한 테스트를 늘리지 않는다. 데이터 무결성·권한·외부 실행·복원과 관련된 위 시나리오는 상태 전이와 경쟁 조건을 실제로 검증해야 한다.

## 13. 개발 인계·백업 계약

GitHub에는 전체 자체 소스·UI·의존성 잠금·설정 예시·환경 준비·실행/빌드/검증·미완료 목록을 보관한다. `.env`·키·토큰·실제 고객 데이터·HighLevel 계정 세션은 올리지 않는다. 원본 제품의 공개 스크립트/자산은 자체 구현 소스와 섞지 않고 근거 보관 범위를 구분한다.

E 백업은 사용자의 프로젝트별 기존 구조를 우선 사용하고 새 버전 경로에 보관한다. 이전 파일을 제자리 덮어쓰지 않는다. 파일별 경로·크기·SHA-256·출처·버전·소스 커밋·실행/복원 절차를 남긴다. 이미 동일한 파일이 있으면 검증 후 재사용하고, 남은 용량을 먼저 확인한다. 100GB 미만 또는 E 공간 부족은 사용자에게 알리고 정리는 승인 없이 수행하지 않는다.

복원 검증은 최소한 다음을 통과해야 한다.

1. 새 경로에서 GitHub clone → 잠금 파일 기준 의존성 준비.
2. 안전한 설정 예시를 복사하고 비밀 백업에서 필요한 값만 주입.
3. DB 마이그레이션 및 저장소 초기화.
4. 파일 해시·스키마 버전·관계 무결성 검사 후 데이터 복원.
5. 테스트 모드/외부 실행 비활성 상태로 UI와 조회를 확인.
6. 공급자 연결을 단계적으로 다시 인증하고 작업 재개 여부 확인.
7. 검증 결과·현재 커밋·미확인 기능을 인계 문서에 갱신.

이 계약은 작업 이정표별 보관 절차다. 별도로 설치하고 시험한 상주 서비스/예약 작업이 없으면 지속 자동 백업이 실행 중이라고 표현하지 않는다.
