feat: 홈 일별 캘린더·시세 갱신·게시판 카테고리·약관동의·계좌 마스킹

- 홈: 6월 예산 아래 일별 수입/지출 캘린더(마우스오버/탭 시 내역 목록)
- 투자: 시세 자동조회 버튼/진입 시 갱신, 보유종목 2줄 표기, 평가액 직접입력
- 게시판: 커뮤니티/짠테크 수다방/재테크 팁 분리, 사이드바 영역 구분선
- 회원가입: 이용약관·개인정보 수집 동의(필수 체크)
- 보안: 계좌번호 화면 마스킹(끝 4자리+눈 토글)
- 정기→고정지출, 내역/정기 라디오·sticky 저장, 태그 드래그 정렬
- fix: 모바일웹 새로고침 시 로그인 팝업(restore localStorage 우선)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-04 05:01:33 +09:00
parent 67aa635dd8
commit 4e6a89fd7a
17 changed files with 1022 additions and 136 deletions
+6 -3
View File
@@ -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>
+13 -21
View File
@@ -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;
}
+6 -3
View File
@@ -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 () => {