feat: P6 모바일 PWA 보안 강화 — /api/mobile/me/* 서버측 본인 필터
스캐폴드의 클라이언트 agentId 쿼리 필터(변조 가능)를
서버측 강제 필터로 교체. 토큰 → user → agentId 자동 추출.
백엔드 (ga-api):
- MobileMeService — 클라이언트 param 의 agentId/userId 는 무시·덮어쓰기.
agentId 미연결 시 BizException(FORBIDDEN). PageHelper 로 정확한 total 산출.
- MobileMeController — GET /api/mobile/me/{summary,statements,
withdraw-requests,notifications} + POST /notifications/{id}/read.
별도 메뉴 권한 없이 인증만 통과 (PWA 사용자 전체 대상).
ga-mobile-pwa:
- api/statement.ts, api/withdraw.ts → /api/mobile/me/* 호출, agentId 제거
- api/notification.ts 신규 — list/markRead/summary
- Home.tsx — summary() 단일 호출로 3카운트 + 안 읽음 Badge
- NoticeList.tsx — Tabs(내 알림 / 공지). 알림 클릭 markRead + summary 무효화
- Statement/WithdrawList — E403(설계사 미연결) 시 ErrorBlock 안내
라이브 검증:
- ga-api 컴파일 BUILD SUCCESSFUL · 재기동 OK
- PWA tsc PASS · vite build PASS (precache 538KB, gzip 176KB)
- admin(agentId=null): /summary → 200 OK 0/0/0,
/statements → E403 차단 정상, /notifications → 200 OK total=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,31 +2,22 @@ import { Card, List, NavBar, Space, Tag } from 'antd-mobile';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { statementApi } from '@/api/statement';
|
||||
import { withdrawApi } from '@/api/withdraw';
|
||||
import { noticeApi } from '@/api/notice';
|
||||
import { notificationApi } from '@/api/notification';
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const profile = useAuthStore((s) => s.profile);
|
||||
const clear = useAuthStore((s) => s.clear);
|
||||
const agentId = profile?.agentId;
|
||||
|
||||
const statements = useQuery({
|
||||
queryKey: ['home.statements', agentId],
|
||||
enabled: !!agentId,
|
||||
queryFn: () => statementApi.list({ agentId, pageNum: 1, pageSize: 1 }),
|
||||
});
|
||||
const withdraws = useQuery({
|
||||
queryKey: ['home.withdraws', agentId],
|
||||
enabled: !!agentId,
|
||||
queryFn: () => withdrawApi.list({ agentId, pageNum: 1, pageSize: 1 }),
|
||||
});
|
||||
const notices = useQuery({
|
||||
queryKey: ['home.notices'],
|
||||
queryFn: () => noticeApi.active(),
|
||||
const summary = useQuery({
|
||||
queryKey: ['mobile.me.summary'],
|
||||
queryFn: () => notificationApi.summary(),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
|
||||
const s = summary.data;
|
||||
const loading = summary.isLoading;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<NavBar back={null} right={<a onClick={() => { clear(); navigate('/login'); }} style={{ fontSize: 14 }}>로그아웃</a>}>
|
||||
@@ -40,7 +31,7 @@ export default function Home() {
|
||||
<div style={{ color: '#888', fontSize: 13, marginTop: 4 }}>
|
||||
{profile?.agentName ? `${profile.agentName} · ${profile.orgName ?? ''}` : '설계사 정보 없음'}
|
||||
</div>
|
||||
{profile?.roleCodes && (
|
||||
{profile?.roleCodes && profile.roleCodes.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Space wrap>
|
||||
{profile.roleCodes.map((r) => <Tag key={r} color="primary">{r}</Tag>)}
|
||||
@@ -52,32 +43,38 @@ export default function Home() {
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<List header="요약">
|
||||
<List.Item
|
||||
extra={statements.isLoading ? '...' : `총 ${statements.data?.total ?? 0}건`}
|
||||
extra={loading ? '...' : `총 ${s?.statementCount ?? 0}건`}
|
||||
onClick={() => navigate('/statement')}
|
||||
clickable
|
||||
>
|
||||
정산 명세서
|
||||
</List.Item>
|
||||
<List.Item
|
||||
extra={withdraws.isLoading ? '...' : `총 ${withdraws.data?.total ?? 0}건`}
|
||||
extra={loading ? '...' : `총 ${s?.withdrawRequestCount ?? 0}건`}
|
||||
onClick={() => navigate('/withdraw')}
|
||||
clickable
|
||||
>
|
||||
출금 신청
|
||||
</List.Item>
|
||||
<List.Item
|
||||
extra={notices.isLoading ? '...' : `${notices.data?.length ?? 0}건`}
|
||||
extra={
|
||||
loading
|
||||
? '...'
|
||||
: (s?.unreadNotificationCount ?? 0) > 0
|
||||
? <Tag color="danger">{s?.unreadNotificationCount}건 안 읽음</Tag>
|
||||
: '모두 확인'
|
||||
}
|
||||
onClick={() => navigate('/notice')}
|
||||
clickable
|
||||
>
|
||||
공지사항
|
||||
알림 / 공지
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
|
||||
{!agentId && (
|
||||
{s && !s.agentId && (
|
||||
<div style={{ marginTop: 24, fontSize: 12, color: '#f96', textAlign: 'center' }}>
|
||||
본 계정에 연결된 설계사(agent)가 없습니다. 일부 기능이 제한될 수 있습니다.
|
||||
본 계정에 연결된 설계사(agent)가 없습니다. 명세서·출금 조회가 제한됩니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user