플랫폼별 통합
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.ini의post_max_size와 앞단 웹서버 본문 크기 제한을 점검하세요. - DB 컬럼 — MySQL은
LONGTEXT, PostgreSQL은TEXT를 권장합니다.