Files
sejong-office/packages/frontend/src/views/HomeView.vue
T
2026-07-21 17:36:42 +09:00

314 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import AppShell from '@/components/AppShell.vue';
import FileTypeIcon from '@/components/FileTypeIcon.vue';
import { APP_DISPLAY_NAME } from '@/config/app';
import { useOpenDocument } from '@/composables/useOpenDocument';
import { usePageTitle } from '@/composables/usePageTitle';
import type { BlankDocumentType } from '@/api/client';
const dragging = ref(false);
const router = useRouter();
const { busy, error, openDocument, createAndOpenBlank } = useOpenDocument();
usePageTitle();
const blankTypes: {
type: BlankDocumentType;
title: string;
subtitle: string;
}[] = [
{ type: 'docx', title: '문서 (DOCX)', subtitle: '새문서.docx' },
{ type: 'xlsx', title: '스프레드시트 (XLSX)', subtitle: '새문서.xlsx' },
{ type: 'pptx', title: '프레젠테이션 (PPTX)', subtitle: '새문서.pptx' },
{ type: 'pdf', title: 'PDF', subtitle: '새문서.pdf' },
{ type: 'hwpx', title: '한글 문서 (HWPX)', subtitle: '새문서.hwpx' },
];
const diagramTypes = [
{
route: 'excalidraw-editor',
extension: 'excalidraw',
title: '스케치 (Excalidraw)',
subtitle: '새그림.excalidraw',
},
{
route: 'mermaid-editor',
extension: 'mmd',
title: '텍스트 다이어그램 (Mermaid)',
subtitle: '새다이어그램.mmd',
},
{
route: 'drawio-editor',
extension: 'drawio',
title: '다이어그램 (draw.io)',
subtitle: '새다이어그램.drawio',
},
] as const;
function onDrop(e: DragEvent) {
dragging.value = false;
void openDocument(e.dataTransfer?.files?.[0]);
}
function onInput(e: Event) {
const input = e.target as HTMLInputElement;
void openDocument(input.files?.[0]);
input.value = '';
}
function onCreate(type: BlankDocumentType) {
void createAndOpenBlank(type);
}
function onCreateDiagram(route: string) {
void router.push({ name: route });
}
</script>
<template>
<AppShell>
<main class="home">
<header class="home__top">
<h1 class="home__brand">{{ APP_DISPLAY_NAME }}</h1>
<RouterLink class="home__licenses" to="/licenses">라이선스</RouterLink>
</header>
<section class="section">
<h2 class="section__title">파일 열기</h2>
<p class="section__lead">
서버에 영구 저장하지 않습니다. 편집 후 저장(다운로드)하세요.
</p>
<label
class="drop"
:class="{ 'drop--active': dragging, 'drop--busy': busy }"
@dragenter.prevent="dragging = true"
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="onDrop"
>
<input
class="drop__input"
type="file"
accept=".docx,.xlsx,.pptx,.pdf,.hwp,.hwpx,.odt,.ods,.odp,.doc,.xls,.ppt,.rtf,.txt,.csv,.excalidraw,.mmd,.mermaid,.drawio,.xml"
:disabled="busy"
@change="onInput"
/>
<span class="drop__label">
{{ busy ? '여는 중…' : '파일을 끌어다 놓거나 클릭하여 선택' }}
</span>
<span class="drop__hint">
오피스·PDF · HWP/HWPX · 다이어그램
</span>
</label>
<p v-if="error" class="home__error" role="alert">{{ error }}</p>
</section>
<section class="section">
<h2 class="section__title">새 문서</h2>
<p class="section__lead">빈 문서를 만들어 바로 편집합니다.</p>
<div class="grid">
<button
v-for="item in blankTypes"
:key="item.type"
type="button"
class="tile"
:disabled="busy"
@click="onCreate(item.type)"
>
<FileTypeIcon :extension="item.type" :size="36" />
<span class="tile__text">
<strong>{{ item.title }}</strong>
<span>{{ item.subtitle }}</span>
</span>
</button>
</div>
</section>
<section class="section">
<h2 class="section__title">다이어그램</h2>
<p class="section__lead">
브라우저에서 직접 편집하고 원본·SVG·PNG로 저장합니다.
</p>
<div class="grid">
<button
v-for="item in diagramTypes"
:key="item.route"
type="button"
class="tile"
:disabled="busy"
@click="onCreateDiagram(item.route)"
>
<FileTypeIcon :extension="item.extension" :size="36" />
<span class="tile__text">
<strong>{{ item.title }}</strong>
<span>{{ item.subtitle }}</span>
</span>
</button>
</div>
</section>
</main>
</AppShell>
</template>
<style scoped>
.home {
flex: 1;
width: min(920px, 100%);
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
}
.home__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.75rem;
}
.home__brand {
margin: 0;
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--ink);
}
.home__licenses {
font-size: 0.85rem;
color: var(--muted);
text-decoration: none;
}
.home__licenses:hover {
color: var(--accent);
}
.section {
margin-bottom: 2rem;
}
.section__title {
margin: 0 0 0.35rem;
font-size: 1.15rem;
font-weight: 600;
color: var(--ink);
}
.section__lead {
margin: 0 0 1rem;
font-size: 0.9rem;
color: var(--muted);
}
.drop {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: 7.5rem;
padding: 1.5rem 1.25rem;
border: 1.5px dashed var(--border);
border-radius: 8px;
background: var(--card);
cursor: pointer;
transition:
border-color 0.2s ease,
background 0.2s ease;
}
.drop:hover,
.drop--active {
border-color: var(--accent);
background: var(--accent-soft);
}
.drop--busy {
opacity: 0.7;
pointer-events: none;
}
.drop__input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
overflow: hidden;
}
.drop__label {
font-weight: 600;
font-size: 0.95rem;
color: var(--ink);
}
.drop__hint {
font-size: 0.8rem;
color: var(--muted);
}
.home__error {
margin: 0.85rem 0 0;
color: var(--danger);
font-size: 0.9rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
}
.tile {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.9rem 1rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card);
text-align: left;
cursor: pointer;
font-family: inherit;
color: var(--ink);
transition:
background 0.15s ease,
border-color 0.15s ease;
}
.tile:hover:not(:disabled) {
background: var(--card-hover);
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}
.tile:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.tile__text {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.tile__text strong {
font-size: 0.92rem;
font-weight: 600;
}
.tile__text span {
font-size: 0.78rem;
color: var(--muted);
}
</style>