- Luckysheet modal: 8-direction resize handles (edge + corner), title bar drag-to-move - First drag/resize converts from flex-centered to absolute positioning - Minimize/close reset to centered state on reopen - New xlsx-viewer.html: standalone Luckysheet page for opening xlsx in separate window - Added "↗ 새 창" button on xlsx cards to open viewer in a resizable popup window - Viewer shares session cookies so auth works transparently Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
6.1 KiB
HTML
140 lines
6.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>스프레드시트</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/css/pluginsCss.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/plugins.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/css/luckysheet.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/assets/iconfont/iconfont.css">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #fff; font-family: 'Nanum Gothic', sans-serif; }
|
|
#toolbar {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 7px 14px; background: #f5f5f5; border-bottom: 1px solid #ddd;
|
|
flex-shrink: 0; height: 46px;
|
|
}
|
|
#toolbar h1 { font-size: 14px; font-weight: 700; color: #222; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.tb-btn {
|
|
background: linear-gradient(135deg, #3b82f6, #2563eb); color: #fff;
|
|
border: none; border-radius: 6px; padding: 5px 14px;
|
|
font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit;
|
|
}
|
|
.tb-btn:disabled { opacity: 0.6; cursor: default; }
|
|
#container {
|
|
position: absolute; top: 46px; left: 0; right: 0; bottom: 0;
|
|
color-scheme: light;
|
|
}
|
|
#container, #container * { font-family: 'Nanum Gothic', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; -webkit-font-smoothing: antialiased; }
|
|
#container .iconfont { font-family: 'iconfont' !important; }
|
|
#loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 13px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="toolbar">
|
|
<h1 id="title">스프레드시트</h1>
|
|
<button class="tb-btn" id="save-btn" onclick="saveFile()">💾 저장</button>
|
|
</div>
|
|
<div id="container">
|
|
<div id="loading">Luckysheet 로딩 중… (최초 1회 ~3MB)</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/js/plugin.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/luckysheet.umd.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/luckyexcel@1.0.1/dist/luckyexcel.umd.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
<script>
|
|
(function() {
|
|
var params = new URLSearchParams(location.search);
|
|
var fileUrl = params.get('file');
|
|
var saveUrl = params.get('save') || fileUrl;
|
|
var title = decodeURIComponent(params.get('title') || '스프레드시트');
|
|
|
|
document.getElementById('title').textContent = title;
|
|
document.title = title;
|
|
|
|
function showError(msg) {
|
|
document.getElementById('container').innerHTML =
|
|
'<div style="padding:24px;color:#b91c1c;font-size:13px">' + msg + '</div>';
|
|
}
|
|
|
|
if (!fileUrl) { showError('파일 URL이 없습니다.'); return; }
|
|
|
|
fetch(fileUrl, { credentials: 'include' })
|
|
.then(function(r) {
|
|
if (r.status === 401 || r.status === 403) throw new Error('인증 오류 — 메인 탭에서 먼저 로그인하세요.');
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.arrayBuffer();
|
|
})
|
|
.then(function(buf) {
|
|
var file = new File([buf], 'sheet.xlsx', {
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
});
|
|
LuckyExcel.transformExcelToLucky(file, function(exportInfo) {
|
|
document.getElementById('container').innerHTML = '';
|
|
luckysheet.create({
|
|
container: 'container',
|
|
data: exportInfo.sheets,
|
|
title: '',
|
|
lang: 'en',
|
|
showinfobar: false,
|
|
showstatisticBar: false,
|
|
sheetBottomConfig: true,
|
|
allowUpdate: true,
|
|
enableAddRow: true,
|
|
enableAddCol: true,
|
|
showToolbar: true,
|
|
plugins: [],
|
|
defaultFontSize: 11,
|
|
defaultFontName: 'Nanum Gothic',
|
|
});
|
|
}, function(err) {
|
|
showError('Excel 변환 오류: ' + String(err?.message || err));
|
|
});
|
|
})
|
|
.catch(function(e) { showError('불러오기 실패: ' + e.message); });
|
|
|
|
window.saveFile = function() {
|
|
if (!saveUrl) { alert('저장할 파일 경로가 없습니다.'); return; }
|
|
var btn = document.getElementById('save-btn');
|
|
btn.disabled = true; btn.textContent = '저장 중…';
|
|
var relPath = saveUrl.replace(/^\/api\/files\//, '');
|
|
try {
|
|
var sheets = luckysheet.getAllSheets();
|
|
var wb = XLSX.utils.book_new();
|
|
sheets.forEach(function(sheet) {
|
|
var aoa = (sheet.data || []).map(function(row) {
|
|
return (row || []).map(function(cell) { return cell ? cell.v : null; });
|
|
});
|
|
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheet.name || 'Sheet1');
|
|
});
|
|
var buf = XLSX.write(wb, { type: 'array', bookType: 'xlsx' });
|
|
fetch('/api/files/' + relPath, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
body: new Uint8Array(buf),
|
|
}).then(function(r) {
|
|
btn.disabled = false;
|
|
if (r.ok) { btn.textContent = '✅ 저장됨'; setTimeout(function() { btn.textContent = '💾 저장'; }, 2000); }
|
|
else { btn.textContent = '❌ 실패'; setTimeout(function() { btn.textContent = '💾 저장'; }, 2000); }
|
|
}).catch(function(e) {
|
|
btn.disabled = false; btn.textContent = '❌ 오류';
|
|
setTimeout(function() { btn.textContent = '💾 저장'; }, 2000);
|
|
});
|
|
} catch(e) {
|
|
btn.disabled = false; btn.textContent = '❌ 오류';
|
|
setTimeout(function() { btn.textContent = '💾 저장'; }, 2000);
|
|
alert('저장 오류: ' + e.message);
|
|
}
|
|
};
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|