플랫폼별 통합

PHP · Laravel

순수 PHP든 Laravel이든 구조는 같습니다 — 페이지 렌더 시 직전 저장본을 JS 리터럴로 주입하고, 저장은 JSON 엔드포인트로 받습니다.

공통 전제

뷰어 산출물 /pdfv/를 웹 루트(Laravel은 public/pdfv/)에 배포해야 합니다 — 시작하기 참고.

순수 PHP

document-view.php php
<?php
// document-view.php
$doc = getDocument($_GET['id']);                 // 고객사 조회 로직
$latest = getLatestCanvasData($doc['id']);       // 직전 저장본 (없으면 null)
?>
<div id="pdf-container" style="width:100%; height:80vh"></div>

<script src="/pdfv/sdk/pdfv-sdk.js"></script>
<script>
  // json_encode + JSON_HEX_TAG: 특수문자·닫는 스크립트 태그 끊김까지 안전한 JS 리터럴 주입
  var initialCanvasData = <?= json_encode($latest, JSON_HEX_TAG) ?>;

  var viewer = Inko.mount('#pdf-container', {
    src: '/pdfv/index.html',
    pdfUrl: <?= json_encode($doc['file_url'], JSON_HEX_TAG) ?>,
    fileName: <?= json_encode($doc['file_name'], JSON_HEX_TAG) ?>,
    initialCanvasData: initialCanvasData || undefined,

    onSave: function (canvasData, ok) {
      if (!ok) return;
      fetch('/api/annotations.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          docId: <?= (int)$doc['id'] ?>,
          canvasData: canvasData
        })
      });
    }
  });
</script>
인라인 주입은 항상 json_encode

canvasData·파일명 같은 동적 값은 json_encode($v, JSON_HEX_TAG)로 주입하세요. 따옴표·특수문자·</script> 조기 종료 문제를 한 번에 차단합니다.

저장 엔드포인트 (PDO)

api/annotations.php php
<?php
// api/annotations.php — append-only INSERT
header('Content-Type: application/json');

$body = json_decode(file_get_contents('php://input'), true);
$docId = (int)($body['docId'] ?? 0);
$canvasData = $body['canvasData'] ?? '';

if ($docId <= 0 || $canvasData === '') {
  http_response_code(400);
  echo json_encode(['ok' => false]);
  exit;
}

$pdo = new PDO($dsn, $user, $pass);
$stmt = $pdo->prepare(
  'INSERT INTO doc_annotations (doc_id, canvas_data, version)
   VALUES (:doc_id, :canvas_data,
     COALESCE((SELECT MAX(version) FROM doc_annotations d2
               WHERE d2.doc_id = :doc_id2), 0) + 1)'
);
$stmt->execute([
  ':doc_id' => $docId,
  ':canvas_data' => $canvasData,
  ':doc_id2' => $docId,
]);

echo json_encode(['ok' => true]);

Laravel (Blade)

show.blade.php php
{{-- resources/views/documents/show.blade.php --}}
<div id="pdf-container" style="width:100%; height:80vh"></div>

<script src="/pdfv/sdk/pdfv-sdk.js"></script>
<script>
  var viewer = Inko.mount('#pdf-container', {
    src: '/pdfv/index.html',
    pdfUrl: @json($doc->file_url),
    initialCanvasData: @json($latestCanvasData) || undefined,

    onSave: function (canvasData, ok) {
      if (!ok) return;
      fetch('{{ route('annotations.store') }}', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-TOKEN': '{{ csrf_token() }}'   // Laravel CSRF
        },
        body: JSON.stringify({ docId: {{ $doc->id }}, canvasData: canvasData })
      });
    }
  });
</script>
AnnotationController.php php
// app/Http/Controllers/AnnotationController.php
public function store(Request $request)
{
    $validated = $request->validate([
        'docId' => 'required|integer',
        'canvasData' => 'required|string',
    ]);

    DB::table('doc_annotations')->insert([
        'doc_id' => $validated['docId'],
        'canvas_data' => $validated['canvasData'],
        'version' => DB::table('doc_annotations')
            ->where('doc_id', $validated['docId'])->max('version') + 1,
        'created_at' => now(),
    ]);

    return response()->json(['ok' => true]);
}

주의사항

  • Laravel CSRF — Laravel은 CSRF(사이트 간 요청 위조) 방지를 위해 POST 요청에 토큰을 요구합니다. 예제처럼 X-CSRF-TOKEN 헤더를 넣거나, 해당 라우트를 API 미들웨어 그룹으로 두세요.
  • 업로드 크기 — canvasData가 커질 수 있으므로 php.inipost_max_size와 앞단 웹서버 본문 크기 제한을 점검하세요.
  • DB 컬럼 — MySQL은 LONGTEXT, PostgreSQL은 TEXT를 권장합니다.