feat: 가계부·게시판 프론트엔드 구현 + Slim Budget 브랜딩/모바일 대응
- 가계부: 대시보드(예산 대비 지출·분류별 파이·기간별 막대·순자산 추이), 내역(검색·필터·태그), 계좌(은행/카드/대출/투자), 예산, 분류, 정기 거래, 태그, 투자 포트폴리오(종목·매수/매도·평가손익) - 게시판: 목록/상세/작성(마크다운)/댓글/태그/열람제한 - 공통: 인증(Bearer 토큰·401 처리), 모바일 반응형(드로어·아이콘 버튼), Slim Budget 브랜딩(로고/타이틀/푸터), 홈 대시보드 자리 - 문서: docs/FRONTEND.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import Editor from '@toast-ui/editor'
|
||||
import '@toast-ui/editor/dist/toastui-editor.css'
|
||||
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
height: { type: String, default: '400px' },
|
||||
placeholder: { type: String, default: '' },
|
||||
initialEditType: { type: String, default: 'wysiwyg' }, // 'markdown' | 'wysiwyg'
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const el = ref(null)
|
||||
let editor = null
|
||||
|
||||
onMounted(() => {
|
||||
editor = new Editor({
|
||||
el: el.value,
|
||||
height: props.height,
|
||||
initialEditType: props.initialEditType,
|
||||
hideModeSwitch: true, // 마크다운/위지윅 전환 탭 숨김 (WYSIWYG 전용)
|
||||
plugins: [codeSyntaxHighlight], // 코드 구문 강조
|
||||
initialValue: props.modelValue || '',
|
||||
placeholder: props.placeholder,
|
||||
usageStatistics: false,
|
||||
autofocus: false,
|
||||
events: {
|
||||
change: () => emit('update:modelValue', editor.getMarkdown()),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// 외부 값 변경(수정 로드, 제출 후 초기화 등)을 에디터에 반영 (입력 중 루프 방지 가드)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (editor && (val || '') !== editor.getMarkdown()) {
|
||||
editor.setMarkdown(val || '')
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor?.destroy()
|
||||
editor = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import Viewer from '@toast-ui/editor/dist/toastui-editor-viewer'
|
||||
import '@toast-ui/editor/dist/toastui-editor-viewer.css'
|
||||
import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css'
|
||||
|
||||
const props = defineProps({
|
||||
content: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const el = ref(null)
|
||||
let viewer = null
|
||||
|
||||
// 렌더된 코드 블록(pre)에 Copy 버튼 주입
|
||||
function enhanceCodeBlocks() {
|
||||
if (!el.value) return
|
||||
el.value.querySelectorAll('pre').forEach((pre) => {
|
||||
if (pre.querySelector('.code-copy-btn')) return
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.className = 'code-copy-btn'
|
||||
btn.textContent = 'Copy'
|
||||
btn.addEventListener('click', () => {
|
||||
const code = pre.querySelector('code')?.innerText ?? pre.innerText
|
||||
navigator.clipboard?.writeText(code).then(() => {
|
||||
btn.textContent = 'Copied!'
|
||||
setTimeout(() => (btn.textContent = 'Copy'), 1500)
|
||||
})
|
||||
})
|
||||
pre.appendChild(btn)
|
||||
})
|
||||
}
|
||||
|
||||
async function render(markdown) {
|
||||
viewer?.setMarkdown(markdown || '')
|
||||
await nextTick()
|
||||
enhanceCodeBlocks()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
viewer = new Viewer({
|
||||
el: el.value,
|
||||
initialValue: props.content || '',
|
||||
plugins: [codeSyntaxHighlight],
|
||||
})
|
||||
await nextTick()
|
||||
enhanceCodeBlocks()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
(val) => render(val),
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
viewer?.destroy()
|
||||
viewer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="el"></div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user