내부 api 삭제

This commit is contained in:
2025-11-24 22:50:28 +09:00
parent 74eee0a3c0
commit eaec13e386
14 changed files with 342 additions and 1863 deletions

View File

@@ -1,211 +0,0 @@
# API 엔드포인트 문서
이 문서는 데이터베이스에 데이터를 생성하는 API 엔드포인트를 설명합니다.
## 📋 API 목록
### 1. 사용자 API (`/api/users`)
#### POST - 사용자 생성
```bash
POST /api/users
Content-Type: application/json
{
"email": "user@example.com",
"password": "hashed_password",
"name": "홍길동",
"phone": "010-1234-5678",
"gender": "M",
"birthdate": "1990-01-01",
"role": "LEARNER", // 또는 "ADMIN"
"status": "ACTIVE" // 또는 "INACTIVE"
}
```
**응답:**
```json
{
"message": "사용자가 성공적으로 생성되었습니다.",
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "홍길동",
...
}
}
```
#### GET - 사용자 목록 조회
```bash
GET /api/users?role=LEARNER&status=ACTIVE&page=1&limit=10
```
**쿼리 파라미터:**
- `role`: 필터링할 역할 (LEARNER, ADMIN)
- `status`: 필터링할 상태 (ACTIVE, INACTIVE)
- `page`: 페이지 번호 (기본값: 1)
- `limit`: 페이지당 항목 수 (기본값: 10)
---
### 2. 교육과정 API (`/api/courses`)
#### POST - 교육과정 생성
```bash
POST /api/courses
Content-Type: application/json
{
"courseName": "웹 개발 기초",
"instructorId": "instructor_uuid",
"createdById": "admin_uuid" // 선택사항, 기본값: instructorId
}
```
**응답:**
```json
{
"message": "교육과정이 성공적으로 생성되었습니다.",
"course": {
"id": "uuid",
"courseName": "웹 개발 기초",
"instructor": { ... },
"createdBy": { ... }
}
}
```
#### GET - 교육과정 목록 조회
```bash
GET /api/courses?instructorId=uuid&page=1&limit=10
```
---
### 3. 강좌 API (`/api/lessons`)
#### POST - 강좌 생성
```bash
POST /api/lessons
Content-Type: application/json
{
"courseId": "course_uuid",
"lessonName": "HTML 기초",
"learningGoal": "HTML의 기본 문법을 이해하고 활용할 수 있다.",
"createdById": "admin_uuid" // 선택사항
}
```
**응답:**
```json
{
"message": "강좌가 성공적으로 생성되었습니다.",
"lesson": {
"id": "uuid",
"lessonName": "HTML 기초",
"course": { ... },
"createdBy": { ... }
}
}
```
#### GET - 강좌 목록 조회
```bash
GET /api/lessons?courseId=uuid&page=1&limit=10
```
---
### 4. 공지사항 API (`/api/notices`)
#### POST - 공지사항 생성
```bash
POST /api/notices
Content-Type: application/json
{
"title": "공지사항 제목",
"content": "공지사항 내용",
"writerId": "admin_uuid",
"hasAttachment": false // 선택사항
}
```
**응답:**
```json
{
"message": "공지사항이 성공적으로 생성되었습니다.",
"notice": {
"id": "uuid",
"title": "공지사항 제목",
"content": "공지사항 내용",
"writer": { ... }
}
}
```
#### GET - 공지사항 목록 조회
```bash
GET /api/notices?writerId=uuid&page=1&limit=10
```
---
## 🔧 사용 예시
### JavaScript/TypeScript (fetch)
```typescript
// 사용자 생성
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
password: 'hashed_password',
name: '홍길동',
role: 'LEARNER',
}),
});
const data = await response.json();
console.log(data);
```
### cURL
```bash
# 사용자 생성
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "hashed_password",
"name": "홍길동",
"role": "LEARNER"
}'
```
---
## ⚠️ 주의사항
1. **비밀번호 해시화**: 실제 프로덕션에서는 비밀번호를 해시화하여 저장해야 합니다.
2. **인증/인가**: 현재 API는 인증이 없습니다. 프로덕션에서는 JWT나 세션 기반 인증을 추가해야 합니다.
3. **에러 처리**: 모든 API는 적절한 에러 응답을 반환합니다.
4. **데이터 검증**: 필수 필드 검증이 포함되어 있습니다.
---
## 📝 다음 단계
- [ ] 인증 미들웨어 추가
- [ ] 비밀번호 해시화 로직 추가
- [ ] 파일 업로드 API 추가 (공지사항 첨부파일 등)
- [ ] 수정/삭제 API 추가
- [ ] 상세 조회 API 추가

View File

@@ -1,126 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 교육과정 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { courseName, instructorId, createdById } = body;
// 필수 필드 검증
if (!courseName || !instructorId) {
return NextResponse.json(
{ error: '교육과정명과 강사 ID는 필수입니다.' },
{ status: 400 }
);
}
// 강사 존재 확인
const instructor = await prisma.user.findUnique({
where: { id: instructorId },
});
if (!instructor) {
return NextResponse.json(
{ error: '강사를 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 교육과정 생성
const course = await prisma.course.create({
data: {
courseName,
instructorId,
createdById: createdById || instructorId,
},
include: {
instructor: {
select: {
id: true,
name: true,
email: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
},
});
return NextResponse.json(
{ message: '교육과정이 성공적으로 생성되었습니다.', course },
{ status: 201 }
);
} catch (error) {
console.error('교육과정 생성 오류:', error);
return NextResponse.json(
{ error: '교육과정 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 교육과정 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const instructorId = searchParams.get('instructorId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (instructorId) where.instructorId = instructorId;
const [courses, total] = await Promise.all([
prisma.course.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
instructor: {
select: {
id: true,
name: true,
email: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
lessons: true,
},
},
},
}),
prisma.course.count({ where }),
]);
return NextResponse.json({
courses,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('교육과정 조회 오류:', error);
return NextResponse.json(
{ error: '교육과정 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -1,272 +0,0 @@
/**
* API 사용 예제 모음
*
* 이 파일은 API를 사용하는 다양한 예제를 제공합니다.
* 실제 프로젝트에서 참고하여 사용하세요.
*/
// ============================================
// 1. 사용자 API 예제
// ============================================
/**
* 사용자 생성 예제
*/
export async function createUserExample() {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'newuser@example.com',
password: 'hashed_password_here', // 실제로는 bcrypt로 해시화
name: '홍길동',
phone: '010-1234-5678',
gender: 'M',
birthdate: '1990-01-01',
role: 'LEARNER',
status: 'ACTIVE',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.user;
}
/**
* 사용자 목록 조회 예제 (필터링)
*/
export async function getUsersExample() {
const params = new URLSearchParams({
role: 'LEARNER',
status: 'ACTIVE',
page: '1',
limit: '10',
});
const response = await fetch(`/api/users?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 2. 교육과정 API 예제
// ============================================
/**
* 교육과정 생성 예제
*/
export async function createCourseExample(instructorId: string) {
const response = await fetch('/api/courses', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseName: '웹 개발 기초',
instructorId: instructorId,
createdById: instructorId, // 선택사항
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.course;
}
/**
* 교육과정 목록 조회 예제
*/
export async function getCoursesExample(instructorId?: string) {
const params = new URLSearchParams();
if (instructorId) {
params.append('instructorId', instructorId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/courses?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 3. 강좌 API 예제
// ============================================
/**
* 강좌 생성 예제
*/
export async function createLessonExample(courseId: string) {
const response = await fetch('/api/lessons', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseId: courseId,
lessonName: 'HTML 기초',
learningGoal: 'HTML의 기본 문법을 이해하고 활용할 수 있다.',
createdById: undefined, // 선택사항
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.lesson;
}
/**
* 강좌 목록 조회 예제
*/
export async function getLessonsExample(courseId?: string) {
const params = new URLSearchParams();
if (courseId) {
params.append('courseId', courseId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/lessons?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 4. 공지사항 API 예제
// ============================================
/**
* 공지사항 생성 예제
*/
export async function createNoticeExample(writerId: string) {
const response = await fetch('/api/notices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '공지사항 제목',
content: '공지사항 내용입니다.\n여러 줄로 작성할 수 있습니다.',
writerId: writerId,
hasAttachment: false,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
const data = await response.json();
return data.notice;
}
/**
* 공지사항 목록 조회 예제
*/
export async function getNoticesExample(writerId?: string) {
const params = new URLSearchParams();
if (writerId) {
params.append('writerId', writerId);
}
params.append('page', '1');
params.append('limit', '10');
const response = await fetch(`/api/notices?${params.toString()}`);
const data = await response.json();
return data;
}
// ============================================
// 5. 통합 예제 - 전체 워크플로우
// ============================================
/**
* 전체 워크플로우 예제
* 1. 관리자 사용자 생성
* 2. 교육과정 생성
* 3. 강좌 생성
* 4. 공지사항 생성
*/
export async function completeWorkflowExample() {
try {
// 1. 관리자 사용자 생성
const adminUser = await createUserExample();
console.log('관리자 생성:', adminUser);
// 2. 교육과정 생성
const course = await createCourseExample(adminUser.id);
console.log('교육과정 생성:', course);
// 3. 강좌 생성
const lesson = await createLessonExample(course.id);
console.log('강좌 생성:', lesson);
// 4. 공지사항 생성
const notice = await createNoticeExample(adminUser.id);
console.log('공지사항 생성:', notice);
return {
admin: adminUser,
course,
lesson,
notice,
};
} catch (error) {
console.error('워크플로우 실행 중 오류:', error);
throw error;
}
}
// ============================================
// 6. 에러 처리 예제
// ============================================
/**
* 에러 처리를 포함한 안전한 API 호출 예제
*/
export async function safeApiCall<T>(
apiCall: () => Promise<Response>
): Promise<{ data?: T; error?: string }> {
try {
const response = await apiCall();
if (!response.ok) {
const errorData = await response.json();
return { error: errorData.error || '알 수 없는 오류가 발생했습니다.' };
}
const data = await response.json();
return { data: data as T };
} catch (error) {
return {
error: error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.',
};
}
}
// 사용 예시:
// const result = await safeApiCall(() =>
// fetch('/api/users', { method: 'POST', ... })
// );
// if (result.error) {
// console.error(result.error);
// } else {
// console.log(result.data);
// }

View File

@@ -1,127 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 강좌 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { courseId, lessonName, learningGoal, createdById } = body;
// 필수 필드 검증
if (!courseId || !lessonName) {
return NextResponse.json(
{ error: '교육과정 ID와 강좌명은 필수입니다.' },
{ status: 400 }
);
}
// 교육과정 존재 확인
const course = await prisma.course.findUnique({
where: { id: courseId },
});
if (!course) {
return NextResponse.json(
{ error: '교육과정을 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 강좌 생성
const lesson = await prisma.lesson.create({
data: {
courseId,
lessonName,
learningGoal: learningGoal || null,
createdById: createdById || course.createdById,
},
include: {
course: {
select: {
id: true,
courseName: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
},
});
return NextResponse.json(
{ message: '강좌가 성공적으로 생성되었습니다.', lesson },
{ status: 201 }
);
} catch (error) {
console.error('강좌 생성 오류:', error);
return NextResponse.json(
{ error: '강좌 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 강좌 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const courseId = searchParams.get('courseId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (courseId) where.courseId = courseId;
const [lessons, total] = await Promise.all([
prisma.lesson.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
course: {
select: {
id: true,
courseName: true,
},
},
createdBy: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
videos: true,
vrContents: true,
questions: true,
},
},
},
}),
prisma.lesson.count({ where }),
]);
return NextResponse.json({
lessons,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('강좌 조회 오류:', error);
return NextResponse.json(
{ error: '강좌 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -1,109 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
// 공지사항 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, content, writerId, hasAttachment } = body;
// 필수 필드 검증
if (!title || !content || !writerId) {
return NextResponse.json(
{ error: '제목, 내용, 작성자 ID는 필수입니다.' },
{ status: 400 }
);
}
// 작성자 존재 확인
const writer = await prisma.user.findUnique({
where: { id: writerId },
});
if (!writer) {
return NextResponse.json(
{ error: '작성자를 찾을 수 없습니다.' },
{ status: 404 }
);
}
// 공지사항 생성
const notice = await prisma.notice.create({
data: {
title,
content,
writerId,
hasAttachment: hasAttachment || false,
},
include: {
writer: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
return NextResponse.json(
{ message: '공지사항이 성공적으로 생성되었습니다.', notice },
{ status: 201 }
);
} catch (error) {
console.error('공지사항 생성 오류:', error);
return NextResponse.json(
{ error: '공지사항 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 공지사항 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const writerId = searchParams.get('writerId');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (writerId) where.writerId = writerId;
const [notices, total] = await Promise.all([
prisma.notice.findMany({
where,
skip,
take: limit,
orderBy: { date: 'desc' },
include: {
writer: {
select: {
id: true,
name: true,
},
},
},
}),
prisma.notice.count({ where }),
]);
return NextResponse.json({
notices,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('공지사항 조회 오류:', error);
return NextResponse.json(
{ error: '공지사항 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -1,115 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { UserRole, UserStatus } from '@prisma/client';
// 사용자 생성
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password, name, phone, gender, birthdate, role, status } = body;
// 필수 필드 검증
if (!email || !password || !name) {
return NextResponse.json(
{ error: '이메일, 비밀번호, 이름은 필수입니다.' },
{ status: 400 }
);
}
// 이메일 중복 확인
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return NextResponse.json(
{ error: '이미 존재하는 이메일입니다.' },
{ status: 409 }
);
}
// 사용자 생성
const user = await prisma.user.create({
data: {
email,
password, // 실제로는 해시화된 비밀번호를 저장해야 합니다
name,
phone: phone || null,
gender: gender || null,
birthdate: birthdate ? new Date(birthdate) : null,
role: role || UserRole.LEARNER,
status: status || UserStatus.ACTIVE,
},
});
// 비밀번호 제외하고 반환
const { password: _, ...userWithoutPassword } = user;
return NextResponse.json(
{ message: '사용자가 성공적으로 생성되었습니다.', user: userWithoutPassword },
{ status: 201 }
);
} catch (error) {
console.error('사용자 생성 오류:', error);
return NextResponse.json(
{ error: '사용자 생성 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// 사용자 목록 조회
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const role = searchParams.get('role');
const status = searchParams.get('status');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const skip = (page - 1) * limit;
const where: any = {};
if (role) where.role = role;
if (status) where.status = status;
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
phone: true,
gender: true,
birthdate: true,
role: true,
status: true,
joinDate: true,
createdAt: true,
updatedAt: true,
},
}),
prisma.user.count({ where }),
]);
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
} catch (error) {
console.error('사용자 조회 오류:', error);
return NextResponse.json(
{ error: '사용자 조회 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}

View File

@@ -27,12 +27,36 @@ export default function FindIdPage() {
return Object.keys(next).length === 0;
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!validateAll()) return;
const mockUserId = `${name.trim()}@example.com`;
setFoundUserId(mockUserId);
setIsDoneOpen(true);
try {
const response = await fetch('https://hrdi.coconutmeet.net/auth/find-id', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
phone: phone,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error('아이디 찾기 실패:', errorData.error || response.statusText);
setIsFailedOpen(true);
return;
}
const data = await response.json();
setFoundUserId(data.email);
setIsDoneOpen(true);
} catch (error) {
console.error('아이디 찾기 오류:', error);
setIsFailedOpen(true);
}
}
return (

View File

@@ -41,3 +41,11 @@ body {
background: var(--background);
color: var(--foreground);
}
button {
cursor: pointer;
}
button:hover {
cursor: pointer;
}

View File

@@ -20,13 +20,57 @@ export default function LoginPage() {
const [idError, setIdError] = useState("");
const [passwordError, setPasswordError] = useState("");
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// 실제 로그인 API 연동 전까지는 실패 모달을 노출합니다.
// API 연동 시 결과에 따라 성공/실패 분기에서 setIsLoginErrorOpen(true) 호출로 교체하세요.
// if (userId.trim().length > 0 && password.trim().length > 0) {
// setIsLoginErrorOpen(true);
// }
if (userId.trim().length === 0 || password.trim().length === 0) {
return;
}
try {
const response = await fetch("https://hrdi.coconutmeet.net/auth/login", {
method: "POST",
headers: {"Content-Type": "application/json",},
body: JSON.stringify({
email: userId,
password: password
})
});
if (!response.ok) {
let errorMessage = `로그인 실패 (${response.status})`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error;
} else if (errorData.message) {
errorMessage = errorData.message;
} else if (response.statusText) {
errorMessage = `${response.statusText} (${response.status})`;
}
} catch (parseError) {
if (response.statusText) {
errorMessage = `${response.statusText} (${response.status})`;
}
}
console.error("로그인 실패:", errorMessage);
setIsLoginErrorOpen(true);
return;
}
const data = await response.json();
console.log("로그인 성공:", data);
// 로그인 성공 시 처리 (예: 토큰 저장, 리다이렉트 등)
// TODO: 성공 시 처리 로직 추가 (예: localStorage에 토큰 저장, 메인 페이지로 이동 등)
// if (data.token) {
// localStorage.setItem('token', data.token);
// window.location.href = '/menu';
// }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "네트워크 오류가 발생했습니다.";
console.error("로그인 오류:", errorMessage);
setIsLoginErrorOpen(true);
}
}
return (
@@ -106,12 +150,12 @@ export default function LoginPage() {
onBlur={() => setIsPasswordFocused(false)}
placeholder="비밀번호"
className="
h-[40px] px-[12px] py-[7px] rounded-[8px] w-full border border-neutral-40
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
focus:appearance-none focus:border-neutral-700
text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text
pr-[40px]
"
h-[40px] px-[12px] py-[7px] rounded-[8px] w-full border border-neutral-40
focus:outline-none focus:ring-0 focus:ring-offset-0 focus:shadow-none
focus:appearance-none focus:border-neutral-700
text-[18px] text-neutral-700 font-normal leading-[150%] placeholder:text-input-placeholder-text
pr-[40px]
"
/>
{password.trim().length > 0 && isPasswordFocused && (
<button

View File

@@ -133,6 +133,10 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
}
);
if (!response.ok) {
if (response.status === 409) {
setErrors((prev) => ({ ...prev, email: "이메일이 중복되었습니다." }));
return;
}
console.error("이메일 인증번호 전송 실패:", response.statusText);
alert("인증번호 전송실패");
return;
@@ -141,6 +145,12 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
setEmailCodeSent(true);
setEmailCode("");
setEmailCodeVerified(false);
// 성공 시 이메일 에러 제거
setErrors((prev) => {
const next = { ...prev };
if (next.email) delete next.email;
return next;
});
} catch (error) {
console.error("이메일 인증번호 전송 오류:", error);
alert("인증번호 전송실패");
@@ -275,7 +285,17 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => {
setEmail(e.target.value);
// 이메일 변경 시 중복 오류 제거
if (errors.email === "이메일이 중복되었습니다.") {
setErrors((prev) => {
const next = { ...prev };
delete next.email;
return next;
});
}
}}
onFocus={() => setFocused((p) => ({ ...p, email: true }))}
onBlur={() => setFocused((p) => ({ ...p, email: false }))}
placeholder="이메일을 입력해 주세요."
@@ -296,10 +316,10 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
<button
type="button"
disabled={!isEmailValid || emailCodeVerified}
className={`h-[40px] px-[12px] rounded-[8px] text-[16px] font-semibold ${isEmailValid && !emailCodeVerified ? "bg-inactive-button text-white" : "bg-gray-50 text-input-placeholder-text"}`}
className={`h-[40px] w-[130px] px-[12px] rounded-[8px] text-[16px] font-semibold ${isEmailValid && !emailCodeVerified ? "bg-[#F1F3F5] text-neutral-700" : "bg-gray-50 text-input-placeholder-text"}`}
onClick={sendEmailCode}
>
{emailCodeSent && !emailCodeVerified ? "인증번호 재전송" : "인증번호 전송"}
</button>
</div>
{errors.email && <p className="text-error text-[13px] leading-tight">{errors.email}</p>}
@@ -337,7 +357,7 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
<button
type="button"
disabled={emailCodeVerified}
className={`h-[40px] px-[12px] rounded-[8px] text-[16px] font-semibold ${!emailCodeVerified ? "bg-active-button text-white" : "bg-gray-50 text-input-placeholder-text"}`}
className={`h-[40px] w-[130px] px-[12px] rounded-[8px] text-[16px] font-semibold ${!emailCodeVerified ? "bg-[#F1F3F5] text-neutral-700" : "bg-gray-50 text-input-placeholder-text"}`}
onClick={verifyEmailCode}
>
{emailCodeVerified ? "인증완료" : "인증하기"}
@@ -429,8 +449,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
onChange={() => setGender("MALE")}
className="sr-only"
/>
<span className={`inline-block rounded-full size-[18px] border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "MALE" && <span className="block size-[9px] rounded-full bg-active-button m-[4.5px]" />}
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "MALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "MALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
</span>
</label>
@@ -443,8 +463,8 @@ export default function RegisterForm({ onOpenDone, onOpenCodeError }: RegisterFo
onChange={() => setGender("FEMALE")}
className="sr-only"
/>
<span className={`inline-block rounded-full size-[18px] border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "FEMALE" && <span className="block size-[9px] rounded-full bg-active-button m-[4.5px]" />}
<span className={`flex items-center justify-center rounded-full size-[18px] border ${gender === "FEMALE" ? "border-active-button" : "border-[#8c95a1]"}`}>
{gender === "FEMALE" && <span className="block size-[9px] rounded-full bg-active-button" />}
</span>
</label>