Merge branch 'dev'
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
---
|
||||
|
||||
## 1. 홈 대시보드
|
||||
- **일별 수입·지출 캘린더** 추가 (예산 대비 지출 아래). 날짜에 마우스 오버/탭 시 그 날의 내역 목록 표시.
|
||||
- **일별 수입·지출 캘린더** 추가 (예산 대비 지출 아래). 날짜를 **탭하면 그 날의 내역을 팝업(모달)으로** 표시(시인성 개선·단일 탭 동작).
|
||||
|
||||
## 2. 투자 / 계좌
|
||||
- **시세 자동조회**: 종목코드(6자리)로 네이버 금융에서 현재가 자동 갱신. 투자 탭 진입·포트폴리오 펼침 시 자동, "↻ 시세 갱신" 버튼도 제공.
|
||||
@@ -39,8 +39,41 @@
|
||||
|
||||
---
|
||||
|
||||
# (이어진 작업) 앱 UX · 보안 · 품질 강화
|
||||
|
||||
## 8. 앱 하단 내비게이션
|
||||
- 화면 **하단 고정 아이콘 바**(뒤로 · 앞으로 · 홈 · 새로고침 · 설정). 스크롤과 무관하게 항상 노출, **안드로이드 소프트키 위** 안전영역에 배치.
|
||||
- 사이드바의 '홈' 메뉴 제거(하단 내비로 대체). 로그인 전에는 햄버거 메뉴·네이버 로그인 버튼 숨김.
|
||||
|
||||
## 9. 설정 / 계정정보
|
||||
- **설정 화면**(하단 내비 → 설정): 계정정보 · 앱 버전(`v1.0.0`) · **App Data 삭제**(로그인/캐시 초기화 후 재시작).
|
||||
- **계정정보 상세 → 정보변경**: 비밀번호 재인증 후 **이름 · 이메일 · 비밀번호를 한 화면에서** 변경. 헤더의 비밀번호 변경 아이콘·모달은 제거(진입점 일원화).
|
||||
- **로그인 후 대시보드(`/`)로 이동** 통일.
|
||||
|
||||
## 10. 투자 매매내역
|
||||
- 종목별 매매이력을 **인라인 최근 5건 + "전체 N건 보기" 스크롤 모달**로 정리(길어짐 방지).
|
||||
- **매매 수정 기능** 추가(`PUT /api/account/invest/trades/{id}`). 수정 시 시점별 시뮬레이션으로 **보유수량 음수 방지** 검증.
|
||||
|
||||
## 11. 보안 / 인증 (백엔드)
|
||||
- **세션 DB 이중 저장**(`auth_session`): Redis 유실·재시작에도 로그인 유지(Redis 미스 시 DB에서 복원·재수화).
|
||||
- **무차별 대입 방지**: 실패한 시도만 카운트 — 로그인 IP당 10회/10분, 비밀번호 재인증 회원당 5회/10분 초과 시 429.
|
||||
- **인터셉터 보호 경로 누락 수정**: `verify-password`·`profile`이 인증 없이 500 나던 문제 → 401 정상화.
|
||||
- **전역 예외 처리 강화**: 미처리 500 로그에 메서드+경로 기록(추적성), 잘못된 JSON은 400.
|
||||
|
||||
## 12. 품질 / 배포 파이프라인
|
||||
- **자동화 테스트 도입**: 프론트 **Vitest 32**(스토어·API·로그인 플로우), 백엔드 **JUnit 34**(단위 + `@WebMvcTest` 통합).
|
||||
- **테스트 게이트**: CI와 **배포 워크플로 모두 테스트 실패 시 배포 중단**. 백엔드 배포에 CI와 동일한 MariaDB/Redis 서비스 추가.
|
||||
- 번들: 마크다운 에디터는 라우트 지연 청크임을 명시(경고 한도 현실화).
|
||||
|
||||
## 13. 안드로이드
|
||||
- 에지투에지(`targetSdk 36`) 대응: **콘텐츠 루트에 시스템 바 인셋 패딩** 적용 → 헤더는 상태바 아래, 하단 내비는 소프트키 위.
|
||||
|
||||
---
|
||||
|
||||
## 운영 반영 시 체크
|
||||
- 서버 `/opt/sb-backend/.env`
|
||||
- `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음).
|
||||
- `app_setting` 테이블 — 배포 시 `member.sql` 초기화로 자동 생성(회원가입 허용 플래그).
|
||||
- Redis `appendonly yes` — 세션 영속(이미 적용).
|
||||
- `app_setting` · `auth_session` 테이블 — 배포 시 `member.sql` 초기화로 자동 생성(가입 허용 플래그 / 세션 DB 백업).
|
||||
- Redis `appendonly yes` — 세션 영속(이미 적용). 단, 세션 DB 이중 저장으로 Redis 유실에도 로그인 유지.
|
||||
- 앱 버전 표기는 `package.json` 의 `version`(현재 `1.0.0`)을 단일 출처로 사용.
|
||||
- 배포는 `main` 푸시 → CI·Deploy 워크플로 모두 **테스트 통과해야 진행**(Gitea Actions).
|
||||
|
||||
+118
-40
@@ -66,8 +66,16 @@ const calendarCells = computed(() => {
|
||||
return cells
|
||||
})
|
||||
|
||||
// 마우스 오버 / 탭한 날짜의 내역 목록
|
||||
// 탭한 날짜의 내역을 팝업(모달)으로 표시 — 캘린더 아래가 가려져도 잘 보이게
|
||||
const activeDay = ref(0)
|
||||
const dayModalOpen = ref(false)
|
||||
function openDay(c) {
|
||||
activeDay.value = c.day
|
||||
dayModalOpen.value = true
|
||||
}
|
||||
function closeDayModal() {
|
||||
dayModalOpen.value = false
|
||||
}
|
||||
const activeItems = computed(() => (activeDay.value ? dayMap.value[activeDay.value]?.items || [] : []))
|
||||
const activeDateLabel = computed(() => {
|
||||
if (!activeDay.value) return ''
|
||||
@@ -230,8 +238,7 @@ onMounted(load)
|
||||
type="button"
|
||||
class="cal-cell"
|
||||
:class="{ today: c.day === todayDate, active: c.day === activeDay, has: c.items.length }"
|
||||
@mouseenter="activeDay = c.day"
|
||||
@click="activeDay = activeDay === c.day ? 0 : c.day"
|
||||
@click="openDay(c)"
|
||||
>
|
||||
<span class="cal-day">{{ c.day }}</span>
|
||||
<span v-if="c.income" class="cal-amt income">+{{ won(c.income) }}</span>
|
||||
@@ -240,24 +247,35 @@ onMounted(load)
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 선택/오버한 날짜의 내역 목록 -->
|
||||
<div class="cal-detail" :class="{ open: activeDay && activeItems.length }">
|
||||
<template v-if="activeDay && activeItems.length">
|
||||
<div class="cd-date">{{ activeDateLabel }}</div>
|
||||
<ul class="cd-list">
|
||||
<li v-for="(e, idx) in activeItems" :key="idx" class="cd-item">
|
||||
<span class="cd-cat">{{ itemLabel(e) }}</span>
|
||||
<span v-if="e.memo" class="cd-memo">{{ e.memo }}</span>
|
||||
<span class="cd-amt" :class="{ income: e.type === 'INCOME', expense: e.type === 'EXPENSE' }">
|
||||
{{ itemSign(e.type) }}{{ won(e.amount) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<p v-else class="cd-hint">날짜에 마우스를 올리거나 탭하면 내역이 표시됩니다.</p>
|
||||
</div>
|
||||
<p class="cal-hint">날짜를 탭하면 그 날의 내역이 표시됩니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 일별 내역 팝업 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="dayModalOpen" class="day-modal-backdrop" @click.self="closeDayModal">
|
||||
<div class="day-modal" role="dialog" aria-modal="true" aria-label="일별 내역">
|
||||
<button class="day-modal-close" type="button" aria-label="닫기" @click="closeDayModal">×</button>
|
||||
<div class="dm-date">{{ activeDateLabel }}</div>
|
||||
<div class="dm-sum">
|
||||
<span v-if="dayMap[activeDay]?.income" class="income">수입 +{{ won(dayMap[activeDay].income) }}</span>
|
||||
<span v-if="dayMap[activeDay]?.expense" class="expense">지출 -{{ won(dayMap[activeDay].expense) }}</span>
|
||||
</div>
|
||||
<ul v-if="activeItems.length" class="dm-list">
|
||||
<li v-for="(e, idx) in activeItems" :key="idx" class="dm-item">
|
||||
<span class="dm-cat">{{ itemLabel(e) }}</span>
|
||||
<span v-if="e.memo" class="dm-memo">{{ e.memo }}</span>
|
||||
<span class="dm-amt" :class="{ income: e.type === 'INCOME', expense: e.type === 'EXPENSE' }">
|
||||
{{ itemSign(e.type) }}{{ won(e.amount) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="dm-empty">이 날은 내역이 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- 바로가기 -->
|
||||
<div class="shortcuts">
|
||||
<RouterLink v-for="s in shortcuts" :key="s.to" :to="s.to" class="shortcut">
|
||||
@@ -490,57 +508,117 @@ onMounted(load)
|
||||
.cal-amt.expense {
|
||||
color: #c0392b;
|
||||
}
|
||||
.cal-detail {
|
||||
margin-top: 0.7rem;
|
||||
padding-top: 0.7rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-height: 1.2rem;
|
||||
.cal-hint {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 0.76rem;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
}
|
||||
.cd-date {
|
||||
font-size: 0.82rem;
|
||||
|
||||
/* 일별 내역 팝업 */
|
||||
.day-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 1rem;
|
||||
}
|
||||
.day-modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.4rem 1.25rem 1.25rem;
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.day-modal-close {
|
||||
position: absolute;
|
||||
top: 0.4rem;
|
||||
right: 0.55rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dm-date {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.cd-list {
|
||||
.dm-sum {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
padding-bottom: 0.6rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.dm-sum .income {
|
||||
color: #2e7d32;
|
||||
}
|
||||
.dm-sum .expense {
|
||||
color: #c0392b;
|
||||
}
|
||||
.dm-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.cd-item {
|
||||
.dm-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
.cd-cat {
|
||||
.dm-cat {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cd-memo {
|
||||
.dm-memo {
|
||||
opacity: 0.6;
|
||||
font-size: 0.76rem;
|
||||
font-size: 0.78rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cd-amt {
|
||||
.dm-amt {
|
||||
margin-left: auto;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cd-amt.income {
|
||||
.dm-amt.income {
|
||||
color: #2e7d32;
|
||||
}
|
||||
.cd-amt.expense {
|
||||
.dm-amt.expense {
|
||||
color: #c0392b;
|
||||
}
|
||||
.cd-hint {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.5;
|
||||
margin: 0;
|
||||
.dm-empty {
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.6;
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 바로가기 그리드 */
|
||||
|
||||
Reference in New Issue
Block a user