mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 06:16:24 +00:00
이벤트 목록 Mock 데이터 적용 및 Participation API 연동
- 이벤트 목록 페이지에 Mock 데이터 적용 (evt_2025012301 등 4개 이벤트) - 이벤트 상세 페이지 Analytics API 임시 주석처리 (서버 이슈) - Participation API 프록시 라우트 URL 구조 수정 (/events/ 제거) - EventID localStorage 저장 기능 추가 - 상세한 console.log 추가 (생성된 eventId, objective, timestamp) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
86ae038a31
commit
974961e1bd
357
USER_API_CHANGES.md
Normal file
357
USER_API_CHANGES.md
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# User API 타입 변경사항 (2025-01-30)
|
||||||
|
|
||||||
|
## 📋 주요 변경사항 요약
|
||||||
|
|
||||||
|
### **userId 및 storeId 타입 변경: `number` → `string` (UUID)**
|
||||||
|
|
||||||
|
백엔드 API 스펙에 따라 userId와 storeId가 UUID 형식의 문자열로 변경되었습니다.
|
||||||
|
|
||||||
|
| 항목 | 기존 (Old) | 변경 (New) |
|
||||||
|
|------|-----------|-----------|
|
||||||
|
| **userId** | `number` (예: `1`) | `string` (예: `"550e8400-e29b-41d4-a716-446655440000"`) |
|
||||||
|
| **storeId** | `number` (예: `1`) | `string` (예: `"6ba7b810-9dad-11d1-80b4-00c04fd430c8"`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 영향을 받는 인터페이스
|
||||||
|
|
||||||
|
### LoginResponse
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RegisterResponse
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
interface RegisterResponse {
|
||||||
|
token: string;
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
storeId: number;
|
||||||
|
storeName: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
interface RegisterResponse {
|
||||||
|
token: string;
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
storeId: string; // UUID format
|
||||||
|
storeName: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ProfileResponse
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
interface ProfileResponse {
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
storeId: number;
|
||||||
|
storeName: string;
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
businessHours: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastLoginAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
interface ProfileResponse {
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
storeId: string; // UUID format
|
||||||
|
storeName: string;
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
businessHours: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastLoginAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
storeId?: number;
|
||||||
|
storeName?: string;
|
||||||
|
industry?: string;
|
||||||
|
address?: string;
|
||||||
|
businessHours?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
storeId?: string; // UUID format
|
||||||
|
storeName?: string;
|
||||||
|
industry?: string;
|
||||||
|
address?: string;
|
||||||
|
businessHours?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 수정된 파일 목록
|
||||||
|
|
||||||
|
### 1. Type Definitions
|
||||||
|
- ✅ `src/entities/user/model/types.ts`
|
||||||
|
- `LoginResponse.userId`: `number` → `string`
|
||||||
|
- `RegisterResponse.userId`: `number` → `string`
|
||||||
|
- `RegisterResponse.storeId`: `number` → `string`
|
||||||
|
- `ProfileResponse.userId`: `number` → `string`
|
||||||
|
- `ProfileResponse.storeId`: `number` → `string`
|
||||||
|
- `User.userId`: `number` → `string`
|
||||||
|
- `User.storeId`: `number` → `string`
|
||||||
|
|
||||||
|
### 2. Stores
|
||||||
|
- ✅ `src/stores/authStore.ts`
|
||||||
|
- `User.id`: UUID 주석 추가
|
||||||
|
|
||||||
|
### 3. Components
|
||||||
|
- ✅ No changes required (타입 추론 사용)
|
||||||
|
- `src/features/auth/model/useAuth.ts`
|
||||||
|
- `src/app/(auth)/login/page.tsx`
|
||||||
|
- `src/app/(auth)/register/page.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 API 응답 예시
|
||||||
|
|
||||||
|
### 로그인 응답
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": 1,
|
||||||
|
"userName": "홍길동",
|
||||||
|
"role": "USER",
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"role": "USER",
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 회원가입 응답
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": 1,
|
||||||
|
"userName": "홍길동",
|
||||||
|
"storeId": 1,
|
||||||
|
"storeName": "홍길동 고깃집"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"storeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||||
|
"storeName": "홍길동 고깃집"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 프로필 조회 응답
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": 1,
|
||||||
|
"userName": "홍길동",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"role": "USER",
|
||||||
|
"storeId": 1,
|
||||||
|
"storeName": "홍길동 고깃집",
|
||||||
|
"industry": "restaurant",
|
||||||
|
"address": "서울특별시 강남구",
|
||||||
|
"businessHours": "09:00-18:00",
|
||||||
|
"createdAt": "2025-01-01T00:00:00",
|
||||||
|
"lastLoginAt": "2025-01-10T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"role": "USER",
|
||||||
|
"storeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||||
|
"storeName": "홍길동 고깃집",
|
||||||
|
"industry": "restaurant",
|
||||||
|
"address": "서울특별시 강남구",
|
||||||
|
"businessHours": "09:00-18:00",
|
||||||
|
"createdAt": "2025-01-01T00:00:00",
|
||||||
|
"lastLoginAt": "2025-01-10T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 주의사항
|
||||||
|
|
||||||
|
### 1. localStorage 초기화 필요
|
||||||
|
기존에 number 타입으로 저장된 사용자 정보가 있다면 localStorage를 초기화해야 합니다:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 브라우저 콘솔에서 실행
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. UUID 형식
|
||||||
|
- UUID는 표준 UUID v4 형식입니다: `550e8400-e29b-41d4-a716-446655440000`
|
||||||
|
- 하이픈(`-`)을 포함한 36자 문자열
|
||||||
|
- 비교 시 대소문자 구분 없음 (일반적으로 소문자 사용)
|
||||||
|
|
||||||
|
### 3. 기존 Mock 데이터
|
||||||
|
기존에 number 타입으로 작성된 Mock 데이터는 UUID 문자열로 변경해야 합니다:
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```typescript
|
||||||
|
const mockUser = {
|
||||||
|
userId: 1,
|
||||||
|
storeId: 1,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```typescript
|
||||||
|
const mockUser = {
|
||||||
|
userId: "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
storeId: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 하위 호환성
|
||||||
|
- 이전 number 타입과는 호환되지 않습니다
|
||||||
|
- 기존 세션은 모두 무효화됩니다
|
||||||
|
- 사용자는 다시 로그인해야 합니다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 마이그레이션 체크리스트
|
||||||
|
|
||||||
|
- [x] TypeScript 인터페이스 업데이트
|
||||||
|
- [x] 타입 정의 파일 수정 완료
|
||||||
|
- [x] 빌드 테스트 통과
|
||||||
|
- [ ] localStorage 초기화 (사용자)
|
||||||
|
- [ ] 개발 서버 테스트 (사용자)
|
||||||
|
- [ ] 실제 API 연동 테스트 (사용자)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 관련 문서
|
||||||
|
|
||||||
|
- API 문서: http://kt-event-marketing-api.20.214.196.128.nip.io/api/v1/users/swagger-ui/index.html
|
||||||
|
- OpenAPI Spec: http://kt-event-marketing-api.20.214.196.128.nip.io/api/v1/users/v3/api-docs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 변경 이유
|
||||||
|
|
||||||
|
**백엔드 아키텍처 개선:**
|
||||||
|
- 분산 시스템에서 ID 충돌 방지
|
||||||
|
- 데이터베이스 독립적인 고유 식별자
|
||||||
|
- 보안 강화 (ID 추측 불가)
|
||||||
|
- 마이크로서비스 간 데이터 통합 용이
|
||||||
|
|
||||||
|
**UUID의 장점:**
|
||||||
|
- 전역적으로 고유한 식별자 (Globally Unique Identifier)
|
||||||
|
- Auto-increment ID의 한계 극복
|
||||||
|
- 분산 환경에서 중앙 조정 없이 생성 가능
|
||||||
|
- 보안성 향상 (순차적 ID 노출 방지)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 롤백 방법
|
||||||
|
|
||||||
|
만약 이전 버전으로 돌아가야 한다면:
|
||||||
|
|
||||||
|
1. Git을 통한 코드 복원:
|
||||||
|
```bash
|
||||||
|
git log --oneline # 커밋 찾기
|
||||||
|
git revert <commit-hash> # 또는 특정 커밋으로 복원
|
||||||
|
```
|
||||||
|
|
||||||
|
2. localStorage 초기화:
|
||||||
|
```javascript
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 개발 서버 재시작:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**문서 작성일**: 2025-01-30
|
||||||
|
**마지막 업데이트**: 2025-01-30
|
||||||
|
**변경 적용 버전**: v1.1.0
|
||||||
597
USER_API_STATUS.md
Normal file
597
USER_API_STATUS.md
Normal file
@ -0,0 +1,597 @@
|
|||||||
|
# User API 연동 현황
|
||||||
|
|
||||||
|
## 📋 연동 완료 요약
|
||||||
|
|
||||||
|
User API는 **완전히 구현되어 있으며**, 로그인 및 회원가입 기능이 정상적으로 작동합니다.
|
||||||
|
|
||||||
|
### ✅ 구현 완료 항목
|
||||||
|
|
||||||
|
1. **API 클라이언트 설정**
|
||||||
|
- Gateway를 통한 백엔드 직접 연동
|
||||||
|
- 토큰 기반 인증 시스템
|
||||||
|
- Request/Response 인터셉터
|
||||||
|
|
||||||
|
2. **타입 정의**
|
||||||
|
- LoginRequest/Response
|
||||||
|
- RegisterRequest/Response
|
||||||
|
- ProfileResponse
|
||||||
|
- User 및 AuthState 인터페이스
|
||||||
|
|
||||||
|
3. **인증 로직**
|
||||||
|
- useAuth 커스텀 훅
|
||||||
|
- AuthProvider Context
|
||||||
|
- localStorage 기반 세션 관리
|
||||||
|
|
||||||
|
4. **UI 페이지**
|
||||||
|
- 로그인 페이지 (/login)
|
||||||
|
- 회원가입 페이지 (/register)
|
||||||
|
- 3단계 회원가입 플로우
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 아키텍처 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
프론트엔드 Gateway 백엔드
|
||||||
|
┌─────────────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│ │ HTTP │ │ HTTP │ │
|
||||||
|
│ Browser ├────────────>│Gateway ├─────────────>│ User API │
|
||||||
|
│ │<────────────┤ │<─────────────┤ │
|
||||||
|
└─────────────┘ JSON+JWT └────────┘ JSON+JWT └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 클라이언트 설정
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/shared/api/client.ts
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
const API_HOSTS = {
|
||||||
|
user: GATEWAY_HOST,
|
||||||
|
event: GATEWAY_HOST,
|
||||||
|
content: GATEWAY_HOST,
|
||||||
|
ai: GATEWAY_HOST,
|
||||||
|
participation: GATEWAY_HOST,
|
||||||
|
distribution: GATEWAY_HOST,
|
||||||
|
analytics: GATEWAY_HOST,
|
||||||
|
};
|
||||||
|
|
||||||
|
// User API는 apiClient를 통해 직접 Gateway에 연결
|
||||||
|
export const apiClient: AxiosInstance = axios.create({
|
||||||
|
baseURL: API_HOSTS.user,
|
||||||
|
timeout: 90000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**💡 프록시 라우트 불필요**: User API는 Next.js 프록시를 거치지 않고 브라우저에서 Gateway로 직접 요청합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 User API 엔드포인트
|
||||||
|
|
||||||
|
### 1. 로그인
|
||||||
|
```http
|
||||||
|
POST /api/v1/users/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"role": "USER",
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 회원가입
|
||||||
|
```http
|
||||||
|
POST /api/v1/users/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "홍길동",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"storeName": "홍길동 고깃집",
|
||||||
|
"industry": "restaurant",
|
||||||
|
"address": "서울특별시 강남구",
|
||||||
|
"businessHours": "09:00-18:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"storeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||||
|
"storeName": "홍길동 고깃집"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 로그아웃
|
||||||
|
```http
|
||||||
|
POST /api/v1/users/logout
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "로그아웃되었습니다"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 프로필 조회
|
||||||
|
```http
|
||||||
|
GET /api/v1/users/profile
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**응답:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"userName": "홍길동",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"role": "USER",
|
||||||
|
"storeId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||||||
|
"storeName": "홍길동 고깃집",
|
||||||
|
"industry": "restaurant",
|
||||||
|
"address": "서울특별시 강남구",
|
||||||
|
"businessHours": "09:00-18:00",
|
||||||
|
"createdAt": "2025-01-01T00:00:00",
|
||||||
|
"lastLoginAt": "2025-01-10T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 프로필 수정
|
||||||
|
```http
|
||||||
|
PUT /api/v1/users/profile
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "홍길동",
|
||||||
|
"phoneNumber": "01012345678",
|
||||||
|
"storeName": "홍길동 고깃집",
|
||||||
|
"industry": "restaurant",
|
||||||
|
"address": "서울특별시 강남구",
|
||||||
|
"businessHours": "09:00-18:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 비밀번호 변경
|
||||||
|
```http
|
||||||
|
PUT /api/v1/users/password
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"currentPassword": "oldpassword",
|
||||||
|
"newPassword": "newpassword123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 인증 플로우
|
||||||
|
|
||||||
|
### 로그인 플로우
|
||||||
|
```
|
||||||
|
1. 사용자가 이메일/비밀번호 입력
|
||||||
|
2. userApi.login() 호출
|
||||||
|
3. 서버에서 JWT 토큰 발급
|
||||||
|
4. localStorage에 토큰 저장
|
||||||
|
5. userApi.getProfile() 호출 (storeId 포함된 전체 정보 획득)
|
||||||
|
6. localStorage에 User 정보 저장
|
||||||
|
7. AuthContext 상태 업데이트
|
||||||
|
8. 메인 페이지로 리디렉션
|
||||||
|
```
|
||||||
|
|
||||||
|
### 회원가입 플로우
|
||||||
|
```
|
||||||
|
1. 3단계 폼 작성
|
||||||
|
- Step 1: 계정 정보 (이메일, 비밀번호)
|
||||||
|
- Step 2: 개인 정보 (이름, 전화번호)
|
||||||
|
- Step 3: 사업장 정보 (상호명, 업종, 주소 등)
|
||||||
|
2. userApi.register() 호출
|
||||||
|
3. 서버에서 사용자 생성 및 JWT 토큰 발급
|
||||||
|
4. localStorage에 토큰 및 User 정보 저장
|
||||||
|
5. AuthContext 상태 업데이트
|
||||||
|
6. 회원가입 완료 다이얼로그 표시
|
||||||
|
7. 메인 페이지로 리디렉션
|
||||||
|
```
|
||||||
|
|
||||||
|
### 로그아웃 플로우
|
||||||
|
```
|
||||||
|
1. userApi.logout() 호출
|
||||||
|
2. 서버에서 세션 무효화 (실패해도 계속 진행)
|
||||||
|
3. localStorage에서 토큰 및 User 정보 삭제
|
||||||
|
4. AuthContext 상태 초기화
|
||||||
|
5. 로그인 페이지로 리디렉션
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 파일 구조
|
||||||
|
|
||||||
|
### API Layer
|
||||||
|
```
|
||||||
|
src/entities/user/
|
||||||
|
├── api/
|
||||||
|
│ └── userApi.ts # User API 서비스 함수
|
||||||
|
├── model/
|
||||||
|
│ └── types.ts # TypeScript 타입 정의
|
||||||
|
└── index.ts # Public exports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features Layer
|
||||||
|
```
|
||||||
|
src/features/auth/
|
||||||
|
├── model/
|
||||||
|
│ ├── useAuth.ts # 인증 커스텀 훅
|
||||||
|
│ └── AuthProvider.tsx # Context Provider
|
||||||
|
└── index.ts # Public exports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pages
|
||||||
|
```
|
||||||
|
src/app/(auth)/
|
||||||
|
├── login/
|
||||||
|
│ └── page.tsx # 로그인 페이지
|
||||||
|
└── register/
|
||||||
|
└── page.tsx # 회원가입 페이지 (3단계 플로우)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared
|
||||||
|
```
|
||||||
|
src/shared/api/
|
||||||
|
├── client.ts # Axios 클라이언트 설정
|
||||||
|
└── index.ts # Public exports
|
||||||
|
|
||||||
|
src/stores/
|
||||||
|
└── authStore.ts # Zustand 인증 스토어 (참고용)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 주요 코드 구조
|
||||||
|
|
||||||
|
### 1. User API Service
|
||||||
|
|
||||||
|
**src/entities/user/api/userApi.ts:**
|
||||||
|
```typescript
|
||||||
|
const USER_API_BASE = '/api/v1/users';
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
login: async (data: LoginRequest): Promise<LoginResponse> => {...},
|
||||||
|
register: async (data: RegisterRequest): Promise<RegisterResponse> => {...},
|
||||||
|
logout: async (): Promise<LogoutResponse> => {...},
|
||||||
|
getProfile: async (): Promise<ProfileResponse> => {...},
|
||||||
|
updateProfile: async (data: UpdateProfileRequest): Promise<ProfileResponse> => {...},
|
||||||
|
changePassword: async (data: ChangePasswordRequest): Promise<void> => {...},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. useAuth Hook
|
||||||
|
|
||||||
|
**src/features/auth/model/useAuth.ts:**
|
||||||
|
```typescript
|
||||||
|
export const useAuth = () => {
|
||||||
|
const [authState, setAuthState] = useState<AuthState>({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 초기 인증 상태 확인 (localStorage 기반)
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem(TOKEN_KEY);
|
||||||
|
const userStr = localStorage.getItem(USER_KEY);
|
||||||
|
if (token && userStr) {
|
||||||
|
const user = JSON.parse(userStr) as User;
|
||||||
|
setAuthState({
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
isAuthenticated: true,
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 로그인 함수
|
||||||
|
const login = async (credentials: LoginRequest) => {
|
||||||
|
const response = await userApi.login(credentials);
|
||||||
|
localStorage.setItem(TOKEN_KEY, response.token);
|
||||||
|
const profile = await userApi.getProfile();
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||||
|
setAuthState({...});
|
||||||
|
return { success: true, user };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 회원가입, 로그아웃, 프로필 새로고침 함수들...
|
||||||
|
|
||||||
|
return {
|
||||||
|
...authState,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
refreshProfile,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. AuthProvider Context
|
||||||
|
|
||||||
|
**src/features/auth/model/AuthProvider.tsx:**
|
||||||
|
```typescript
|
||||||
|
const AuthContext = createContext<AuthContextType | null>(null);
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const auth = useAuth();
|
||||||
|
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthContext = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuthContext must be used within AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. RootLayout 적용
|
||||||
|
|
||||||
|
**src/app/layout.tsx:**
|
||||||
|
```typescript
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="ko">
|
||||||
|
<body>
|
||||||
|
<MUIThemeProvider>
|
||||||
|
<ReactQueryProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
|
</ReactQueryProvider>
|
||||||
|
</MUIThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 테스트 방법
|
||||||
|
|
||||||
|
### 1. 회원가입 테스트
|
||||||
|
|
||||||
|
1. 개발 서버 실행
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 브라우저에서 `/register` 접속
|
||||||
|
|
||||||
|
3. 3단계 폼 작성:
|
||||||
|
- **Step 1**: 이메일 및 비밀번호 입력
|
||||||
|
- **Step 2**: 이름 및 전화번호 입력 (010-1234-5678 형식)
|
||||||
|
- **Step 3**: 사업장 정보 입력 및 약관 동의
|
||||||
|
|
||||||
|
4. "가입완료" 버튼 클릭
|
||||||
|
|
||||||
|
5. 성공 시:
|
||||||
|
- 회원가입 완료 다이얼로그 표시
|
||||||
|
- localStorage에 토큰 및 사용자 정보 저장
|
||||||
|
- 메인 페이지로 리디렉션
|
||||||
|
|
||||||
|
### 2. 로그인 테스트
|
||||||
|
|
||||||
|
1. 브라우저에서 `/login` 접속
|
||||||
|
|
||||||
|
2. 이메일 및 비밀번호 입력
|
||||||
|
|
||||||
|
3. "로그인" 버튼 클릭
|
||||||
|
|
||||||
|
4. 성공 시:
|
||||||
|
- localStorage에 토큰 및 사용자 정보 저장
|
||||||
|
- 메인 페이지로 리디렉션
|
||||||
|
- 헤더에 사용자 정보 표시
|
||||||
|
|
||||||
|
### 3. 로그아웃 테스트
|
||||||
|
|
||||||
|
1. 로그인된 상태에서 프로필 페이지 또는 헤더 메뉴 접근
|
||||||
|
|
||||||
|
2. "로그아웃" 버튼 클릭
|
||||||
|
|
||||||
|
3. 성공 시:
|
||||||
|
- localStorage에서 토큰 및 사용자 정보 삭제
|
||||||
|
- 로그인 페이지로 리디렉션
|
||||||
|
|
||||||
|
### 4. 디버깅
|
||||||
|
|
||||||
|
브라우저 개발자 도구 Console에서 다음 로그 확인:
|
||||||
|
|
||||||
|
```
|
||||||
|
📞 전화번호 변환: 010-1234-5678 -> 01012345678
|
||||||
|
📦 회원가입 요청 데이터: {...}
|
||||||
|
🚀 registerUser 함수 호출
|
||||||
|
📥 registerUser 결과: {...}
|
||||||
|
✅ 회원가입 성공: {...}
|
||||||
|
💾 localStorage에 토큰과 사용자 정보 저장 완료
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 주의사항
|
||||||
|
|
||||||
|
### 1. 전화번호 형식 변환
|
||||||
|
- **UI 입력**: `010-1234-5678` (하이픈 포함)
|
||||||
|
- **API 전송**: `01012345678` (하이픈 제거)
|
||||||
|
- 회원가입 페이지에서 자동 변환 처리됨
|
||||||
|
|
||||||
|
### 2. 토큰 관리
|
||||||
|
- Access Token은 localStorage에 `accessToken` 키로 저장
|
||||||
|
- User 정보는 localStorage에 `user` 키로 저장
|
||||||
|
- 401 응답 시 자동으로 로그인 페이지로 리디렉션
|
||||||
|
|
||||||
|
### 3. 프록시 라우트 없음
|
||||||
|
- User API는 Next.js 프록시를 사용하지 않음
|
||||||
|
- 브라우저에서 Gateway로 직접 요청
|
||||||
|
- CORS 설정이 Gateway에서 처리되어야 함
|
||||||
|
|
||||||
|
### 4. 로그아웃 에러 처리
|
||||||
|
- 로그아웃 API 실패해도 로컬 상태는 정리됨
|
||||||
|
- 서버 에러 발생 시에도 사용자는 정상적으로 로그아웃됨
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 타입 정의 요약
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 로그인
|
||||||
|
interface LoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회원가입
|
||||||
|
interface RegisterRequest {
|
||||||
|
name: string;
|
||||||
|
phoneNumber: string; // "01012345678" 형식
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
storeName: string;
|
||||||
|
industry?: string;
|
||||||
|
address: string;
|
||||||
|
businessHours?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegisterResponse {
|
||||||
|
token: string;
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
storeId: string; // UUID format
|
||||||
|
storeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 프로필
|
||||||
|
interface ProfileResponse {
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
storeId: string; // UUID format
|
||||||
|
storeName: string;
|
||||||
|
industry: string;
|
||||||
|
address: string;
|
||||||
|
businessHours: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastLoginAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User 상태
|
||||||
|
interface User {
|
||||||
|
userId: string; // UUID format
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
storeId?: string; // UUID format
|
||||||
|
storeName?: string;
|
||||||
|
industry?: string;
|
||||||
|
address?: string;
|
||||||
|
businessHours?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증 상태
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 체크리스트
|
||||||
|
|
||||||
|
- [x] API 클라이언트 설정 완료
|
||||||
|
- [x] TypeScript 타입 정의 완료
|
||||||
|
- [x] useAuth Hook 구현 완료
|
||||||
|
- [x] AuthProvider Context 구현 완료
|
||||||
|
- [x] 로그인 페이지 구현 완료
|
||||||
|
- [x] 회원가입 페이지 (3단계) 구현 완료
|
||||||
|
- [x] localStorage 세션 관리 완료
|
||||||
|
- [x] Request/Response 인터셉터 설정 완료
|
||||||
|
- [x] 401 에러 핸들링 완료
|
||||||
|
- [x] RootLayout에 AuthProvider 적용 완료
|
||||||
|
- [x] 빌드 테스트 통과 ✅
|
||||||
|
- [ ] 개발 서버 실행 및 실제 API 테스트 (사용자가 수행)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 관련 파일
|
||||||
|
|
||||||
|
### 핵심 파일
|
||||||
|
- `src/entities/user/api/userApi.ts` - User API 서비스
|
||||||
|
- `src/entities/user/model/types.ts` - TypeScript 타입 정의
|
||||||
|
- `src/features/auth/model/useAuth.ts` - 인증 Hook
|
||||||
|
- `src/features/auth/model/AuthProvider.tsx` - Context Provider
|
||||||
|
- `src/app/(auth)/login/page.tsx` - 로그인 페이지
|
||||||
|
- `src/app/(auth)/register/page.tsx` - 회원가입 페이지
|
||||||
|
- `src/shared/api/client.ts` - Axios 클라이언트 설정
|
||||||
|
|
||||||
|
### 참고 파일
|
||||||
|
- `src/stores/authStore.ts` - Zustand 인증 스토어 (참고용, 현재 미사용)
|
||||||
|
- `src/app/layout.tsx` - RootLayout with AuthProvider
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 다음 단계
|
||||||
|
|
||||||
|
User API 연동은 완료되었으므로 다음 작업을 진행할 수 있습니다:
|
||||||
|
|
||||||
|
1. **개발 서버 테스트**: `npm run dev` 실행 후 실제 회원가입/로그인 테스트
|
||||||
|
2. **프로필 페이지 개선**: 사용자 정보 수정 기능 강화
|
||||||
|
3. **비밀번호 찾기**: 비밀번호 재설정 플로우 구현 (현재 미구현)
|
||||||
|
4. **소셜 로그인**: 카카오톡, 네이버 소셜 로그인 구현 (현재 준비 중)
|
||||||
|
5. **권한 관리**: Role 기반 접근 제어 (ADMIN, USER) 구현
|
||||||
|
6. **세션 갱신**: Refresh Token 로직 추가 (필요시)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 문의
|
||||||
|
|
||||||
|
User API 관련 문제나 개선사항은 프로젝트 팀에 문의하세요.
|
||||||
|
|
||||||
|
**문서 작성일**: 2025-01-30
|
||||||
|
**마지막 업데이트**: 2025-01-30
|
||||||
@ -89,6 +89,11 @@ export default function LoginPage() {
|
|||||||
showToast(`${provider === 'kakao' ? '카카오톡' : '네이버'} 로그인은 준비 중입니다`, 'info');
|
showToast(`${provider === 'kakao' ? '카카오톡' : '네이버'} 로그인은 준비 중입니다`, 'info');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUnavailableFeature = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showToast('현재는 해당 기능을 제공하지 않습니다', 'info');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -233,7 +238,8 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 4 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 4 }}>
|
||||||
<Link
|
<Link
|
||||||
href="/forgot-password"
|
href="#"
|
||||||
|
onClick={handleUnavailableFeature}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@ -245,7 +251,8 @@ export default function LoginPage() {
|
|||||||
|
|
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link
|
<Link
|
||||||
href="/register"
|
href="#"
|
||||||
|
onClick={handleUnavailableFeature}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="primary"
|
color="primary"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@ -268,7 +275,7 @@ export default function LoginPage() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => handleSocialLogin('kakao')}
|
onClick={handleUnavailableFeature}
|
||||||
sx={{
|
sx={{
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
borderColor: '#FEE500',
|
borderColor: '#FEE500',
|
||||||
@ -289,7 +296,7 @@ export default function LoginPage() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => handleSocialLogin('naver')}
|
onClick={handleUnavailableFeature}
|
||||||
sx={{
|
sx={{
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
borderColor: '#03C75A',
|
borderColor: '#03C75A',
|
||||||
@ -327,11 +334,11 @@ export default function LoginPage() {
|
|||||||
{/* 약관 동의 안내 */}
|
{/* 약관 동의 안내 */}
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', display: 'block' }}>
|
||||||
회원가입 시{' '}
|
회원가입 시{' '}
|
||||||
<Link href="/terms" underline="hover" sx={{ color: 'text.secondary' }}>
|
<Link href="#" onClick={handleUnavailableFeature} underline="hover" sx={{ color: 'text.secondary' }}>
|
||||||
이용약관
|
이용약관
|
||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
및{' '}
|
및{' '}
|
||||||
<Link href="/privacy" underline="hover" sx={{ color: 'text.secondary' }}>
|
<Link href="#" onClick={handleUnavailableFeature} underline="hover" sx={{ color: 'text.secondary' }}>
|
||||||
개인정보처리방침
|
개인정보처리방침
|
||||||
</Link>
|
</Link>
|
||||||
에 동의하게 됩니다.
|
에 동의하게 됩니다.
|
||||||
|
|||||||
57
src/app/(auth)/logout/page.tsx
Normal file
57
src/app/(auth)/logout/page.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||||
|
import { useAuthContext } from '@/features/auth';
|
||||||
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
|
export default function LogoutPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { logout } = useAuthContext();
|
||||||
|
const { showToast } = useUIStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
console.log('🚪 로그아웃 시작');
|
||||||
|
await logout();
|
||||||
|
console.log('✅ 로그아웃 완료');
|
||||||
|
showToast('로그아웃되었습니다', 'success');
|
||||||
|
|
||||||
|
// 로그인 페이지로 리디렉션
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/login');
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 로그아웃 에러:', error);
|
||||||
|
showToast('로그아웃 중 오류가 발생했습니다', 'error');
|
||||||
|
|
||||||
|
// 에러가 발생해도 로그인 페이지로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/login');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLogout();
|
||||||
|
}, [logout, router, showToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} sx={{ mb: 3 }} />
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
로그아웃 중입니다...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -155,52 +155,71 @@ export default function EventDetailPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||||
|
|
||||||
// Analytics API 호출
|
// Analytics API 호출 (임시 주석처리 - 서버 이슈)
|
||||||
const fetchAnalytics = async (forceRefresh = false) => {
|
const fetchAnalytics = async (forceRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
console.log('🔄 데이터 새로고침 시작...');
|
console.log('🔄 Mock 데이터 새로고침...');
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
} else {
|
} else {
|
||||||
console.log('📊 Analytics 데이터 로딩 시작...');
|
console.log('📊 Mock Analytics 데이터 로딩...');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// TODO: Analytics API 서버 이슈 해결 후 주석 해제
|
||||||
// Event Analytics API 병렬 호출
|
// Event Analytics API 병렬 호출
|
||||||
const [dashboard, timeline, roi, channels] = await Promise.all([
|
// const [dashboard, timeline, roi, channels] = await Promise.all([
|
||||||
analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
|
// analyticsApi.getEventAnalytics(eventId, { refresh: forceRefresh }),
|
||||||
analyticsApi.getEventTimelineAnalytics(eventId, {
|
// analyticsApi.getEventTimelineAnalytics(eventId, {
|
||||||
interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
|
// interval: chartPeriod === '7d' ? 'daily' : chartPeriod === '30d' ? 'daily' : 'daily',
|
||||||
refresh: forceRefresh
|
// refresh: forceRefresh
|
||||||
}),
|
// }),
|
||||||
analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true, refresh: forceRefresh }),
|
// analyticsApi.getEventRoiAnalytics(eventId, { includeProjection: true, refresh: forceRefresh }),
|
||||||
analyticsApi.getEventChannelAnalytics(eventId, { refresh: forceRefresh }),
|
// analyticsApi.getEventChannelAnalytics(eventId, { refresh: forceRefresh }),
|
||||||
]);
|
// ]);
|
||||||
|
|
||||||
console.log('✅ Dashboard 데이터:', dashboard);
|
// 임시 Mock 데이터
|
||||||
console.log('✅ Timeline 데이터:', timeline);
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
console.log('✅ ROI 데이터:', roi);
|
|
||||||
console.log('✅ Channel 데이터:', channels);
|
const mockAnalyticsData = {
|
||||||
|
dashboard: {
|
||||||
|
summary: {
|
||||||
|
participants: mockEventData.participants,
|
||||||
|
totalViews: mockEventData.views,
|
||||||
|
conversionRate: mockEventData.conversion / 100,
|
||||||
|
},
|
||||||
|
roi: {
|
||||||
|
roi: mockEventData.roi,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
participations: [
|
||||||
|
{ date: '2025-01-15', count: 12 },
|
||||||
|
{ date: '2025-01-16', count: 18 },
|
||||||
|
{ date: '2025-01-17', count: 25 },
|
||||||
|
{ date: '2025-01-18', count: 31 },
|
||||||
|
{ date: '2025-01-19', count: 22 },
|
||||||
|
{ date: '2025-01-20', count: 20 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
roi: {
|
||||||
|
currentRoi: mockEventData.roi,
|
||||||
|
projectedRoi: mockEventData.roi + 50,
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
distribution: [
|
||||||
|
{ channel: '우리동네TV', participants: 45 },
|
||||||
|
{ channel: '링고비즈', participants: 38 },
|
||||||
|
{ channel: 'SNS', participants: 45 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Analytics 데이터 저장
|
// Analytics 데이터 저장
|
||||||
setAnalyticsData({
|
setAnalyticsData(mockAnalyticsData);
|
||||||
dashboard,
|
|
||||||
timeline,
|
|
||||||
roi,
|
|
||||||
channels,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event 기본 정보 업데이트
|
console.log('✅ Mock Analytics 데이터 로딩 완료');
|
||||||
setEvent(prev => ({
|
|
||||||
...prev,
|
|
||||||
participants: dashboard.summary.participants,
|
|
||||||
views: dashboard.summary.totalViews,
|
|
||||||
roi: Math.round(dashboard.roi.roi),
|
|
||||||
conversion: Math.round(dashboard.summary.conversionRate * 100),
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log('✅ Analytics 데이터 로딩 완료');
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ Analytics 데이터 로딩 실패:', err);
|
console.error('❌ Analytics 데이터 로딩 실패:', err);
|
||||||
setError(err.message || 'Analytics 데이터를 불러오는데 실패했습니다.');
|
setError(err.message || 'Analytics 데이터를 불러오는데 실패했습니다.');
|
||||||
|
|||||||
@ -98,10 +98,27 @@ export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
|||||||
|
|
||||||
// 새로운 eventId 생성
|
// 새로운 eventId 생성
|
||||||
const eventId = generateEventId();
|
const eventId = generateEventId();
|
||||||
console.log('✅ 새로운 eventId 생성:', eventId);
|
console.log('🎉 ========================================');
|
||||||
|
console.log('✅ 새로운 이벤트 ID 생성:', eventId);
|
||||||
|
console.log('📋 선택된 목적:', selected);
|
||||||
|
console.log('🎉 ========================================');
|
||||||
|
|
||||||
// 쿠키에 저장
|
// 쿠키에 저장
|
||||||
setCookie('eventId', eventId, 1); // 1일 동안 유지
|
setCookie('eventId', eventId, 1); // 1일 동안 유지
|
||||||
|
console.log('🍪 쿠키에 eventId 저장 완료:', eventId);
|
||||||
|
|
||||||
|
// localStorage에도 저장
|
||||||
|
try {
|
||||||
|
localStorage.setItem('eventId', eventId);
|
||||||
|
console.log('💾 localStorage에 eventId 저장 완료:', eventId);
|
||||||
|
console.log('📦 저장된 데이터 확인:', {
|
||||||
|
eventId: eventId,
|
||||||
|
objective: selected,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ localStorage 저장 실패:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// objective와 eventId를 함께 전달
|
// objective와 eventId를 함께 전달
|
||||||
onNext({ objective: selected, eventId });
|
onNext({ objective: selected, eventId });
|
||||||
|
|||||||
@ -57,13 +57,74 @@ export default function EventsPage() {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const itemsPerPage = 20;
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
// API 데이터 가져오기
|
// 목업 데이터
|
||||||
const { events: apiEvents, loading, error, pageInfo, refetch } = useEvents({
|
const mockEvents = [
|
||||||
page: currentPage - 1,
|
{
|
||||||
size: itemsPerPage,
|
eventId: 'evt_2025012301',
|
||||||
sort: 'createdAt',
|
eventName: '신규 고객 환영 이벤트',
|
||||||
order: 'desc'
|
status: 'PUBLISHED' as ApiEventStatus,
|
||||||
});
|
startDate: '2025-01-23',
|
||||||
|
endDate: '2025-02-23',
|
||||||
|
participants: 1250,
|
||||||
|
targetParticipants: 2000,
|
||||||
|
roi: 320,
|
||||||
|
createdAt: '2025-01-15T00:00:00',
|
||||||
|
aiRecommendations: [{
|
||||||
|
reward: '스타벅스 아메리카노 (5명)',
|
||||||
|
participationMethod: '전화번호 입력'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: 'evt_2025011502',
|
||||||
|
eventName: '재방문 고객 감사 이벤트',
|
||||||
|
status: 'PUBLISHED' as ApiEventStatus,
|
||||||
|
startDate: '2025-01-15',
|
||||||
|
endDate: '2025-02-15',
|
||||||
|
participants: 890,
|
||||||
|
targetParticipants: 1000,
|
||||||
|
roi: 280,
|
||||||
|
createdAt: '2025-01-10T00:00:00',
|
||||||
|
aiRecommendations: [{
|
||||||
|
reward: '커피 쿠폰 (10명)',
|
||||||
|
participationMethod: 'SNS 팔로우'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: 'evt_2025010803',
|
||||||
|
eventName: '신년 특별 할인 이벤트',
|
||||||
|
status: 'ENDED' as ApiEventStatus,
|
||||||
|
startDate: '2025-01-01',
|
||||||
|
endDate: '2025-01-08',
|
||||||
|
participants: 2500,
|
||||||
|
targetParticipants: 2000,
|
||||||
|
roi: 450,
|
||||||
|
createdAt: '2024-12-28T00:00:00',
|
||||||
|
aiRecommendations: [{
|
||||||
|
reward: '10% 할인 쿠폰 (선착순 100명)',
|
||||||
|
participationMethod: '구매 인증'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: 'evt_2025020104',
|
||||||
|
eventName: '2월 신메뉴 출시 기념',
|
||||||
|
status: 'DRAFT' as ApiEventStatus,
|
||||||
|
startDate: '2025-02-01',
|
||||||
|
endDate: '2025-02-28',
|
||||||
|
participants: 0,
|
||||||
|
targetParticipants: 1500,
|
||||||
|
roi: 0,
|
||||||
|
createdAt: '2025-01-25T00:00:00',
|
||||||
|
aiRecommendations: [{
|
||||||
|
reward: '신메뉴 무료 쿠폰 (20명)',
|
||||||
|
participationMethod: '이메일 등록'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const loading = false;
|
||||||
|
const error = null;
|
||||||
|
const apiEvents = mockEvents;
|
||||||
|
const refetch = () => {};
|
||||||
|
|
||||||
// API 상태를 UI 상태로 매핑
|
// API 상태를 UI 상태로 매핑
|
||||||
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
|
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
|
||||||
@ -241,41 +302,6 @@ export default function EventsPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error State */}
|
|
||||||
{error && (
|
|
||||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4, bgcolor: '#FEE2E2' }}>
|
|
||||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
|
||||||
<Warning sx={{ fontSize: 48, color: '#DC2626', mb: 2 }} />
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
sx={{ mb: 1, color: '#991B1B', fontSize: { xs: '1rem', sm: '1.25rem' } }}
|
|
||||||
>
|
|
||||||
이벤트 목록을 불러오는데 실패했습니다
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ color: '#7F1D1D', mb: 2 }}>
|
|
||||||
{error.message}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
component="button"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
sx={{
|
|
||||||
px: 3,
|
|
||||||
py: 1.5,
|
|
||||||
borderRadius: 2,
|
|
||||||
border: 'none',
|
|
||||||
bgcolor: '#DC2626',
|
|
||||||
color: 'white',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
'&:hover': { bgcolor: '#B91C1C' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
다시 시도
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summary Statistics */}
|
{/* Summary Statistics */}
|
||||||
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
|
||||||
|
|||||||
@ -13,24 +13,23 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Avatar,
|
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
InputAdornment,
|
|
||||||
IconButton,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Person, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-material';
|
import { CheckCircle } from '@mui/icons-material';
|
||||||
import { useAuthContext } from '@/features/auth';
|
import { useAuthContext } from '@/features/auth';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
import { userApi } from '@/entities/user';
|
import { userApi } from '@/entities/user';
|
||||||
import Header from '@/shared/ui/Header';
|
import Header from '@/shared/ui/Header';
|
||||||
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import userImage from '@/shared/ui/user_img.png';
|
||||||
|
|
||||||
// 기본 정보 스키마
|
// 기본 정보 스키마
|
||||||
const basicInfoSchema = z.object({
|
const basicInfoSchema = z.object({
|
||||||
@ -50,32 +49,13 @@ const businessInfoSchema = z.object({
|
|||||||
businessHours: z.string().optional(),
|
businessHours: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 비밀번호 변경 스키마
|
|
||||||
const passwordSchema = z
|
|
||||||
.object({
|
|
||||||
currentPassword: z.string().min(1, '현재 비밀번호를 입력해주세요'),
|
|
||||||
newPassword: z
|
|
||||||
.string()
|
|
||||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
|
||||||
.max(100, '비밀번호는 100자 이하여야 합니다'),
|
|
||||||
confirmPassword: z.string(),
|
|
||||||
})
|
|
||||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
||||||
message: '새 비밀번호가 일치하지 않습니다',
|
|
||||||
path: ['confirmPassword'],
|
|
||||||
});
|
|
||||||
|
|
||||||
type BasicInfoData = z.infer<typeof basicInfoSchema>;
|
type BasicInfoData = z.infer<typeof basicInfoSchema>;
|
||||||
type BusinessInfoData = z.infer<typeof businessInfoSchema>;
|
type BusinessInfoData = z.infer<typeof businessInfoSchema>;
|
||||||
type PasswordData = z.infer<typeof passwordSchema>;
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, logout, refreshProfile } = useAuthContext();
|
const { user, logout, refreshProfile } = useAuthContext();
|
||||||
const { showToast, setLoading } = useUIStore();
|
const { showToast, setLoading } = useUIStore();
|
||||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
||||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
||||||
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
||||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
||||||
const [profileLoaded, setProfileLoaded] = useState(false);
|
const [profileLoaded, setProfileLoaded] = useState(false);
|
||||||
@ -105,26 +85,12 @@ export default function ProfilePage() {
|
|||||||
resolver: zodResolver(businessInfoSchema),
|
resolver: zodResolver(businessInfoSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
businessName: '',
|
businessName: '',
|
||||||
businessType: '',
|
businessType: 'restaurant',
|
||||||
businessLocation: '',
|
businessLocation: '',
|
||||||
businessHours: '',
|
businessHours: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 비밀번호 변경 폼
|
|
||||||
const {
|
|
||||||
control: passwordControl,
|
|
||||||
handleSubmit: handlePasswordSubmit,
|
|
||||||
formState: { errors: passwordErrors },
|
|
||||||
reset: resetPassword,
|
|
||||||
} = useForm<PasswordData>({
|
|
||||||
resolver: zodResolver(passwordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 프로필 데이터 로드
|
// 프로필 데이터 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -164,7 +130,7 @@ export default function ProfilePage() {
|
|||||||
// 사업장 정보 폼 초기화
|
// 사업장 정보 폼 초기화
|
||||||
resetBusiness({
|
resetBusiness({
|
||||||
businessName: profile.storeName || '',
|
businessName: profile.storeName || '',
|
||||||
businessType: profile.industry || '',
|
businessType: profile.industry || 'restaurant',
|
||||||
businessLocation: profile.address || '',
|
businessLocation: profile.address || '',
|
||||||
businessHours: profile.businessHours || '',
|
businessHours: profile.businessHours || '',
|
||||||
});
|
});
|
||||||
@ -244,40 +210,6 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangePassword = async (data: PasswordData) => {
|
|
||||||
console.log('🔐 비밀번호 변경 시작');
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const passwordData = {
|
|
||||||
currentPassword: data.currentPassword,
|
|
||||||
newPassword: data.newPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('📡 비밀번호 변경 API 호출');
|
|
||||||
await userApi.changePassword(passwordData);
|
|
||||||
console.log('✅ 비밀번호 변경 성공');
|
|
||||||
|
|
||||||
showToast('비밀번호가 변경되었습니다', 'success');
|
|
||||||
resetPassword();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('❌ 비밀번호 변경 실패:', error);
|
|
||||||
|
|
||||||
let errorMessage = '비밀번호 변경에 실패했습니다';
|
|
||||||
if (error.response) {
|
|
||||||
errorMessage = error.response.data?.message ||
|
|
||||||
error.response.data?.error ||
|
|
||||||
`서버 오류 (${error.response.status})`;
|
|
||||||
} else if (error.request) {
|
|
||||||
errorMessage = '서버로부터 응답이 없습니다';
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(errorMessage, 'error');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
handleBasicSubmit((basicData) => {
|
handleBasicSubmit((basicData) => {
|
||||||
@ -319,18 +251,25 @@ export default function ProfilePage() {
|
|||||||
{/* 사용자 정보 섹션 */}
|
{/* 사용자 정보 섹션 */}
|
||||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10, textAlign: 'center' }}>
|
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10, textAlign: 'center' }}>
|
||||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
||||||
<Avatar
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
mx: 'auto',
|
mx: 'auto',
|
||||||
mb: 3,
|
mb: 3,
|
||||||
bgcolor: colors.purple,
|
borderRadius: '50%',
|
||||||
color: 'white',
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Person sx={{ fontSize: 56 }} />
|
<Image
|
||||||
</Avatar>
|
src={userImage}
|
||||||
|
alt="User Profile"
|
||||||
|
fill
|
||||||
|
style={{ objectFit: 'cover' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: 1 }}>
|
<Typography variant="h5" sx={{ ...responsiveText.h3, mb: 1 }}>
|
||||||
{user?.userName}
|
{user?.userName}
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -469,121 +408,6 @@ export default function ProfilePage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 비밀번호 변경 */}
|
|
||||||
<Card elevation={0} sx={{ ...cardStyles.default, mb: 10 }}>
|
|
||||||
<CardContent sx={{ p: { xs: 6, sm: 8 } }}>
|
|
||||||
<Typography variant="h6" sx={{ ...responsiveText.h4, fontWeight: 700, mb: 6 }}>
|
|
||||||
비밀번호 변경
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
||||||
<Controller
|
|
||||||
name="currentPassword"
|
|
||||||
control={passwordControl}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
type={showCurrentPassword ? 'text' : 'password'}
|
|
||||||
label="현재 비밀번호"
|
|
||||||
placeholder="현재 비밀번호를 입력하세요"
|
|
||||||
error={!!passwordErrors.currentPassword}
|
|
||||||
helperText={passwordErrors.currentPassword?.message}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showCurrentPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="newPassword"
|
|
||||||
control={passwordControl}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
type={showNewPassword ? 'text' : 'password'}
|
|
||||||
label="새 비밀번호"
|
|
||||||
placeholder="새 비밀번호를 입력하세요"
|
|
||||||
error={!!passwordErrors.newPassword}
|
|
||||||
helperText={passwordErrors.newPassword?.message || '8자 이상 입력해주세요'}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showNewPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="confirmPassword"
|
|
||||||
control={passwordControl}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
|
||||||
label="비밀번호 확인"
|
|
||||||
placeholder="비밀번호를 다시 입력하세요"
|
|
||||||
error={!!passwordErrors.confirmPassword}
|
|
||||||
helperText={passwordErrors.confirmPassword?.message}
|
|
||||||
InputProps={{
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
size="large"
|
|
||||||
onClick={handlePasswordSubmit(onChangePassword)}
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
py: 3,
|
|
||||||
borderRadius: 3,
|
|
||||||
fontSize: '1rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
borderWidth: 2,
|
|
||||||
'&:hover': {
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
비밀번호 변경
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 액션 버튼 */}
|
{/* 액션 버튼 */}
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
58
src/app/api/participations/[eventId]/draw-winners/route.ts
Normal file
58
src/app/api/participations/[eventId]/draw-winners/route.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { eventId: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { eventId } = params;
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('🎰 [Proxy] Draw winners request:', {
|
||||||
|
eventId,
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '인증 토큰이 필요합니다.' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/draw-winners`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Draw winners response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Draw winners error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '당첨자 추첨 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { eventId: string; participantId: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { eventId, participantId } = params;
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
|
||||||
|
console.log('👤 [Proxy] Get participant request:', {
|
||||||
|
eventId,
|
||||||
|
participantId,
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participants/${participantId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Get participant response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Get participant error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '참여자 조회 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app/api/participations/[eventId]/participants/route.ts
Normal file
56
src/app/api/participations/[eventId]/participants/route.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { eventId: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { eventId } = params;
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
console.log('📋 [Proxy] Get participants request:', {
|
||||||
|
eventId,
|
||||||
|
hasToken: !!token,
|
||||||
|
params: Object.fromEntries(searchParams),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = `${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participants${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Get participants response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Get participants error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '참여자 목록 조회 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app/api/participations/[eventId]/participate/route.ts
Normal file
56
src/app/api/participations/[eventId]/participate/route.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { eventId: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { eventId } = params;
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('🎫 [Proxy] Participate request:', {
|
||||||
|
eventId,
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${GATEWAY_HOST}/api/v1/participations/events/${eventId}/participate`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Participate response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Participate error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '참여 요청 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/app/api/participations/[eventId]/winners/route.ts
Normal file
56
src/app/api/participations/[eventId]/winners/route.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { eventId: string } }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { eventId } = params;
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
console.log('🏆 [Proxy] Get winners request:', {
|
||||||
|
eventId,
|
||||||
|
hasToken: !!token,
|
||||||
|
params: Object.fromEntries(searchParams),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = `${GATEWAY_HOST}/api/v1/participations/events/${eventId}/winners${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Get winners response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Get winners error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '당첨자 목록 조회 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/app/api/v1/users/login/route.ts
Normal file
41
src/app/api/v1/users/login/route.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('🔐 [Proxy] Login request:', {
|
||||||
|
email: body.email,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Login response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Login error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '로그인 요청 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/app/api/v1/users/logout/route.ts
Normal file
47
src/app/api/v1/users/logout/route.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
|
||||||
|
console.log('🚪 [Proxy] Logout request:', {
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Logout response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Logout error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '로그아웃 요청 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/app/api/v1/users/password/route.ts
Normal file
53
src/app/api/v1/users/password/route.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('🔑 [Proxy] Change password request:', {
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '인증 토큰이 필요합니다.' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/password`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('📥 [Proxy] Change password response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Change password response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse(null, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Change password error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '비밀번호 변경 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/app/api/v1/users/profile/route.ts
Normal file
95
src/app/api/v1/users/profile/route.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
|
||||||
|
console.log('👤 [Proxy] Get profile request:', {
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '인증 토큰이 필요합니다.' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/profile`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Get profile response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Get profile error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '프로필 조회 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.headers.get('Authorization');
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('✏️ [Proxy] Update profile request:', {
|
||||||
|
hasToken: !!token,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '인증 토큰이 필요합니다.' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/profile`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Update profile response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Update profile error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '프로필 수정 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/app/api/v1/users/register/route.ts
Normal file
42
src/app/api/v1/users/register/route.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const GATEWAY_HOST = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('📝 [Proxy] Register request:', {
|
||||||
|
email: body.email,
|
||||||
|
name: body.name,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${GATEWAY_HOST}/api/v1/users/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log('📥 [Proxy] Register response:', {
|
||||||
|
status: response.status,
|
||||||
|
success: response.ok,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [Proxy] Register error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: '회원가입 요청 중 오류가 발생했습니다.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/entities/participation/api/index.ts
Normal file
1
src/entities/participation/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { participationApi, default } from './participationApi';
|
||||||
142
src/entities/participation/api/participationApi.ts
Normal file
142
src/entities/participation/api/participationApi.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import type {
|
||||||
|
ParticipationRequest,
|
||||||
|
ParticipationResponse,
|
||||||
|
ApiResponse,
|
||||||
|
PageResponse,
|
||||||
|
DrawWinnersRequest,
|
||||||
|
DrawWinnersResponse,
|
||||||
|
} from '../model/types';
|
||||||
|
|
||||||
|
// Use Next.js API proxy to bypass CORS issues
|
||||||
|
const PARTICIPATION_API_BASE = '/api/participations';
|
||||||
|
|
||||||
|
const participationApiClient: AxiosInstance = axios.create({
|
||||||
|
baseURL: PARTICIPATION_API_BASE,
|
||||||
|
timeout: 90000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
participationApiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
console.log('🎫 Participation API Request:', {
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
if (token && config.headers) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('❌ Participation API Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
participationApiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('✅ Participation API Response:', {
|
||||||
|
status: response.status,
|
||||||
|
url: response.config.url,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('❌ Participation API Error:', {
|
||||||
|
message: error.message,
|
||||||
|
status: error.response?.status,
|
||||||
|
url: error.config?.url,
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Participation API Service
|
||||||
|
* 이벤트 참여 관리 API
|
||||||
|
*/
|
||||||
|
export const participationApi = {
|
||||||
|
/**
|
||||||
|
* 이벤트 참여
|
||||||
|
*/
|
||||||
|
participate: async (
|
||||||
|
eventId: string,
|
||||||
|
data: ParticipationRequest
|
||||||
|
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||||
|
const response = await participationApiClient.post<
|
||||||
|
ApiResponse<ParticipationResponse>
|
||||||
|
>(`/${eventId}/participate`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참여자 목록 조회
|
||||||
|
*/
|
||||||
|
getParticipants: async (
|
||||||
|
eventId: string,
|
||||||
|
params?: {
|
||||||
|
storeVisited?: boolean;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
sort?: string[];
|
||||||
|
}
|
||||||
|
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||||
|
const response = await participationApiClient.get<
|
||||||
|
ApiResponse<PageResponse<ParticipationResponse>>
|
||||||
|
>(`/${eventId}/participants`, { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 참여자 조회
|
||||||
|
*/
|
||||||
|
getParticipant: async (
|
||||||
|
eventId: string,
|
||||||
|
participantId: string
|
||||||
|
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||||
|
const response = await participationApiClient.get<
|
||||||
|
ApiResponse<ParticipationResponse>
|
||||||
|
>(`/${eventId}/participants/${participantId}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 당첨자 추첨
|
||||||
|
*/
|
||||||
|
drawWinners: async (
|
||||||
|
eventId: string,
|
||||||
|
data: DrawWinnersRequest
|
||||||
|
): Promise<ApiResponse<DrawWinnersResponse>> => {
|
||||||
|
const response = await participationApiClient.post<
|
||||||
|
ApiResponse<DrawWinnersResponse>
|
||||||
|
>(`/${eventId}/draw-winners`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 당첨자 목록 조회
|
||||||
|
*/
|
||||||
|
getWinners: async (
|
||||||
|
eventId: string,
|
||||||
|
params?: {
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
sort?: string[];
|
||||||
|
}
|
||||||
|
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||||
|
const response = await participationApiClient.get<
|
||||||
|
ApiResponse<PageResponse<ParticipationResponse>>
|
||||||
|
>(`/${eventId}/winners`, { params });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default participationApi;
|
||||||
10
src/entities/participation/index.ts
Normal file
10
src/entities/participation/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export { participationApi } from './api';
|
||||||
|
export type {
|
||||||
|
ParticipationRequest,
|
||||||
|
ParticipationResponse,
|
||||||
|
ApiResponse,
|
||||||
|
PageResponse,
|
||||||
|
DrawWinnersRequest,
|
||||||
|
DrawWinnersResponse,
|
||||||
|
WinnerSummary,
|
||||||
|
} from './model';
|
||||||
9
src/entities/participation/model/index.ts
Normal file
9
src/entities/participation/model/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type {
|
||||||
|
ParticipationRequest,
|
||||||
|
ParticipationResponse,
|
||||||
|
ApiResponse,
|
||||||
|
PageResponse,
|
||||||
|
DrawWinnersRequest,
|
||||||
|
DrawWinnersResponse,
|
||||||
|
WinnerSummary,
|
||||||
|
} from './types';
|
||||||
114
src/entities/participation/model/types.ts
Normal file
114
src/entities/participation/model/types.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Participation API Types
|
||||||
|
* 이벤트 참여 관련 타입 정의
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참여 요청
|
||||||
|
*/
|
||||||
|
export interface ParticipationRequest {
|
||||||
|
/** 이름 (2-50자, 필수) */
|
||||||
|
name: string;
|
||||||
|
/** 전화번호 (형식: "010-1234-5678", 필수) */
|
||||||
|
phoneNumber: string;
|
||||||
|
/** 이메일 (선택) */
|
||||||
|
email?: string;
|
||||||
|
/** 채널 (선택) */
|
||||||
|
channel?: string;
|
||||||
|
/** 마케팅 동의 (선택) */
|
||||||
|
agreeMarketing?: boolean;
|
||||||
|
/** 개인정보 동의 (필수) */
|
||||||
|
agreePrivacy: boolean;
|
||||||
|
/** 매장 방문 여부 (선택) */
|
||||||
|
storeVisited?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참여 응답
|
||||||
|
*/
|
||||||
|
export interface ParticipationResponse {
|
||||||
|
/** 참여자 ID (UUID) */
|
||||||
|
participantId: string;
|
||||||
|
/** 이벤트 ID */
|
||||||
|
eventId: string;
|
||||||
|
/** 이름 */
|
||||||
|
name: string;
|
||||||
|
/** 전화번호 */
|
||||||
|
phoneNumber: string;
|
||||||
|
/** 이메일 */
|
||||||
|
email?: string;
|
||||||
|
/** 채널 */
|
||||||
|
channel?: string;
|
||||||
|
/** 참여 일시 */
|
||||||
|
participatedAt: string;
|
||||||
|
/** 매장 방문 여부 */
|
||||||
|
storeVisited?: boolean;
|
||||||
|
/** 보너스 응모권 수 */
|
||||||
|
bonusEntries: number;
|
||||||
|
/** 당첨 여부 */
|
||||||
|
isWinner: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 공통 응답
|
||||||
|
*/
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
errorCode?: string;
|
||||||
|
message?: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지 응답
|
||||||
|
*/
|
||||||
|
export interface PageResponse<T> {
|
||||||
|
content: T[];
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
first: boolean;
|
||||||
|
last: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 당첨자 추첨 요청
|
||||||
|
*/
|
||||||
|
export interface DrawWinnersRequest {
|
||||||
|
/** 당첨자 수 (최소 1명, 필수) */
|
||||||
|
winnerCount: number;
|
||||||
|
/** 매장 방문 보너스 적용 여부 (선택) */
|
||||||
|
applyStoreVisitBonus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 당첨자 요약 정보
|
||||||
|
*/
|
||||||
|
export interface WinnerSummary {
|
||||||
|
/** 참여자 ID */
|
||||||
|
participantId: string;
|
||||||
|
/** 이름 */
|
||||||
|
name: string;
|
||||||
|
/** 전화번호 */
|
||||||
|
phoneNumber: string;
|
||||||
|
/** 등수 */
|
||||||
|
rank: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 당첨자 추첨 응답
|
||||||
|
*/
|
||||||
|
export interface DrawWinnersResponse {
|
||||||
|
/** 이벤트 ID */
|
||||||
|
eventId: string;
|
||||||
|
/** 총 참여자 수 */
|
||||||
|
totalParticipants: number;
|
||||||
|
/** 당첨자 수 */
|
||||||
|
winnerCount: number;
|
||||||
|
/** 추첨 일시 */
|
||||||
|
drawnAt: string;
|
||||||
|
/** 당첨자 목록 */
|
||||||
|
winners: WinnerSummary[];
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { apiClient } from '@/shared/api';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
import type {
|
import type {
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
@ -10,8 +10,57 @@ import type {
|
|||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
} from '../model/types';
|
} from '../model/types';
|
||||||
|
|
||||||
|
// Use Next.js API proxy to bypass CORS issues
|
||||||
const USER_API_BASE = '/api/v1/users';
|
const USER_API_BASE = '/api/v1/users';
|
||||||
|
|
||||||
|
const userApiClient: AxiosInstance = axios.create({
|
||||||
|
baseURL: USER_API_BASE,
|
||||||
|
timeout: 90000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
userApiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
console.log('👤 User API Request:', {
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
if (token && config.headers) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('❌ User API Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
userApiClient.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('✅ User API Response:', {
|
||||||
|
status: response.status,
|
||||||
|
url: response.config.url,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('❌ User API Error:', {
|
||||||
|
message: error.message,
|
||||||
|
status: error.response?.status,
|
||||||
|
url: error.config?.url,
|
||||||
|
});
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User API Service
|
* User API Service
|
||||||
* 사용자 인증 및 프로필 관리 API
|
* 사용자 인증 및 프로필 관리 API
|
||||||
@ -21,8 +70,8 @@ export const userApi = {
|
|||||||
* 로그인
|
* 로그인
|
||||||
*/
|
*/
|
||||||
login: async (data: LoginRequest): Promise<LoginResponse> => {
|
login: async (data: LoginRequest): Promise<LoginResponse> => {
|
||||||
const response = await apiClient.post<LoginResponse>(
|
const response = await userApiClient.post<LoginResponse>(
|
||||||
`${USER_API_BASE}/login`,
|
'/login',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -33,15 +82,14 @@ export const userApi = {
|
|||||||
*/
|
*/
|
||||||
register: async (data: RegisterRequest): Promise<RegisterResponse> => {
|
register: async (data: RegisterRequest): Promise<RegisterResponse> => {
|
||||||
console.log('📞 userApi.register 호출');
|
console.log('📞 userApi.register 호출');
|
||||||
console.log('🎯 URL:', `${USER_API_BASE}/register`);
|
|
||||||
console.log('📦 요청 데이터:', {
|
console.log('📦 요청 데이터:', {
|
||||||
...data,
|
...data,
|
||||||
password: '***'
|
password: '***'
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<RegisterResponse>(
|
const response = await userApiClient.post<RegisterResponse>(
|
||||||
`${USER_API_BASE}/register`,
|
'/register',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
console.log('✅ userApi.register 성공:', response.data);
|
console.log('✅ userApi.register 성공:', response.data);
|
||||||
@ -56,15 +104,9 @@ export const userApi = {
|
|||||||
* 로그아웃
|
* 로그아웃
|
||||||
*/
|
*/
|
||||||
logout: async (): Promise<LogoutResponse> => {
|
logout: async (): Promise<LogoutResponse> => {
|
||||||
const token = localStorage.getItem('accessToken');
|
const response = await userApiClient.post<LogoutResponse>(
|
||||||
const response = await apiClient.post<LogoutResponse>(
|
'/logout',
|
||||||
`${USER_API_BASE}/logout`,
|
{}
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@ -73,8 +115,8 @@ export const userApi = {
|
|||||||
* 프로필 조회
|
* 프로필 조회
|
||||||
*/
|
*/
|
||||||
getProfile: async (): Promise<ProfileResponse> => {
|
getProfile: async (): Promise<ProfileResponse> => {
|
||||||
const response = await apiClient.get<ProfileResponse>(
|
const response = await userApiClient.get<ProfileResponse>(
|
||||||
`${USER_API_BASE}/profile`
|
'/profile'
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@ -85,8 +127,8 @@ export const userApi = {
|
|||||||
updateProfile: async (
|
updateProfile: async (
|
||||||
data: UpdateProfileRequest
|
data: UpdateProfileRequest
|
||||||
): Promise<ProfileResponse> => {
|
): Promise<ProfileResponse> => {
|
||||||
const response = await apiClient.put<ProfileResponse>(
|
const response = await userApiClient.put<ProfileResponse>(
|
||||||
`${USER_API_BASE}/profile`,
|
'/profile',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -96,7 +138,7 @@ export const userApi = {
|
|||||||
* 비밀번호 변경
|
* 비밀번호 변경
|
||||||
*/
|
*/
|
||||||
changePassword: async (data: ChangePasswordRequest): Promise<void> => {
|
changePassword: async (data: ChangePasswordRequest): Promise<void> => {
|
||||||
await apiClient.put(`${USER_API_BASE}/password`, data);
|
await userApiClient.put('/password', data);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export interface LoginRequest {
|
|||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
token: string;
|
token: string;
|
||||||
userId: number;
|
userId: string; // UUID format
|
||||||
userName: string;
|
userName: string;
|
||||||
role: string;
|
role: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -31,9 +31,9 @@ export interface RegisterRequest {
|
|||||||
|
|
||||||
export interface RegisterResponse {
|
export interface RegisterResponse {
|
||||||
token: string;
|
token: string;
|
||||||
userId: number;
|
userId: string; // UUID format
|
||||||
userName: string;
|
userName: string;
|
||||||
storeId: number;
|
storeId: string; // UUID format
|
||||||
storeName: string;
|
storeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,12 +45,12 @@ export interface LogoutResponse {
|
|||||||
|
|
||||||
// 프로필 조회/수정
|
// 프로필 조회/수정
|
||||||
export interface ProfileResponse {
|
export interface ProfileResponse {
|
||||||
userId: number;
|
userId: string; // UUID format
|
||||||
userName: string;
|
userName: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
storeId: number;
|
storeId: string; // UUID format
|
||||||
storeName: string;
|
storeName: string;
|
||||||
industry: string;
|
industry: string;
|
||||||
address: string;
|
address: string;
|
||||||
@ -77,12 +77,12 @@ export interface ChangePasswordRequest {
|
|||||||
|
|
||||||
// User 상태
|
// User 상태
|
||||||
export interface User {
|
export interface User {
|
||||||
userId: number;
|
userId: string; // UUID format
|
||||||
userName: string;
|
userName: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
storeId?: number;
|
storeId?: string; // UUID format
|
||||||
storeName?: string;
|
storeName?: string;
|
||||||
industry?: string;
|
industry?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const API_HOSTS = {
|
|||||||
|
|
||||||
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';
|
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'api';
|
||||||
|
|
||||||
// 기본 User API 클라이언트 (기존 호환성 유지)
|
// 기본 User API 클라이언트 (Gateway 직접 연결)
|
||||||
const API_BASE_URL = API_HOSTS.user;
|
const API_BASE_URL = API_HOSTS.user;
|
||||||
|
|
||||||
export const apiClient: AxiosInstance = axios.create({
|
export const apiClient: AxiosInstance = axios.create({
|
||||||
|
|||||||
@ -15,14 +15,14 @@ import type {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 이벤트 참여 신청
|
* 이벤트 참여 신청
|
||||||
* POST /api/v1/events/{eventId}/participate
|
* POST /api/participations/{eventId}/participate
|
||||||
*/
|
*/
|
||||||
export const participate = async (
|
export const participate = async (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
data: ParticipationRequest
|
data: ParticipationRequest
|
||||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||||
const response = await axios.post<ApiResponse<ParticipationResponse>>(
|
const response = await axios.post<ApiResponse<ParticipationResponse>>(
|
||||||
`/api/v1/events/${eventId}/participate`,
|
`/api/participations/${eventId}/participate`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -30,37 +30,35 @@ export const participate = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 참여자 목록 조회 (페이징)
|
* 참여자 목록 조회 (페이징)
|
||||||
* GET /api/v1/events/{eventId}/participants
|
* GET /api/participations/{eventId}/participants
|
||||||
*/
|
*/
|
||||||
export const getParticipants = async (
|
export const getParticipants = async (
|
||||||
params: GetParticipantsParams
|
params: GetParticipantsParams
|
||||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||||
const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params;
|
const { eventId, storeVisited, page = 0, size = 20, sort = ['createdAt,DESC'] } = params;
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (storeVisited !== undefined) queryParams.append('storeVisited', String(storeVisited));
|
||||||
|
queryParams.append('page', String(page));
|
||||||
|
queryParams.append('size', String(size));
|
||||||
|
sort.forEach(s => queryParams.append('sort', s));
|
||||||
|
|
||||||
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
||||||
`/api/v1/events/${eventId}/participants`,
|
`/api/participations/${eventId}/participants?${queryParams.toString()}`
|
||||||
{
|
|
||||||
params: {
|
|
||||||
storeVisited,
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
sort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 참여자 정보 조회
|
* 특정 참여자 정보 조회
|
||||||
* GET /api/v1/events/{eventId}/participants/{participantId}
|
* GET /api/participations/{eventId}/participants/{participantId}
|
||||||
*/
|
*/
|
||||||
export const getParticipant = async (
|
export const getParticipant = async (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
participantId: string
|
participantId: string
|
||||||
): Promise<ApiResponse<ParticipationResponse>> => {
|
): Promise<ApiResponse<ParticipationResponse>> => {
|
||||||
const response = await axios.get<ApiResponse<ParticipationResponse>>(
|
const response = await axios.get<ApiResponse<ParticipationResponse>>(
|
||||||
`/api/v1/events/${eventId}/participants/${participantId}`
|
`/api/participations/${eventId}/participants/${participantId}`
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
@ -113,7 +111,7 @@ export const searchParticipants = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 당첨자 추첨
|
* 당첨자 추첨
|
||||||
* POST /api/v1/events/{eventId}/draw-winners
|
* POST /api/participations/{eventId}/draw-winners
|
||||||
*/
|
*/
|
||||||
export const drawWinners = async (
|
export const drawWinners = async (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
@ -121,7 +119,7 @@ export const drawWinners = async (
|
|||||||
applyStoreVisitBonus?: boolean
|
applyStoreVisitBonus?: boolean
|
||||||
): Promise<ApiResponse<import('../types/api.types').DrawWinnersResponse>> => {
|
): Promise<ApiResponse<import('../types/api.types').DrawWinnersResponse>> => {
|
||||||
const response = await axios.post<ApiResponse<import('../types/api.types').DrawWinnersResponse>>(
|
const response = await axios.post<ApiResponse<import('../types/api.types').DrawWinnersResponse>>(
|
||||||
`/api/v1/events/${eventId}/draw-winners`,
|
`/api/participations/${eventId}/draw-winners`,
|
||||||
{
|
{
|
||||||
winnerCount,
|
winnerCount,
|
||||||
applyStoreVisitBonus,
|
applyStoreVisitBonus,
|
||||||
@ -132,7 +130,7 @@ export const drawWinners = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 당첨자 목록 조회
|
* 당첨자 목록 조회
|
||||||
* GET /api/v1/events/{eventId}/winners
|
* GET /api/participations/{eventId}/winners
|
||||||
*/
|
*/
|
||||||
export const getWinners = async (
|
export const getWinners = async (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
@ -140,15 +138,13 @@ export const getWinners = async (
|
|||||||
size = 20,
|
size = 20,
|
||||||
sort: string[] = ['winnerRank,ASC']
|
sort: string[] = ['winnerRank,ASC']
|
||||||
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
): Promise<ApiResponse<PageResponse<ParticipationResponse>>> => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('page', String(page));
|
||||||
|
queryParams.append('size', String(size));
|
||||||
|
sort.forEach(s => queryParams.append('sort', s));
|
||||||
|
|
||||||
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
const response = await axios.get<ApiResponse<PageResponse<ParticipationResponse>>>(
|
||||||
`/api/v1/events/${eventId}/winners`,
|
`/api/participations/${eventId}/winners?${queryParams.toString()}`
|
||||||
{
|
|
||||||
params: {
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
sort,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|||||||
BIN
src/shared/ui/user_img.png
Normal file
BIN
src/shared/ui/user_img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@ -2,7 +2,7 @@ import { create } from 'zustand';
|
|||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string; // UUID format
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user