Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions jwt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# JWT 인증 기능 구현 참고 가이드

## 📁 핵심 참고 파일들

### 1. Redux Store 구조
**파일**: `src/mong/store/index.ts`
- Redux store 설정
- RTK Query 또는 일반 Redux 설정 참고
- 미들웨어 설정 방법

### 2. 인증 상태 관리 (Redux Slice)
**파일**: `src/mong/store/slices/authSlice.ts`
- **현재 상태**: localStorage 기반 인증 (JWT 미적용)
- **참고 포인트**:
- `createAsyncThunk`를 사용한 비동기 액션 패턴
- `loginUser`, `registerUser` thunk 구조
- Redux state 관리 방식
- 에러 처리 및 로딩 상태 관리
- **JWT 적용 시 수정 필요**:
- `accessToken`, `refreshToken` 상태 추가
- API 호출을 axios로 변경
- 토큰 자동 갱신 로직 추가

### 3. 타입 정의
**파일**: `src/mong/types/redux.ts`
- **AuthState 인터페이스**:
```typescript
export interface AuthState {
user: User | null;
isAuthenticated: boolean;
loading: boolean;
error: string | null;
// JWT 추가 필요: accessToken, refreshToken
}
```
- **참고 포인트**: 타입 안전성 확보 방법

**파일**: `src/mong/types/common.ts`
- **User 인터페이스**: 사용자 데이터 구조
- **참고 포인트**: 공통 타입 정의 방식

### 4. API 클라이언트 설정
**파일**: `src/mong/api/lib/axiosPublic.ts`
- **현재 상태**: 기본 axios 설정
- **참고 포인트**:
- baseURL 설정 방법
- 공개 API용 axios 인스턴스 생성

**파일**: `src/mong/api/lib/axiosAuth.ts`
- **현재 상태**: 인증된 API용 axios 설정 (미완성)
- **참고 포인트**:
- `authGet`, `authPost` 헬퍼 함수
- 토큰 자동 첨부 방식
- **JWT 적용 시 활용**: 인터셉터로 토큰 자동 관리

**파일**: `src/mong/api/features/auth/thunks.ts`
- **현재 상태**: 기본적인 로그인 thunk (미완성)
- **참고 포인트**:
- `createAsyncThunk` 사용법
- API 호출 후 Redux 상태 업데이트

### 5. 커스텀 훅
**파일**: `src/mong/store/hooks.ts`
- **useAuth 훅**:
- 현재 상태 관리 방법
- 액션 디스패치 패턴
- 메모이제이션을 통한 성능 최적화
- **참고 포인트**:
- `useCallback`을 사용한 안전한 액션 디스패치
- 타입 안전한 selector 사용법

### 6. 로컬 스토리지 관리
**파일**: `src/mong/utils/storage.ts`
- **참고 포인트**:
- 타입 안전한 localStorage 관리
- 에러 처리 포함한 storage 헬퍼
- **JWT 적용 시**: 토큰 저장/관리 로직 추가 필요

## 🔧 JWT 구현 시 활용 방법

### 1. Axios 인터셉터 설정
```typescript
// src/mong/api/lib/axiosAuth.ts 참고
// 요청 인터셉터: 토큰 자동 첨부
// 응답 인터셉터: 401 에러 시 토큰 갱신
```

### 2. Redux 상태 확장
```typescript
// src/mong/types/redux.ts의 AuthState 확장
interface AuthState {
// 기존 필드들...
accessToken: string | null;
refreshToken: string | null;
isRefreshing: boolean;
}
```

### 3. 토큰 관리 유틸리티
```typescript
// src/mong/utils/tokenManager.ts 생성 필요
// - 토큰 저장/조회/삭제
// - 토큰 만료 검증
// - 자동 갱신 로직
```

### 4. API 엔드포인트 매핑
```typescript
// src/mong/api/features/auth/thunks.ts 확장
// - loginThunk: /api/users/login
// - registerThunk: /api/users/sign-up
// - refreshTokenThunk: /api/token/reissue
// - logoutThunk: /api/users/log-out
```

## 📋 구현 순서 가이드

1. **토큰 관리 유틸리티 생성** (`src/mong/utils/tokenManager.ts`)
2. **Axios 인터셉터 설정** (`src/mong/api/lib/axiosAuth.ts` 수정)
3. **Redux 상태 확장** (`src/mong/types/redux.ts` 수정)
4. **Auth Slice 업데이트** (`src/mong/store/slices/authSlice.ts` 수정)
5. **API Thunk 구현** (`src/mong/api/features/auth/thunks.ts` 완성)
6. **컴포넌트에서 사용** (`src/mong/pages/Login.tsx` 등)

## ⚠️ 주의사항

- 현재 프로젝트는 localStorage 기반 인증으로 구현되어 있음
- JWT 적용 시 기존 localStorage 로직을 토큰 기반으로 변경 필요
- `src/mong/api/lib/axiosAuth.ts`의 store import 경로 수정 필요
- CORS 설정 확인 (백엔드: 5000, 프론트엔드: 5173)
3 changes: 3 additions & 0 deletions src/mong/assets/heartIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/mong/assets/statisticsIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 42 additions & 20 deletions src/mong/components/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import moonIcon from '../assets/moonIcon.svg';
import '../styles/header.css';

Expand All @@ -20,6 +20,7 @@ const NavBar: React.FC<NavBarProps> = ({ userProfile, onLogout, onStartSleepReco
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const location = useLocation();

// 드롭다운 외부 클릭 시 닫기
useEffect(() => {
Expand Down Expand Up @@ -62,6 +63,15 @@ const NavBar: React.FC<NavBarProps> = ({ userProfile, onLogout, onStartSleepReco
navigate(`/daily-report/${today}`);
};

const handleBackClick = () => {
navigate('/dashboard');
};

// 현재 경로가 통계 또는 일별보고서 페이지인지 확인
const isStatisticsPage = location.pathname === '/statistics';
const isDailyReportPage = location.pathname.startsWith('/daily-report/');
const showBackButton = isStatisticsPage || isDailyReportPage;

return (
<header className="profile-header">
<div className="profile-header-content">
Expand All @@ -73,36 +83,48 @@ const NavBar: React.FC<NavBarProps> = ({ userProfile, onLogout, onStartSleepReco
<div className="profile-header-logo">
<img src={moonIcon} alt="moon icon" className="profile-header-logo-icon" />
</div>
<span className="profile-header-brand">mong</span>
<span className="profile-header-brand" style={{ fontFamily: 'Righteous, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif' }}>mong</span>
</button>
</div>

{/* 중앙 메뉴 버튼들 - 가운데 고정 */}
<div className="flex items-center gap-2" style={{ position: 'absolute', left: '50%', transform: 'translateX(-50%)' }}>
<button
className="px-4 py-2 bg-transparent text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors"
onClick={handleDailyReportClick}
>
일별 상세기록
</button>
<button
className="px-4 py-2 bg-transparent text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors"
onClick={handleStatisticsClick}
>
통계
</button>
</div>
{/* 중앙 메뉴 버튼들 - 가운데 고정 (통계/일별보고서 페이지에서는 숨김) */}
{!showBackButton && (
<div className="flex items-center gap-2" style={{ position: 'absolute', left: '50%', transform: 'translateX(-50%)' }}>
<button
className="px-4 py-2 bg-transparent text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors"
onClick={handleDailyReportClick}
>
일별 상세기록
</button>
<button
className="px-4 py-2 bg-transparent text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors"
onClick={handleStatisticsClick}
>
통계
</button>
</div>
)}

{/* 수면 기록 시작 버튼 - 프로필 바로 왼쪽 */}
{/* 수면 기록 시작 버튼 또는 뒤로가기 버튼 - 프로필 바로 왼쪽 */}
<div className="flex items-center gap-4">
{showStartButton && (
{showBackButton ? (
<button
className="start-sleep-button"
onClick={handleBackClick}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"></path>
</svg>
뒤로가기
</button>
) : showStartButton ? (
<button className="start-sleep-button" onClick={onStartSleepRecord}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"></path>
</svg>
수면 기록 시작
</button>
)}
) : null}

{/* 사용자 드롭다운 */}
<div className="relative" ref={dropdownRef}>
Expand Down
10 changes: 8 additions & 2 deletions src/mong/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react'
import { useNavigate } from 'react-router-dom';
import Container from '../components/Container'
import moonIcon from '../assets/moonIcon.svg'
import heartIcon from '../assets/heartIcon.svg'
import statisticsIcon from '../assets/statisticsIcon.svg'

export default function Home() {
const navigate = useNavigate();
Expand All @@ -9,7 +12,7 @@ export default function Home() {
<Container backgroundColor="#000" minHeight="100vh" textColor="#fff">
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16, paddingTop: 48, paddingBottom: 48 }}>
<div style={{ width: 80, height: 80, borderRadius: 9999, backgroundColor: 'rgba(0, 212, 170, 0.10)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 40, height: 40, backgroundColor: '#00d4aa', borderRadius: 8 }} />
<img src={moonIcon} alt="moon" style={{ width: 40, height: 40 }} />
</div>
<h1 style={{ fontSize: 48, lineHeight: '48px', margin: 0, fontFamily: 'Righteous, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif', textAlign: 'center' }}>mong</h1>
<p style={{ fontSize: 20, lineHeight: '28px', color: '#a1a1aa', margin: 0, textAlign: 'center' }}>
Expand All @@ -22,16 +25,19 @@ export default function Home() {
{[{
title: '스마트 수면 측정',
desc: '정확한 수면 단계 분석으로 수면의 질을 측\n정합니다',
icon: moonIcon
}, {
title: '상세한 통계',
desc: '개인 맞춤 수면 패턴 분석과 개선 제안을 제\n공합니다',
icon: statisticsIcon
}, {
title: '건강 관리',
desc: '수면을 통한 전체적인 건강 상태 모니터링',
icon: heartIcon
}].map((card, idx) => (
<div key={idx} style={{ backgroundColor: '#1a1a1a', border: '1px solid #2a2a2a', borderRadius: 14, padding: 16, display: 'flex', alignItems: 'center', flexDirection: 'column', gap: 8 }}>
<div style={{ width: 48, height: 48, borderRadius: 9999, backgroundColor: 'rgba(0, 212, 170, 0.10)', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 8 }}>
<div style={{ width: 24, height: 24, backgroundColor: '#00d4aa', borderRadius: 6 }} />
<img src={card.icon} alt="icon" style={{ width: 24, height: 24 }} />
</div>
<div style={{ fontSize: 18, lineHeight: '28px' }}>{card.title}</div>
<div style={{ fontSize: 14, lineHeight: '22.75px', color: '#a1a1aa', textAlign: 'center', whiteSpace: 'pre-line' }}>{card.desc}</div>
Expand Down
Loading