feat: 홈 일별 캘린더·시세 갱신·게시판 카테고리·약관동의·계좌 마스킹
- 홈: 6월 예산 아래 일별 수입/지출 캘린더(마우스오버/탭 시 내역 목록) - 투자: 시세 자동조회 버튼/진입 시 갱신, 보유종목 2줄 표기, 평가액 직접입력 - 게시판: 커뮤니티/짠테크 수다방/재테크 팁 분리, 사이드바 영역 구분선 - 회원가입: 이용약관·개인정보 수집 동의(필수 체크) - 보안: 계좌번호 화면 마스킹(끝 4자리+눈 토글) - 정기→고정지출, 내역/정기 라디오·sticky 저장, 태그 드래그 정렬 - fix: 모바일웹 새로고침 시 로그인 팝업(restore localStorage 우선) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,15 @@ import { formatRelative } from '@/utils/datetime'
|
||||
import MarkdownViewer from '@/components/editor/MarkdownViewer.vue'
|
||||
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import { DEFAULT_BOARD } from '@/constants/boards'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const postId = route.params.id
|
||||
// 게시판(카테고리): URL 우선, 없으면 글의 category, 그래도 없으면 기본
|
||||
const category = computed(() => route.params.category || post.value?.category || DEFAULT_BOARD)
|
||||
const post = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
@@ -49,7 +52,7 @@ async function removePost() {
|
||||
if (!confirm('이 게시글을 삭제하시겠습니까?')) return
|
||||
try {
|
||||
await boardApi.remove(postId)
|
||||
router.replace('/board')
|
||||
router.replace(`/board/${category.value}`)
|
||||
} catch (e) {
|
||||
alert(e.response?.data?.message || '삭제에 실패했습니다.')
|
||||
}
|
||||
@@ -137,14 +140,14 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<IconBtn icon="back" title="목록" @click="router.push('/board')" />
|
||||
<IconBtn icon="back" title="목록" @click="router.push(`/board/${category}`)" />
|
||||
<div class="right-actions">
|
||||
<!-- 관리자 제한/해제 -->
|
||||
<button v-if="isAdmin && !post.blocked" type="button" class="warn" @click="blockPost">열람 제한</button>
|
||||
<button v-if="isAdmin && post.blocked" type="button" class="warn" @click="unblockPost">제한 해제</button>
|
||||
<!-- 작성자/관리자 수정·삭제 -->
|
||||
<template v-if="canModifyPost && !restricted">
|
||||
<IconBtn v-if="!post.blocked" icon="edit" title="수정" @click="router.push(`/board/${post.id}/edit`)" />
|
||||
<IconBtn v-if="!post.blocked" icon="edit" title="수정" @click="router.push(`/board/${category}/${post.id}/edit`)" />
|
||||
<IconBtn icon="trash" title="삭제" variant="danger" @click="removePost" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { boardApi } from '@/api/boardApi'
|
||||
import { formatRelative } from '@/utils/datetime'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { boardLabel, DEFAULT_BOARD } from '@/constants/boards'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -11,6 +12,10 @@ const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const isAdmin = computed(() => auth.user?.role === 'ADMIN')
|
||||
|
||||
// 현재 게시판(카테고리)
|
||||
const category = computed(() => route.params.category || DEFAULT_BOARD)
|
||||
const boardTitle = computed(() => boardLabel(category.value))
|
||||
|
||||
const posts = ref([])
|
||||
const tags = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -38,6 +43,7 @@ async function load() {
|
||||
error.value = null
|
||||
try {
|
||||
const res = await boardApi.list({
|
||||
category: category.value,
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
tag: activeTag.value,
|
||||
@@ -107,14 +113,9 @@ function clearSearch() {
|
||||
pushQuery({ page: 1, tag: activeTag.value })
|
||||
}
|
||||
|
||||
// 목록 표시 순번: 전체건수 기준 내림차순 (최신글이 큰 번호)
|
||||
function rowNumber(index) {
|
||||
return totalElements.value - (page.value - 1) * size.value - index
|
||||
}
|
||||
|
||||
// 쿼리가 바뀔 때마다(검색·페이지 이동·뒤로가기 포함) 상태 동기화 후 재조회
|
||||
// 경로(게시판 전환)·쿼리(검색·페이지·뒤로가기)가 바뀌면 상태 동기화 후 재조회
|
||||
watch(
|
||||
() => route.query,
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
syncFromQuery()
|
||||
load()
|
||||
@@ -128,8 +129,8 @@ onMounted(loadTags)
|
||||
<template>
|
||||
<section class="board">
|
||||
<header class="board-head">
|
||||
<h1>자유게시판</h1>
|
||||
<IconBtn icon="edit" title="글쓰기" variant="primary" @click="router.push('/board/write')" />
|
||||
<h1>{{ boardTitle }}</h1>
|
||||
<IconBtn icon="edit" title="글쓰기" variant="primary" @click="router.push(`/board/${category}/write`)" />
|
||||
</header>
|
||||
|
||||
<div v-if="tags.length" class="tag-filter">
|
||||
@@ -155,7 +156,6 @@ onMounted(loadTags)
|
||||
<table v-else-if="posts.length" class="post-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-id">번호</th>
|
||||
<th>제목</th>
|
||||
<th class="col-author">작성자</th>
|
||||
<th class="col-num">조회</th>
|
||||
@@ -163,8 +163,7 @@ onMounted(loadTags)
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(p, index) in posts" :key="p.id" @click="router.push(`/board/${p.id}`)">
|
||||
<td class="col-id">{{ rowNumber(index) }}</td>
|
||||
<tr v-for="p in posts" :key="p.id" @click="router.push(`/board/${category}/${p.id}`)">
|
||||
<td>
|
||||
<template v-if="p.blocked && !isAdmin">
|
||||
<span class="blocked-msg">🚫 관리자에 의해 제한된 게시글입니다.</span>
|
||||
@@ -291,7 +290,6 @@ button:disabled {
|
||||
.post-table tbody tr:hover {
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
.col-id,
|
||||
.col-num {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
@@ -440,20 +438,14 @@ button:disabled {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
/* 제목(2번째 셀): 첫 줄 전체 */
|
||||
.post-table td:nth-child(2) {
|
||||
/* 제목(첫 번째 셀): 첫 줄 전체 */
|
||||
.post-table td:nth-child(1) {
|
||||
order: 0;
|
||||
width: 100% !important;
|
||||
font-size: 0.97rem;
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.post-table .col-id {
|
||||
order: 1;
|
||||
}
|
||||
.post-table .col-id::before {
|
||||
content: '#';
|
||||
}
|
||||
.post-table .col-author {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { boardApi } from '@/api/boardApi'
|
||||
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import { DEFAULT_BOARD } from '@/constants/boards'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const category = route.params.category || DEFAULT_BOARD
|
||||
const editId = route.params.id || null
|
||||
const isEdit = computed(() => !!editId)
|
||||
|
||||
@@ -62,13 +64,14 @@ async function submit() {
|
||||
const payload = {
|
||||
title: title.value.trim(),
|
||||
content: content.value,
|
||||
category,
|
||||
tagIds: selectedTagIds.value,
|
||||
}
|
||||
try {
|
||||
const saved = isEdit.value
|
||||
? await boardApi.update(editId, payload)
|
||||
: await boardApi.create(payload)
|
||||
router.replace(`/board/${saved.id}`)
|
||||
router.replace(`/board/${category}/${saved.id}`)
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || '저장에 실패했습니다.'
|
||||
} finally {
|
||||
@@ -77,8 +80,8 @@ async function submit() {
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (isEdit.value) router.replace(`/board/${editId}`)
|
||||
else router.replace('/board')
|
||||
if (isEdit.value) router.replace(`/board/${category}/${editId}`)
|
||||
else router.replace(`/board/${category}`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
Reference in New Issue
Block a user