외부 시스템이 SmartOffice Online 편집기로 문서를 여는 방식은 WOPI(Web Application Open Platform Interface) 입니다. 편집기는 파일을 직접 갖고 있지 않고, 여러분의 시스템(WOPI 호스트)에게 파일을 달라고 하고 저장할 때 돌려줍니다.
<iframe> 으로 띄웁니다.CheckFileInfo 로 파일 정보(이름·크기·권한)를 묻습니다.GetFile 로 파일 내용을 받아 엽니다.PutFile 로 바뀐 내용을 보냅니다. ┌───────────┐ ① 편집기 URL(WOPISrc + token) ┌────────────┐
│ 내 시스템 │ ────────────────────────────────▶ │ 브라우저 │
│ (WOPI │ │ (iframe) │
│ 호스트) │ ◀─────────────────────────────── └─────┬──────┘
└─────┬─────┘ ③ CheckFileInfo (파일 정보) │
│ ④ GetFile (파일 내용) │ ② 편집기 로드
│ ⑤ PutFile (저장) ▼
│ ┌──────────────┐
└───────────────────────────────────────▶│ office.smart │
서버 ↔ 서버 (브라우저 아님) └──────────────┘
③④⑤ 는 편집기 서버가 내 시스템으로 직접 부르는 요청입니다. 브라우저를 거치지 않으므로, 내 시스템의 WOPI 주소가 편집기 서버에서 닿는 곳이어야 합니다(사설망·localhost 주의).
| 누가 | 경로 | 하는 일 |
|---|---|---|
| 편집기 제공 | GET /hosting/discovery | 확장자별 편집기 진입 URL 목록(XML) |
| 편집기 제공 | GET /hosting/capabilities | 제품 이름·버전·기능(JSON) |
| 편집기 제공 | POST /cool/convert-to/<형식> | WOPI 없이 파일 변환 |
| 내가 구현 | GET /wopi/files/<id> | CheckFileInfo — 파일 정보 |
| 내가 구현 | GET /wopi/files/<id>/contents | GetFile — 파일 내용 |
| 내가 구현 | POST /wopi/files/<id>/contents | PutFile — 저장 |
편집기가 어떤 확장자를 어떤 URL 로 여는지 알려 주는 목록입니다. 연동을 시작할 때 한 번 받아 캐시해 두고, 편집기를 올릴 때마다 갱신하면 됩니다(빌드 해시가 URL 에 들어가므로 버전이 바뀌면 값도 바뀝니다).
curl -s https://office.smartertools.co.kr/hosting/discovery
응답(발췌) — urlsrc 가 편집기 진입 주소입니다.
<wopi-discovery>
<net-zone name="external-https">
<app name="application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<action ext="docx" name="edit"
urlsrc="https://office.smartertools.co.kr/browser/<해시>/cool.html?"/>
</app>
<app name="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
<action ext="xlsx" name="edit" urlsrc="…"/>
</app>
</net-zone>
</wopi-discovery>curl -s https://office.smartertools.co.kr/hosting/capabilities
{
"productName": "SmartOffice Online",
"productVersion": "25.04.10.3",
"convert-to": { "available": true, "endpoint": "/cool/convert-to" },
"hasMobileSupport": true,
"hasTemplateSource": true
}
내 시스템이 만들어야 하는 세 개의 엔드포인트입니다. 편집기는 요청마다
access_token 을 쿼리(또는 Authorization: Bearer)로 함께 보냅니다 — 그 값으로 누가 어떤 파일에
접근하는지 확인하세요.
{
"BaseFileName": "보고서.docx", // 확장자 포함 (편집기가 이걸로 형식을 정함)
"Size": 20480, // 바이트
"OwnerId": "user-1",
"UserId": "user-1",
"UserFriendlyName": "홍길동", // 편집 중 표시되는 이름
"UserCanWrite": true, // false 면 읽기 전용으로 열림
"UserCanNotWriteRelative": true, // "다른 이름으로 저장" 안 쓸 때
"LastModifiedTime": "2026-09-05T06:00:00.000Z",
"Version": "v3", // 내용이 바뀌면 반드시 함께 바뀌어야 함
"PostMessageOrigin": "https://my-app.example.com" // 편집기→내 페이지 메시지 허용 출처
}
흔한 실수 — BaseFileName 에 확장자가 없으면 편집기가 형식을 몰라 열지 못합니다.
Version 을 고정값으로 두면 저장 후 편집기가 옛 내용을 계속 씁니다.
파일 바이트를 그대로 돌려줍니다(application/octet-stream). JSON 으로 감싸면 안 됩니다.
본문이 저장할 파일 바이트입니다. 성공하면 200 과 함께
새 {"LastModifiedTime": …} 를 돌려주면 됩니다.
const express = require('express');
const fs = require('fs');
const app = express();
const FILE = '/srv/docs/보고서.docx';
const TOKEN = 'demo-token'; // 실제로는 사용자·문서별 일회용 토큰
// 편집기가 보내는 토큰 확인 (쿼리 또는 Authorization 헤더)
function auth(req, res, next) {
const t = req.query.access_token ||
(req.headers.authorization || '').replace(/^Bearer /, '');
if (t !== TOKEN) return res.sendStatus(401);
next();
}
// 1) CheckFileInfo
app.get('/wopi/files/:id', auth, (req, res) => {
const st = fs.statSync(FILE);
res.json({
BaseFileName: '보고서.docx',
Size: st.size,
OwnerId: 'user-1',
UserId: 'user-1',
UserFriendlyName: '홍길동',
UserCanWrite: true,
UserCanNotWriteRelative: true,
LastModifiedTime: st.mtime.toISOString(),
Version: String(st.mtimeMs), // 저장할 때마다 바뀌어야 한다
});
});
// 2) GetFile
app.get('/wopi/files/:id/contents', auth, (req, res) => {
res.type('application/octet-stream');
fs.createReadStream(FILE).pipe(res);
});
// 3) PutFile
app.post('/wopi/files/:id/contents', auth,
express.raw({ type: '*/*', limit: '100mb' }), (req, res) => {
fs.writeFileSync(FILE, req.body);
res.json({ LastModifiedTime: new Date().toISOString() });
});
app.listen(3030);
편집기는 <iframe> 으로 띄웁니다. 주소에 WOPISrc(내 WOPI 파일 주소, URL 인코딩)와
access_token 을 붙입니다.
https://office.smartertools.co.kr/browser/<해시>/cool.html
?WOPISrc=https%3A%2F%2Fmy-app.example.com%2Fwopi%2Ffiles%2F123
&access_token=demo-token
&lang=ko-KR
&ui_lang=ko-KR
<해시> 는 Discovery 의 urlsrc 에서 가져오거나
/hosting/capabilities 의 productVersionHash 를 씁니다.
<iframe id="office" style="width:100%;height:100vh;border:0"
allow="clipboard-read *; clipboard-write *"></iframe>
<script>
(async () => {
const OFFICE = 'https://office.smartertools.co.kr';
const WOPISRC = 'https://my-app.example.com/wopi/files/123';
const TOKEN = 'demo-token';
// 편집기 빌드 해시를 받아 URL 을 만든다
const caps = await fetch(OFFICE + '/hosting/capabilities').then(r => r.json());
const url = OFFICE + '/browser/' + caps.productVersionHash + '/cool.html'
+ '?WOPISrc=' + encodeURIComponent(WOPISRC)
+ '&access_token=' + encodeURIComponent(TOKEN)
+ '&lang=ko-KR&ui_lang=ko-KR';
document.getElementById('office').src = url;
})();
</script>
토큰이 길거나 URL 에 남기고 싶지 않으면 <form method="POST"> 로
access_token 을 본문에 담아 iframe 을 target 으로 보내는 방식도 지원합니다.
CheckFileInfo 에 PostMessageOrigin 을 넣으면 편집기가 내 페이지로 상태를 보냅니다
(문서 로드 완료, 수정됨, 저장됨 등). 저장 버튼을 내 UI 에 두고 싶을 때 씁니다.
window.addEventListener('message', (e) => {
if (e.origin !== 'https://office.smartertools.co.kr') return; // 출처 확인은 필수
const msg = JSON.parse(e.data);
if (msg.MessageId === 'App_LoadingStatus' &&
msg.Values.Status === 'Document_Loaded') { /* 로드 완료 */ }
if (msg.MessageId === 'Doc_ModifiedStatus') { /* 수정 여부 */ }
});
// 내 UI 에서 저장시키기
document.getElementById('office').contentWindow.postMessage(
JSON.stringify({ MessageId: 'Action_Save', Values: { Notify: true } }),
'https://office.smartertools.co.kr');문서를 편집하지 않고 변환만 할 때는 WOPI 호스트가 필요 없습니다. 파일을 올리면 변환된 파일이 바로 돌아옵니다.
# docx → pdf
curl -F "data=@보고서.docx" \
https://office.smartertools.co.kr/cool/convert-to/pdf \
-o 보고서.pdf
# xlsx → csv, pptx → pdf, docx → odt … 형식만 바꾸면 된다
curl -F "data=@표.xlsx" https://office.smartertools.co.kr/cool/convert-to/csv -o 표.csv// Node.js
const fd = new FormData();
fd.append('data', new Blob([buf]), '보고서.docx');
const pdf = await fetch('https://office.smartertools.co.kr/cool/convert-to/pdf', {
method: 'POST', body: fd,
}).then(r => r.arrayBuffer());변환도 허용된 호스트에서만 받습니다(아래 «필요한 설정» 참고). 큰 파일은 시간이 걸리니 앞단 프록시의 타임아웃을 넉넉히 주세요.
연동하려면 편집기 쪽에도 설정이 필요합니다. 아래 항목이 안 맞으면 문서가 열리지 않거나 «Failed to load document» 로 끝납니다.
storage > wopi 목록에 넣습니다.
여기 없는 곳에서 온 WOPISrc 는 거부됩니다.
<storage>
<wopi desc="WOPI settings">
<alias_groups mode="groups">
<group>
<host allow="true">https://my-app\.example\.com</host>
</group>
</alias_groups>
</wopi>
</storage>\. 로 escape 하세요.
net > frame_ancestors 에 https://my-app.example.com 을 넣습니다.
(스킴을 빼면 https 로만 해석되니 http 로 접속한다면 http://… 도 함께.)
/cool 에 웹소켓 업그레이드
(Upgrade·Connection)를 넘기고 proxy_read_timeout 을 넉넉히(예 36000s),
client_max_body_size 를 파일 크기에 맞게 올리세요.
localhost·사설 IP 를 WOPISrc 에 쓰면 편집기가 못 찾습니다.
| 공개 주소 | https://office.smartertools.co.kr |
| 콘솔이 쓰는 주소 | http://smoffice-orig:9980 |
| Discovery | https://office.smartertools.co.kr/hosting/discovery |
| Capabilities | https://office.smartertools.co.kr/hosting/capabilities |