공동 편집
공동 편집 통합에 따라 예제 실행, 앱 연결, 영속성과 권한 검증을 진행하세요. 구현 세부 정보와 Web/Server 예제는 협업 빠른 시작를 참고하세요.
인증된 비즈니스 API에서 Collaboration Service로 Unit을 생성한 뒤 반환된 ID를 브라우저에서 로드하세요. 협업 빠른 시작에 따라 HTTP, Session Ticket, WebSocket을 설정하고 Database Adapter로 저장하세요. 읽기, JOIN, 수정은 서버 권한으로 각각 보호해야 합니다.
문서 로드
문서를 로드하기 전에 현재 사용자를 설정하세요. 현재 사용자는 공동 편집의 신원과 로컬 오프라인 캐시의 소유자를 결정합니다. 설정한 뒤 Facade로 명시적으로 로드합니다.
import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const collaboration = univerAPI.getCollaboration()const unit = await collaboration.loadSheetAsync('your-unit-id')동기화 상태, 재전송, 충돌 복구로 저장 상태를 표시하고 새로고침 전에 미확인 변경을 보호하세요.
공동 편집 문서의 기본 URL 로드 기능 제거
주의 사항
1.2.0 버전부터 Univer의 기본 URL 공동 편집 문서 로드 로직이 제거되었습니다. 더 이상 URL의 unit, type, subunit 매개변수를 자동으로 읽어 공동 편집 문서를 로드하고 워크시트를 활성화하지 않습니다.
애플리케이션이 이 동작에 의존한다면 사용자 정의 플러그인으로 복원할 수 있습니다.
사용자 정의 플러그인 연결
아래 네 파일을 다음 디렉터리 구조에 맞춰 프로젝트에 복사하세요.
custom-url-load/├── plugin.ts├── controllers/│ └── data-loader.controller.ts└── services/ ├── url.service.ts └── web-url.service.ts편집기와 공동 편집 플러그인을 초기화한 후 현재 사용자를 설정하고 CustomUrlLoadPlugin을 등록하세요. 플러그인 모드와 프리셋 모드 모두 이 방식을 사용할 수 있습니다.
import { UserManagerService } from '@univerjs/core'import { CustomUrlLoadPlugin } from './custom-url-load/plugin'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })univer.registerPlugin(CustomUrlLoadPlugin)등록 후에는 플러그인이 문서를 로드하므로 같은 문서에 대해 loadSheetAsync()을 호출할 필요가 없습니다. 이미 Facade로 문서를 로드하는 애플리케이션이라면 이전 절의 방식을 유지하고 애플리케이션 요구 사항에 맞춰 URL을 처리할 수 있습니다.
전체 플러그인 코드
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'import { CollaborationDataLoaderController } from './controllers/data-loader.controller'import { IURLService } from './services/url.service'import { WebURLService } from './services/web-url.service'@DependentOn(UniverCollaborationClientPlugin)export class CustomUrlLoadPlugin extends Plugin { static override pluginName = 'CUSTOM_URL_LOAD_PLUGIN' constructor( _config: undefined, @Inject(Injector) protected override readonly _injector: Injector ) { super() } override onStarting(): void { this._injector.add([IURLService, { useClass: WebURLService }]) this._injector.add([CollaborationDataLoaderController]) this.disposeWithMe(this._injector.get(IURLService)) this.disposeWithMe(this._injector.get(CollaborationDataLoaderController)) }}import type { ISetWorksheetActivateCommandParams } from '@univerjs/sheets'import type { Subscription } from 'rxjs'import { DataLoaderService } from '@univerjs-pro/collaboration-client'import { ICommandService, ILogService, Inject, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType, Workbook,} from '@univerjs/core'import { SetWorksheetActivateCommand } from '@univerjs/sheets'import { takeUntil } from 'rxjs'import { IURLService } from '../services/url.service'/** * URL 매개변수로 공동 편집 문서를 로드하고 같은 문서 내 워크시트 탐색을 동기화한다. */export class CollaborationDataLoaderController extends RxDisposable { private _unitId: string | undefined private _unitType = UniverInstanceType.UNIVER_SHEET private _workbook: Workbook | null = null private _activeSheetSubscription: Subscription | null = null constructor( @IURLService private readonly _urlService: IURLService, @ILogService private readonly _logService: ILogService, @ICommandService private readonly _commandService: ICommandService, @Inject(DataLoaderService) private readonly _dataLoaderService: DataLoaderService, @IUniverInstanceService private readonly _instanceService: IUniverInstanceService ) { super() this._init() } private _init(): void { this._unitId = this._urlService.getParam('unit') const type = this._urlService.getParam('type') if (!this._unitId || !type) { return } this._unitType = Number(type) this.disposeWithMe(toDisposable(() => this._unbindWorkbook())) // 이력 복원 시 문서 ID를 유지하면서 Workbook이 교체될 수 있으므로 다시 바인딩한다. this._dataLoaderService.unitLoaded$.pipe(takeUntil(this.dispose$)).subscribe((unit) => { if (unit instanceof Workbook && unit.getUnitId() === this._unitId) { this._bindWorkbook(unit).catch((error: unknown) => this._reportError(error)) } }) this._instanceService.unitDisposed$.pipe(takeUntil(this.dispose$)).subscribe((unit) => { if (unit === this._workbook) { this._unbindWorkbook() } }) this._urlService.urlChange$.pipe(takeUntil(this.dispose$)).subscribe(() => { if (this._workbook) { this._activateWorksheetFromUrl(this._workbook).catch((error: unknown) => this._reportError(error)) } }) this._dataLoaderService.loadUnit(this._unitId, this._unitType, this._urlService.getParam('subunit')) .catch((error: unknown) => this._reportError(error)) } private async _bindWorkbook(workbook: Workbook): Promise<void> { this._unbindWorkbook() this._workbook = workbook // 스냅샷 재생은 모델을 직접 갱신하므로 워크시트를 한 번 활성화하여 렌더러에 알린다. await this._activateWorksheetFromUrl(workbook, true) if (this._workbook !== workbook || this._disposed) { return } this._activeSheetSubscription = workbook.activeSheet$.subscribe((worksheet) => { if (worksheet) { this._updateUrl(worksheet.getSheetId()) } }) } private _unbindWorkbook(): void { this._activeSheetSubscription?.unsubscribe() this._activeSheetSubscription = null this._workbook = null } private _matchesDocument(): boolean { return this._urlService.getParam('unit') === this._unitId && Number(this._urlService.getParam('type')) === this._unitType } private async _activateWorksheetFromUrl(workbook: Workbook, forceActivate = false): Promise<void> { if (this._disposed || this._workbook !== workbook || !this._matchesDocument()) { return } const requestedSheetId = this._urlService.getParam('subunit') const worksheet = (requestedSheetId ? workbook.getSheetBySheetId(requestedSheetId) : null) ?? workbook.getSheetBySheetId(workbook.getUnhiddenWorksheets()[0]) if (!worksheet) { return } const subUnitId = worksheet.getSheetId() if (subUnitId !== requestedSheetId) { this._updateUrl(subUnitId, true) } if (!forceActivate && workbook.getActiveSheet()?.getSheetId() === subUnitId) { return } await this._commandService.executeCommand<ISetWorksheetActivateCommandParams>(SetWorksheetActivateCommand.id, { unitId: workbook.getUnitId(), subUnitId, }) } private _updateUrl(subUnitId: string, replace = false): void { if (this._matchesDocument() && this._urlService.getParam('subunit') !== subUnitId) { this._urlService.setParam('subunit', subUnitId, replace) } } private _reportError(error: unknown): void { this._logService.error('[CollaborationDataLoaderController]', error) }}import type { IDisposable } from '@univerjs/core'import type { Observable } from 'rxjs'import { createIdentifier } from '@univerjs/core'export interface IURLService extends IDisposable { getParam: (key: string) => string | undefined setParam: (key: string, value: string, useReplace?: boolean) => void removeParam: (key: string, useReplace?: boolean) => void readonly urlChange$: Observable<void>}export const IURLService = createIdentifier<IURLService>('demo.custom-url-load.url-service')import type { IURLService } from './url.service'import { RxDisposable } from '@univerjs/core'import { fromEvent, map, shareReplay, takeUntil } from 'rxjs'export class WebURLService extends RxDisposable implements IURLService { readonly urlChange$ = fromEvent(window, 'popstate').pipe( map(() => undefined), takeUntil(this.dispose$), shareReplay({ bufferSize: 1, refCount: true }) ) getParam(key: string): string | undefined { return new URL(window.location.href).searchParams.get(key) ?? undefined } setParam(key: string, value: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.set(key, value) this._updateUrl(url, useReplace) } removeParam(key: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.delete(key) this._updateUrl(url, useReplace) } private _updateUrl(url: URL, useReplace: boolean): void { if (useReplace) { window.history.replaceState(window.history.state, '', url.toString()) } else { window.history.pushState(window.history.state, '', url.toString()) } }}이 가이드는 어떠셨나요?