Migrate to Euro-Office and refresh the editor home experience.

Swap Document Server to Euro-Office, add blank-document creation, and restyle the app with an Office-like light/dark theme and compact editor toolbar.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ysj7705andCursor committed 2026-07-16 17:47:06 +09:00
1 parent 5ac6281304
commit 0800fc8571
27 files changed
+735 -394

No files matched your search

+77 -62
View File
@@ -4,19 +4,24 @@ import { useRouter } from 'vue-router';
import { createEditor, type RhwpEditor } from '@rhwp/editor';
import AppShell from '@/components/AppShell.vue';
import EditorErrorState from '@/components/EditorErrorState.vue';
import EditorToolbar from '@/components/EditorToolbar.vue';
import { ApiError } from '@/api/client';
import { extensionOf, isHwp } from '@/config/app';
import { usePageTitle } from '@/composables/usePageTitle';
import {
clearHwpFile,
loadHwpFile,
storeHwpFile,
useSessionLifecycle,
} from '@/composables/useSessionLifecycle';
import { downloadBlob } from '@/utils/downloadBlob';
defineOptions({ name: 'RhwpEditorView' });
const router = useRouter();
const error = ref<unknown>(null);
const filename = ref('문서.hwp');
const fileInput = ref<HTMLInputElement | null>(null);
let editor: RhwpEditor | null = null;
let initialized = false;
@@ -57,28 +62,76 @@ onUnmounted(() => {
initialized = false;
});
function goHome() {
function onOpen() {
fileInput.value?.click();
}
async function onFileSelected(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file || !editor) return;
if (!isHwp(file.name)) {
error.value = new ApiError(
400,
'unsupported_type',
'HWP/HWPX 파일만 열 수 있습니다.',
);
return;
}
try {
await storeHwpFile(file);
const stored = loadHwpFile();
if (!stored) throw new Error('파일을 읽을 수 없습니다.');
await editor.loadFile(stored.buffer, stored.name);
filename.value = stored.name;
error.value = null;
} catch (err) {
error.value = err;
}
}
async function onSave() {
if (!editor) return;
try {
const ext = extensionOf(filename.value);
const bytes =
ext === 'hwpx' ? await editor.exportHwpx() : await editor.exportHwp();
const name =
ext === 'hwpx' || ext === 'hwp'
? filename.value
: `${filename.value}.hwp`;
downloadBlob(bytes, name);
} catch (err) {
error.value = err;
}
}
function onClose() {
clearHwpFile();
void router.push({ name: 'home' });
}
</script>
<template>
<AppShell :compact-footer="true">
<AppShell :editor="true">
<div class="editor-page">
<header v-if="!error" class="bar">
<button type="button" class="bar__btn bar__btn--ghost" @click="goHome">
← 홈
</button>
<div class="bar__meta">
<strong>{{ filename }}</strong>
<span>
서버에 영구 저장되지 않습니다. 닫기 전에 다운로드하세요.
<em>파일 → 저장</em>
(조판은 한컴과 다를 수 있습니다)
</span>
</div>
</header>
<EditorToolbar
v-if="!error"
:filename="filename"
@open="onOpen"
@save="onSave"
@close="onClose"
/>
<input
ref="fileInput"
class="sr-only"
type="file"
accept=".hwp,.hwpx"
@change="onFileSelected"
/>
<EditorErrorState v-if="error" :error="error" context="rhwp" />
<div v-show="!error" id="rhwp-host" class="editor-frame" />
@@ -96,54 +149,16 @@ function goHome() {
background: var(--canvas);
}
.bar {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.bar__meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 12rem;
}
.bar__meta strong {
font-size: 0.95rem;
}
.bar__meta span {
font-size: 0.75rem;
color: var(--muted);
}
.bar__meta em {
font-style: normal;
font-weight: 600;
color: var(--accent);
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
.bar__btn {
border: none;
padding: 0.5rem 0.95rem;
border-radius: 3px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
.bar__btn--ghost {
background: transparent;
color: var(--ink);
border: 1px solid var(--border);
border: 0;
}
.editor-frame {