Compare commits
2 Commits
91c886298c
...
0963cfdf5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0963cfdf5b | |||
| b1e2f6012c |
@@ -4,7 +4,7 @@ import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import ModalCloseSvg from "@/app/svgs/closexsvg";
|
||||
import DropdownIcon from "@/app/svgs/dropdownicon";
|
||||
import CloseXOSvg from "@/app/svgs/closexo";
|
||||
import { getInstructors, type UserRow } from "@/app/admin/id/mockData";
|
||||
import { type UserRow } from "@/app/admin/id/mockData";
|
||||
import { type Course } from "./mockData";
|
||||
|
||||
type Props = {
|
||||
@@ -24,6 +24,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -32,24 +33,105 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
const [instructors, setInstructors] = useState<UserRow[]>([]);
|
||||
const [isLoadingInstructors, setIsLoadingInstructors] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadInstructors() {
|
||||
// 강사 목록 로드 함수
|
||||
const loadInstructors = async () => {
|
||||
if (isLoadingInstructors) return; // 이미 로딩 중이면 중복 호출 방지
|
||||
|
||||
setIsLoadingInstructors(true);
|
||||
try {
|
||||
const data = await getInstructors();
|
||||
setInstructors(data);
|
||||
const token = localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
// 외부 API 호출
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/users/compact`
|
||||
: 'https://hrdi.coconutmeet.net/admin/users/compact';
|
||||
|
||||
// 쿼리 파라미터 추가: type=ADMIN
|
||||
const apiUrl = new URL(baseUrl);
|
||||
apiUrl.searchParams.set('type', 'ADMIN');
|
||||
|
||||
const response = await fetch(apiUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`강사 목록을 가져오는데 실패했습니다. (${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||
let usersArray: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
usersArray = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
usersArray = data.items || data.users || data.data || data.list || [];
|
||||
}
|
||||
|
||||
// API 응답 데이터를 UserRow 형식으로 변환
|
||||
const transformedUsers: UserRow[] = usersArray.length > 0
|
||||
? usersArray.map((user: any) => {
|
||||
// 가입일을 YYYY-MM-DD 형식으로 변환
|
||||
const formatDate = (dateString: string | null | undefined): string => {
|
||||
if (!dateString) return new Date().toISOString().split('T')[0];
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split('T')[0];
|
||||
} catch {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
};
|
||||
|
||||
// null 값을 명시적으로 처리
|
||||
const getValue = (value: any, fallback: string = '-') => {
|
||||
if (value === null || value === undefined) return fallback;
|
||||
if (typeof value === 'string' && value.trim() === '') return fallback;
|
||||
return String(value);
|
||||
};
|
||||
|
||||
// status가 "ACTIVE"이면 활성화, 아니면 비활성화
|
||||
const accountStatus: 'active' | 'inactive' =
|
||||
user.status === 'ACTIVE' || user.status === 'active' ? 'active' : 'inactive';
|
||||
|
||||
// role 데이터 처리
|
||||
let userRole: 'learner' | 'instructor' | 'admin' = 'learner'; // 기본값
|
||||
if (user.role) {
|
||||
const roleLower = String(user.role).toLowerCase();
|
||||
if (roleLower === 'instructor' || roleLower === '강사') {
|
||||
userRole = 'instructor';
|
||||
} else if (roleLower === 'admin' || roleLower === '관리자') {
|
||||
userRole = 'admin';
|
||||
} else {
|
||||
userRole = 'learner';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(user.id || user.userId || Math.random()),
|
||||
joinDate: formatDate(user.createdAt || user.joinDate || user.join_date),
|
||||
name: getValue(user.name || user.userName, '-'),
|
||||
email: getValue(user.email || user.userEmail, '-'),
|
||||
role: userRole,
|
||||
status: accountStatus,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
setInstructors(transformedUsers);
|
||||
} catch (error) {
|
||||
console.error('강사 목록 로드 오류:', error);
|
||||
setInstructors([]);
|
||||
} finally {
|
||||
setIsLoadingInstructors(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
loadInstructors();
|
||||
}
|
||||
}, [open]);
|
||||
};
|
||||
|
||||
// 선택된 강사 정보
|
||||
const selectedInstructor = useMemo(() => {
|
||||
@@ -60,11 +142,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
useEffect(() => {
|
||||
if (open && editingCourse) {
|
||||
setCourseName(editingCourse.courseName);
|
||||
// 강사명으로 instructorId 찾기
|
||||
const instructor = instructors.find(inst => inst.name === editingCourse.instructorName);
|
||||
if (instructor) {
|
||||
setInstructorId(instructor.id);
|
||||
}
|
||||
// 수정 모드일 때 강사 목록 자동 로드
|
||||
loadInstructors();
|
||||
} else if (!open) {
|
||||
setCourseName("");
|
||||
setInstructorId("");
|
||||
@@ -74,6 +153,17 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
setSelectedImage(null);
|
||||
setPreviewUrl(null);
|
||||
setIsDragging(false);
|
||||
setInstructors([]); // 모달 닫을 때 강사 목록 초기화
|
||||
}
|
||||
}, [open, editingCourse]);
|
||||
|
||||
// instructors가 로드된 후 editingCourse의 강사 찾기
|
||||
useEffect(() => {
|
||||
if (open && editingCourse && instructors.length > 0) {
|
||||
const instructor = instructors.find(inst => inst.name === editingCourse.instructorName);
|
||||
if (instructor) {
|
||||
setInstructorId(instructor.id);
|
||||
}
|
||||
}
|
||||
}, [open, editingCourse, instructors]);
|
||||
|
||||
@@ -103,7 +193,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 핸들러
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
const nextErrors: Record<string, string> = {};
|
||||
|
||||
if (!courseName.trim()) {
|
||||
@@ -119,9 +209,185 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSaving) return; // 이미 저장 중이면 중복 호출 방지
|
||||
|
||||
// selectedInstructor가 없으면 종료
|
||||
if (!selectedInstructor) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next.submit;
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
// 토큰 가져오기
|
||||
const token = localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1];
|
||||
|
||||
// API base URL 설정
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
: 'https://hrdi.coconutmeet.net';
|
||||
|
||||
let imageKey: string | null = null;
|
||||
|
||||
// 이미지가 있으면 먼저 업로드하여 imageKey 받기
|
||||
if (selectedImage) {
|
||||
try {
|
||||
// 이미지 업로드 API 호출 - 일반적인 엔드포인트 경로 시도
|
||||
const possibleEndpoints = [
|
||||
`${baseUrl}/admin/files/upload`,
|
||||
`${baseUrl}/admin/files`,
|
||||
`${baseUrl}/admin/upload`,
|
||||
`${baseUrl}/admin/images/upload`,
|
||||
`${baseUrl}/admin/images`,
|
||||
`${baseUrl}/files/upload`,
|
||||
`${baseUrl}/files`,
|
||||
`${baseUrl}/upload`,
|
||||
`${baseUrl}/api/files/upload`,
|
||||
`${baseUrl}/api/upload`,
|
||||
`${baseUrl}/api/files`,
|
||||
];
|
||||
|
||||
let uploadResponse: Response | null = null;
|
||||
let lastError: Error | null = null;
|
||||
let uploadSuccess = false;
|
||||
let lastStatusCode: number | null = null;
|
||||
|
||||
// 여러 엔드포인트를 시도
|
||||
for (const uploadUrl of possibleEndpoints) {
|
||||
try {
|
||||
// 각 시도마다 새로운 FormData 생성 (FormData는 한 번만 사용 가능)
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedImage);
|
||||
// 일부 API는 'image' 필드명을 사용할 수 있음
|
||||
formData.append('image', selectedImage);
|
||||
|
||||
uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
// Content-Type은 FormData 사용 시 자동으로 설정되므로 명시하지 않음
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
lastStatusCode = uploadResponse.status;
|
||||
|
||||
if (uploadResponse.ok) {
|
||||
const uploadData = await uploadResponse.json();
|
||||
// 응답에서 imageKey 추출 (실제 응답 구조에 맞게 수정 필요)
|
||||
imageKey = uploadData.imageKey || uploadData.key || uploadData.id || uploadData.fileKey || uploadData.fileId || uploadData.data?.key || uploadData.data?.imageKey || null;
|
||||
uploadSuccess = true;
|
||||
break; // 성공하면 루프 종료
|
||||
} else if (uploadResponse.status !== 404) {
|
||||
// 404가 아닌 다른 에러면 해당 엔드포인트가 맞을 수 있으므로 에러 정보 저장
|
||||
try {
|
||||
const errorData = await uploadResponse.json();
|
||||
lastError = new Error(
|
||||
errorData.message || errorData.error || `이미지 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`
|
||||
);
|
||||
} catch {
|
||||
lastError = new Error(`이미지 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`);
|
||||
}
|
||||
// 401, 403 같은 인증/권한 오류는 더 이상 시도하지 않음
|
||||
if (uploadResponse.status === 401 || uploadResponse.status === 403) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (fetchError) {
|
||||
// 네트워크 에러 등은 무시하고 다음 엔드포인트 시도
|
||||
if (!lastError) {
|
||||
lastError = fetchError instanceof Error ? fetchError : new Error('네트워크 오류가 발생했습니다.');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!uploadSuccess) {
|
||||
// 모든 엔드포인트가 404를 반환한 경우와 다른 오류를 구분
|
||||
if (lastStatusCode === 404 || !lastStatusCode) {
|
||||
console.warn('이미지 업로드 엔드포인트를 찾을 수 없습니다. 이미지 없이 계속 진행합니다.');
|
||||
} else {
|
||||
const errorMessage = lastError?.message || '이미지 업로드에 실패했습니다.';
|
||||
console.error('이미지 업로드 실패:', errorMessage);
|
||||
}
|
||||
// 이미지 업로드 실패 시 사용자에게 알림 (경고 수준)
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
image: '이미지 업로드에 실패했습니다. 이미지 없이 계속 진행됩니다.',
|
||||
}));
|
||||
// 이미지 업로드 실패해도 계속 진행 (선택사항)
|
||||
}
|
||||
} catch (uploadError) {
|
||||
const errorMessage = uploadError instanceof Error ? uploadError.message : '이미지 업로드 중 오류가 발생했습니다.';
|
||||
console.error('이미지 업로드 오류:', errorMessage);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
image: '이미지 업로드 중 오류가 발생했습니다. 이미지 없이 계속 진행됩니다.',
|
||||
}));
|
||||
// 이미지 업로드 오류 발생해도 계속 진행 (선택사항)
|
||||
}
|
||||
}
|
||||
|
||||
// /subjects API 호출
|
||||
const requestBody: {
|
||||
title: string;
|
||||
instructor: string;
|
||||
imageKey?: string | null;
|
||||
} = {
|
||||
title: courseName.trim(),
|
||||
instructor: selectedInstructor.name,
|
||||
};
|
||||
|
||||
if (imageKey) {
|
||||
requestBody.imageKey = imageKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/subjects`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// JSON 파싱 실패 시 기본 메시지 사용
|
||||
}
|
||||
console.error('과목 등록 실패:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 성공 시 onSave 콜백 호출
|
||||
if (onSave && selectedInstructor) {
|
||||
onSave(courseName.trim(), selectedInstructor.name);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '네트워크 오류가 발생했습니다.';
|
||||
console.error('과목 등록 오류:', errorMessage);
|
||||
setErrors({ submit: errorMessage });
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제 버튼 클릭 핸들러
|
||||
@@ -253,10 +519,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
className="relative z-10 shadow-xl"
|
||||
onClick={handleModalClick}
|
||||
>
|
||||
<div className="bg-white border border-[#dee1e6] rounded-[12px] w-full min-w-[480px] max-h-[90vh] overflow-y-auto">
|
||||
<div className="bg-white border border-[var(--color-neutral-40)] rounded-[12px] w-full min-w-[480px] max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-[10px] p-6">
|
||||
<h2 className="text-[20px] font-bold leading-[1.5] text-[#333c47]">
|
||||
<h2 className="text-[20px] font-bold leading-normal text-[var(--color-neutral-700)]">
|
||||
{editingCourse ? "교육과정 수정" : "과목 등록"}
|
||||
</h2>
|
||||
<button
|
||||
@@ -274,8 +540,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* 교육 과정명 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] w-[100px]">
|
||||
교육 과정명<span className="text-[#f64c4c]">*</span>
|
||||
<label className="text-[15px] font-semibold leading-normal text-[var(--color-text-label)] w-[100px]">
|
||||
교육 과정명<span className="text-[var(--color-error)]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -291,45 +557,56 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
}
|
||||
}}
|
||||
placeholder="교육 과정명을 입력해 주세요."
|
||||
className={`h-[40px] px-3 py-2 border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none ${
|
||||
className={`h-[40px] px-3 py-2 border rounded-[8px] bg-white text-[16px] font-normal leading-normal text-[var(--color-text-title)] placeholder:text-[var(--color-text-placeholder-alt)] focus:outline-none ${
|
||||
errors.courseName
|
||||
? "border-[#f64c4c] focus:shadow-[inset_0_0_0_1px_#333c47]"
|
||||
: "border-[#dee1e6] focus:shadow-[inset_0_0_0_1px_#333c47]"
|
||||
? "border-[var(--color-error)] focus:shadow-[inset_0_0_0_1px_var(--color-neutral-700)]"
|
||||
: "border-[var(--color-neutral-40)] focus:shadow-[inset_0_0_0_1px_var(--color-neutral-700)]"
|
||||
}`}
|
||||
/>
|
||||
{errors.courseName && (
|
||||
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.courseName}</p>
|
||||
<p className="text-[var(--color-error)] text-[13px] leading-tight">{errors.courseName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 강사 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] w-[100px]">
|
||||
강사<span className="text-[#f64c4c]">*</span>
|
||||
<label className="text-[15px] font-semibold leading-normal text-[var(--color-text-label)] w-[100px]">
|
||||
강사<span className="text-[var(--color-error)]">*</span>
|
||||
</label>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className={`w-full h-[40px] px-3 py-2 border rounded-[8px] bg-white flex items-center justify-between text-left focus:outline-none focus:shadow-[inset_0_0_0_1px_#333c47] cursor-pointer ${
|
||||
onClick={async () => {
|
||||
const willOpen = !isDropdownOpen;
|
||||
setIsDropdownOpen(willOpen);
|
||||
// 드롭다운을 열 때만 API 호출
|
||||
if (willOpen) {
|
||||
await loadInstructors();
|
||||
}
|
||||
}}
|
||||
className={`w-full h-[40px] px-3 py-2 border rounded-[8px] bg-white flex items-center justify-between text-left focus:outline-none focus:shadow-[inset_0_0_0_1px_var(--color-neutral-700)] cursor-pointer ${
|
||||
errors.instructor
|
||||
? "border-[#f64c4c]"
|
||||
: "border-[#dee1e6]"
|
||||
? "border-border-error"
|
||||
: "border-[var(--color-neutral-40)]"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-[16px] font-normal leading-[1.5] flex-1 ${
|
||||
selectedInstructor ? "text-[#1b2027]" : "text-[#6c7682]"
|
||||
className={`text-[16px] font-normal leading-normal flex-1 ${
|
||||
selectedInstructor ? "text-[var(--color-text-title)]" : "text-[var(--color-text-label)]"
|
||||
}`}
|
||||
>
|
||||
{selectedInstructor?.name || "강사를 선택해 주세요."}
|
||||
</span>
|
||||
<DropdownIcon stroke="#8C95A1" className="shrink-0" />
|
||||
<DropdownIcon stroke="var(--color-text-meta)" className="shrink-0" />
|
||||
</button>
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-[#dee1e6] rounded-[8px] shadow-lg z-20 max-h-[200px] overflow-y-auto">
|
||||
{instructors.length === 0 ? (
|
||||
<div className="px-3 py-2 text-[16px] font-normal leading-[1.5] text-[#6c7682] text-center">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white border border-[var(--color-neutral-40)] rounded-[8px] shadow-lg z-20 max-h-[200px] overflow-y-auto">
|
||||
{isLoadingInstructors ? (
|
||||
<div className="px-3 py-2 text-[16px] font-normal leading-normal text-[var(--color-text-label)] text-center">
|
||||
로딩 중...
|
||||
</div>
|
||||
) : instructors.length === 0 ? (
|
||||
<div className="px-3 py-2 text-[16px] font-normal leading-normal text-[var(--color-text-label)] text-center">
|
||||
등록된 강사가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
@@ -348,10 +625,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`w-full px-3 py-2 text-left text-[16px] font-normal leading-[1.5] hover:bg-[#f1f3f5] transition-colors cursor-pointer ${
|
||||
className={`w-full px-3 py-2 text-left text-[16px] font-normal leading-normal hover:bg-[var(--color-bg-gray-light)] transition-colors cursor-pointer ${
|
||||
instructorId === instructor.id
|
||||
? "bg-[#ecf0ff] text-[#1f2b91] font-semibold"
|
||||
: "text-[#1b2027]"
|
||||
? "bg-[var(--color-bg-primary-light)] text-[var(--color-active-button)] font-semibold"
|
||||
: "text-[var(--color-text-title)]"
|
||||
} ${
|
||||
index === 0 ? "rounded-t-[8px]" : ""
|
||||
} ${
|
||||
@@ -366,7 +643,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
)}
|
||||
</div>
|
||||
{errors.instructor && (
|
||||
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.instructor}</p>
|
||||
<p className="text-[var(--color-error)] text-[13px] leading-tight">{errors.instructor}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -374,10 +651,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] whitespace-pre">
|
||||
<label className="text-[15px] font-semibold leading-normal text-[var(--color-text-label)] whitespace-pre">
|
||||
과목 이미지
|
||||
</label>
|
||||
<span className="text-[13px] font-normal leading-[1.4] text-[#8c95a1]">
|
||||
<span className="text-[13px] font-normal leading-[1.4] text-[var(--color-text-meta)]">
|
||||
30MB 미만의 PNG, JPG
|
||||
</span>
|
||||
</div>
|
||||
@@ -399,8 +676,8 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
isDragging
|
||||
? "bg-blue-50 border-blue-300"
|
||||
: previewUrl
|
||||
? "bg-white border-[#dee1e6]"
|
||||
: "bg-gray-50 border-[#dee1e6] hover:bg-gray-100"
|
||||
? "bg-white border-[var(--color-neutral-40)]"
|
||||
: "bg-gray-50 border-[var(--color-neutral-40)] hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{previewUrl ? (
|
||||
@@ -424,7 +701,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-black/0 hover:bg-black/20 transition-colors flex items-center justify-center my-4">
|
||||
<div className="text-center opacity-0 hover:opacity-100 transition-opacity">
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-white">
|
||||
<p className="text-[14px] font-normal leading-normal text-white">
|
||||
클릭하여 이미지 변경
|
||||
</p>
|
||||
</div>
|
||||
@@ -442,7 +719,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
>
|
||||
<path
|
||||
d="M20 13.3333V26.6667M13.3333 20H26.6667"
|
||||
stroke="#8C95A1"
|
||||
stroke="var(--color-text-meta)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -450,7 +727,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] whitespace-pre">
|
||||
<p className="text-[14px] font-normal leading-normal text-[var(--color-text-meta)] whitespace-pre">
|
||||
(클릭하여 이미지 업로드)
|
||||
<br aria-hidden="true" />
|
||||
미첨부 시 기본 이미지가 노출됩니다.
|
||||
@@ -460,12 +737,19 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
)}
|
||||
</div>
|
||||
{errors.image && (
|
||||
<p className="text-[#f64c4c] text-[13px] leading-tight">{errors.image}</p>
|
||||
<p className="text-[var(--color-error)] text-[13px] leading-tight">{errors.image}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 표시 */}
|
||||
{errors.submit && (
|
||||
<div className="px-6 pb-2">
|
||||
<p className="text-[var(--color-error)] text-[13px] leading-tight">{errors.submit}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions Container */}
|
||||
<div className="flex flex-col gap-8 h-[96px] items-center p-6">
|
||||
<div className="flex items-center justify-center gap-3 w-full">
|
||||
@@ -473,7 +757,7 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteClick}
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[#fef2f2] text-[16px] font-semibold leading-[1.5] text-[#f64c4c] w-[136px] hover:bg-[#fae6e6] transition-colors cursor-pointer"
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[#fef2f2] text-[16px] font-semibold leading-normal text-[var(--color-error)] w-[136px] hover:bg-[#fae6e6] transition-colors cursor-pointer"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
@@ -481,16 +765,17 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[#f1f3f5] text-[16px] font-semibold leading-[1.5] text-[#4c5561] w-[136px] hover:bg-[#e5e7eb] transition-colors cursor-pointer"
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[var(--color-bg-gray-light)] text-[16px] font-semibold leading-normal text-[var(--color-basic-text)] w-[136px] hover:bg-[var(--color-bg-gray-hover)] transition-colors cursor-pointer"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white w-[136px] hover:bg-[#1a2478] transition-colors cursor-pointer"
|
||||
disabled={isSaving}
|
||||
className="h-[48px] px-4 rounded-[10px] bg-[var(--color-active-button)] text-[16px] font-semibold leading-normal text-white w-[136px] hover:bg-[var(--color-active-button-hover)] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
저장
|
||||
{isSaving ? '저장 중...' : '저장'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -507,10 +792,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
/>
|
||||
<div className="relative z-10 bg-white rounded-[8px] p-[24px] shadow-[0px_8px_20px_0px_rgba(0,0,0,0.06),0px_24px_60px_0px_rgba(0,0,0,0.12)] flex flex-col gap-[32px] items-end justify-end min-w-[500px]">
|
||||
<div className="flex flex-col gap-[16px] items-start justify-center w-full">
|
||||
<h2 className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">
|
||||
<h2 className="text-[18px] font-semibold leading-normal text-[var(--color-neutral-700)]">
|
||||
교육과정을 삭제하시겠습니까?
|
||||
</h2>
|
||||
<p className="text-[15px] font-normal leading-[1.5] text-[#4c5561]">
|
||||
<p className="text-[15px] font-normal leading-normal text-[var(--color-basic-text)]">
|
||||
삭제된 교육과정은 복구할 수 없습니다.
|
||||
<br />
|
||||
정말 삭제하시겠습니까?
|
||||
@@ -520,14 +805,14 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteCancel}
|
||||
className="h-[40px] px-2 rounded-[8px] bg-[#f1f3f5] text-[16px] font-semibold leading-[1.5] text-[#4c5561] w-[80px] hover:bg-[#e5e7eb] cursor-pointer transition-colors"
|
||||
className="h-[40px] px-2 rounded-[8px] bg-[var(--color-bg-gray-light)] text-[16px] font-semibold leading-normal text-[var(--color-basic-text)] w-[80px] hover:bg-[var(--color-bg-gray-hover)] cursor-pointer transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteConfirm}
|
||||
className="h-[40px] px-4 rounded-[8px] bg-[#fef2f2] text-[16px] font-semibold leading-[1.5] text-[#f64c4c] hover:bg-[#fae6e6] cursor-pointer transition-colors"
|
||||
className="h-[40px] px-4 rounded-[8px] bg-[#fef2f2] text-[16px] font-semibold leading-normal text-[var(--color-error)] hover:bg-[#fae6e6] cursor-pointer transition-colors"
|
||||
>
|
||||
삭제하기
|
||||
</button>
|
||||
|
||||
@@ -7,100 +7,80 @@ export type Course = {
|
||||
hasLessons: boolean; // 강좌포함여부
|
||||
};
|
||||
|
||||
// TODO: 나중에 DB에서 가져오도록 변경
|
||||
export const MOCK_CURRENT_USER = "관리자"; // 현재 로그인한 사용자 이름
|
||||
// 과목 리스트 조회 API
|
||||
export async function getCourses(): Promise<Course[]> {
|
||||
try {
|
||||
const token = typeof window !== 'undefined'
|
||||
? (localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1])
|
||||
: null;
|
||||
|
||||
// TODO: 이 부분도 나중에는 db에서 받아오도록 변경 예정
|
||||
// 임시 데이터 - 기본 강사명 목록 (getInstructors는 async이므로 모듈 레벨에서 사용 불가)
|
||||
const defaultInstructorNames = [
|
||||
"최예준",
|
||||
"정시우",
|
||||
"임건우",
|
||||
"송윤서",
|
||||
"김민수",
|
||||
"정대현",
|
||||
];
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
: 'https://hrdi.coconutmeet.net';
|
||||
|
||||
export const MOCK_COURSES: Course[] = [
|
||||
{
|
||||
id: "1",
|
||||
courseName: "웹 개발 기초",
|
||||
instructorName: defaultInstructorNames[0] || "최예준",
|
||||
createdAt: "2024-01-15",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
courseName: "React 실전 프로젝트",
|
||||
instructorName: defaultInstructorNames[1] || "정시우",
|
||||
createdAt: "2024-02-20",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
courseName: "데이터베이스 설계",
|
||||
instructorName: defaultInstructorNames[2] || "임건우",
|
||||
createdAt: "2024-03-10",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
courseName: "Node.js 백엔드 개발",
|
||||
instructorName: defaultInstructorNames[3] || "송윤서",
|
||||
createdAt: "2024-03-25",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
courseName: "TypeScript 마스터",
|
||||
instructorName: defaultInstructorNames[4] || "김민수",
|
||||
createdAt: "2024-04-05",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
courseName: "UI/UX 디자인 기초",
|
||||
instructorName: defaultInstructorNames[5] || "정대현",
|
||||
createdAt: "2024-04-18",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
courseName: "모바일 앱 개발",
|
||||
instructorName: defaultInstructorNames[0] || "최예준",
|
||||
createdAt: "2024-05-02",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
courseName: "클라우드 인프라",
|
||||
instructorName: defaultInstructorNames[1] || "정시우",
|
||||
createdAt: "2024-05-15",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
courseName: "머신러닝 입문",
|
||||
instructorName: defaultInstructorNames[2] || "임건우",
|
||||
createdAt: "2024-06-01",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
courseName: "DevOps 실무",
|
||||
instructorName: defaultInstructorNames[3] || "송윤서",
|
||||
createdAt: "2024-06-20",
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
hasLessons: false,
|
||||
},
|
||||
];
|
||||
const apiUrl = `${baseUrl}/subjects`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { Authorization: `Bearer ${token}` }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 404 에러는 리소스가 없는 것이므로 빈 배열 반환
|
||||
if (response.status === 404) {
|
||||
console.warn('⚠️ [getCourses] 과목 리스트를 찾을 수 없습니다 (404)');
|
||||
return [];
|
||||
}
|
||||
|
||||
const errorText = await response.text();
|
||||
console.error('❌ [getCourses] API 에러 응답:', errorText);
|
||||
throw new Error(`과목 리스트 조회 실패 (${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 디버깅: API 응답 구조 확인
|
||||
console.log('🔍 [getCourses] API 원본 응답:', data);
|
||||
console.log('🔍 [getCourses] 응답 타입:', Array.isArray(data) ? '배열' : typeof data);
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||
let coursesArray: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
coursesArray = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
// 더 많은 가능한 필드명 확인
|
||||
coursesArray = data.items || data.courses || data.data || data.list || data.subjects || data.subjectList || [];
|
||||
}
|
||||
|
||||
console.log('🔍 [getCourses] 변환 전 배열:', coursesArray);
|
||||
console.log('🔍 [getCourses] 배열 길이:', coursesArray.length);
|
||||
|
||||
// API 응답 데이터를 Course 형식으로 변환
|
||||
const transformedCourses: Course[] = coursesArray.map((item: any) => {
|
||||
const transformed = {
|
||||
id: String(item.id || item.subjectId || item.subject_id || ''),
|
||||
courseName: item.courseName || item.name || item.subjectName || item.subject_name || item.title || '',
|
||||
instructorName: item.instructorName || item.instructor || item.instructor_name || item.teacherName || '',
|
||||
createdAt: item.createdAt || item.createdDate || item.created_date || item.createdAt || '',
|
||||
createdBy: item.createdBy || item.creator || item.created_by || item.creatorName || '',
|
||||
hasLessons: item.hasLessons !== undefined ? item.hasLessons : (item.has_lessons !== undefined ? item.has_lessons : false),
|
||||
};
|
||||
console.log('🔍 [getCourses] 변환된 항목:', transformed);
|
||||
return transformed;
|
||||
});
|
||||
|
||||
console.log('🔍 [getCourses] 최종 변환된 배열:', transformedCourses);
|
||||
console.log('🔍 [getCourses] 최종 배열 길이:', transformedCourses.length);
|
||||
|
||||
return transformedCourses;
|
||||
} catch (error) {
|
||||
console.error('과목 리스트 조회 오류:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import AdminSidebar from "@/app/components/AdminSidebar";
|
||||
import CourseRegistrationModal from "./CourseRegistrationModal";
|
||||
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
|
||||
import { MOCK_COURSES, MOCK_CURRENT_USER, type Course } from "./mockData";
|
||||
import { getCourses, type Course } from "./mockData";
|
||||
|
||||
export default function AdminCoursesPage() {
|
||||
const [courses, setCourses] = useState<Course[]>(MOCK_COURSES);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingCourse, setEditingCourse] = useState<Course | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
// API에서 과목 리스트 가져오기
|
||||
useEffect(() => {
|
||||
async function fetchCourses() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await getCourses();
|
||||
console.log('📋 [AdminCoursesPage] 받은 데이터:', data);
|
||||
console.log('📋 [AdminCoursesPage] 데이터 개수:', data.length);
|
||||
setCourses(data);
|
||||
} catch (error) {
|
||||
console.error('과목 리스트 로드 오류:', error);
|
||||
setCourses([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
const totalCount = useMemo(() => courses.length, [courses]);
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
@@ -30,28 +51,36 @@ export default function AdminCoursesPage() {
|
||||
return sortedCourses.slice(startIndex, endIndex);
|
||||
}, [sortedCourses, currentPage]);
|
||||
|
||||
const handleSaveCourse = (courseName: string, instructorName: string) => {
|
||||
const handleSaveCourse = async (courseName: string, instructorName: string) => {
|
||||
if (editingCourse) {
|
||||
// 수정 모드
|
||||
// 수정 모드 - TODO: API 호출로 변경 필요
|
||||
setCourses(prev => prev.map(course =>
|
||||
course.id === editingCourse.id
|
||||
? { ...course, courseName, instructorName }
|
||||
: course
|
||||
));
|
||||
} else {
|
||||
// 등록 모드
|
||||
// 등록 모드 - TODO: API 호출로 변경 필요
|
||||
const newCourse: Course = {
|
||||
id: String(Date.now()),
|
||||
courseName,
|
||||
instructorName,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
createdBy: '', // API에서 받아오도록 변경 필요
|
||||
hasLessons: false, // 기본값: 미포함
|
||||
};
|
||||
setCourses(prev => [...prev, newCourse]);
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setEditingCourse(null);
|
||||
|
||||
// 저장 후 리스트 새로고침
|
||||
try {
|
||||
const data = await getCourses();
|
||||
setCourses(data);
|
||||
} catch (error) {
|
||||
console.error('과목 리스트 새로고침 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (course: Course) => {
|
||||
@@ -69,14 +98,23 @@ export default function AdminCoursesPage() {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteCourse = () => {
|
||||
const handleDeleteCourse = async () => {
|
||||
if (editingCourse) {
|
||||
// TODO: API 호출로 삭제 처리 필요
|
||||
setCourses(prev => prev.filter(course => course.id !== editingCourse.id));
|
||||
setEditingCourse(null);
|
||||
setShowToast(true);
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 3000);
|
||||
|
||||
// 삭제 후 리스트 새로고침
|
||||
try {
|
||||
const data = await getCourses();
|
||||
setCourses(data);
|
||||
} catch (error) {
|
||||
console.error('과목 리스트 새로고침 오류:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,7 +153,13 @@ export default function AdminCoursesPage() {
|
||||
|
||||
{/* 콘텐츠 영역 */}
|
||||
<div className="flex-1 pt-2 flex flex-col">
|
||||
{courses.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="rounded-lg border border-[#dee1e6] bg-white min-h-[400px] flex items-center justify-center">
|
||||
<p className="text-[16px] font-medium leading-[1.5] text-[#333c47] text-center">
|
||||
로딩 중...
|
||||
</p>
|
||||
</div>
|
||||
) : courses.length === 0 ? (
|
||||
<div className="rounded-lg border border-[#dee1e6] bg-white min-h-[400px] flex items-center justify-center">
|
||||
<p className="text-[16px] font-medium leading-[1.5] text-[#333c47] text-center">
|
||||
등록된 교육과정이 없습니다.
|
||||
@@ -128,18 +172,16 @@ export default function AdminCoursesPage() {
|
||||
<div className="w-full rounded-[8px] border border-[#dee1e6] overflow-visible">
|
||||
<table className="min-w-full border-collapse">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col style={{ width: 140 }} />
|
||||
<col />
|
||||
<col style={{ width: 120 }} />
|
||||
<col style={{ width: '40%' }} />
|
||||
<col style={{ width: '25%' }} />
|
||||
<col style={{ width: '20%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className="h-12 bg-gray-50 text-left">
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">교육과정명</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">강사명</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">생성일</th>
|
||||
<th className="border-r border-[#dee1e6] px-4 text-[14px] font-semibold leading-[1.5] text-[#4c5561]">등록자</th>
|
||||
<th className="px-4 text-center text-[14px] font-semibold leading-[1.5] text-[#4c5561]">강좌포함여부</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -159,9 +201,6 @@ export default function AdminCoursesPage() {
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{course.createdAt}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{course.createdBy}
|
||||
</td>
|
||||
<td className="border-t border-[#dee1e6] px-4 text-left text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{course.hasLessons ? (
|
||||
<div className="inline-flex items-center justify-center h-[20px] px-[4px] rounded-[4px] bg-[#ecf0ff]">
|
||||
|
||||
@@ -22,17 +22,22 @@ export async function getInstructors(): Promise<UserRow[]> {
|
||||
?.split('=')[1])
|
||||
: null;
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/users/compact`
|
||||
: 'https://hrdi.coconutmeet.net/admin/users/compact';
|
||||
|
||||
// 쿼리 파라미터 추가: type=ADMIN, limit=10
|
||||
const apiUrl = new URL(baseUrl);
|
||||
apiUrl.searchParams.set('type', 'ADMIN');
|
||||
apiUrl.searchParams.set('limit', '10');
|
||||
|
||||
console.log('🔍 [getInstructors] API 호출 정보:', {
|
||||
url: apiUrl,
|
||||
url: apiUrl.toString(),
|
||||
hasToken: !!token,
|
||||
tokenLength: token?.length || 0
|
||||
});
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await fetch(apiUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -105,7 +110,7 @@ export async function getInstructors(): Promise<UserRow[]> {
|
||||
|
||||
return transformed;
|
||||
})
|
||||
.filter((user: UserRow) => user.role === 'instructor' && user.status === 'active')
|
||||
.filter((user: UserRow) => user.role === 'admin' && user.status === 'active')
|
||||
: [];
|
||||
|
||||
console.log('✅ [getInstructors] 변환된 강사 데이터:', {
|
||||
|
||||
@@ -51,11 +51,17 @@ export default function AdminIdPage() {
|
||||
?.split('=')[1];
|
||||
|
||||
// 외부 API 호출
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/admin/users/compact`
|
||||
: 'https://hrdi.coconutmeet.net/admin/users/compact';
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
// 쿼리 파라미터 추가: type=STUDENT, limit=10, page=currentPage
|
||||
const apiUrl = new URL(baseUrl);
|
||||
apiUrl.searchParams.set('type', 'STUDENT');
|
||||
apiUrl.searchParams.set('limit', '10');
|
||||
apiUrl.searchParams.set('page', String(currentPage));
|
||||
|
||||
const response = await fetch(apiUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -137,7 +143,7 @@ export default function AdminIdPage() {
|
||||
}
|
||||
|
||||
fetchUsers();
|
||||
}, []);
|
||||
}, [currentPage]);
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
return activeTab === 'all'
|
||||
@@ -414,7 +420,7 @@ export default function AdminIdPage() {
|
||||
</h1>
|
||||
</div>
|
||||
{/* 탭 네비게이션 */}
|
||||
<div>
|
||||
{/* <div>
|
||||
<div className="flex items-center gap-8 border-b border-input-border">
|
||||
{[
|
||||
{ id: 'all' as TabType, label: '전체' },
|
||||
@@ -440,7 +446,7 @@ export default function AdminIdPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 콘텐츠 영역 */}
|
||||
<div className="flex-1 pt-8 flex flex-col">
|
||||
|
||||
@@ -5,7 +5,7 @@ import AdminSidebar from "@/app/components/AdminSidebar";
|
||||
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
|
||||
import DropdownIcon from "@/app/svgs/dropdownicon";
|
||||
import BackArrowSvg from "@/app/svgs/backarrow";
|
||||
import { MOCK_COURSES, MOCK_CURRENT_USER } from "@/app/admin/courses/mockData";
|
||||
import { getCourses, type Course } from "@/app/admin/courses/mockData";
|
||||
import CloseXOSvg from "@/app/svgs/closexo";
|
||||
|
||||
type Lesson = {
|
||||
@@ -24,6 +24,8 @@ export default function AdminLessonsPage() {
|
||||
const [isRegistrationMode, setIsRegistrationMode] = useState(false);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<string>("관리자");
|
||||
|
||||
// 등록 폼 상태
|
||||
const [selectedCourse, setSelectedCourse] = useState<string>("");
|
||||
@@ -35,6 +37,60 @@ export default function AdminLessonsPage() {
|
||||
const [vrContentFiles, setVrContentFiles] = useState<string[]>([]);
|
||||
const [questionFileCount, setQuestionFileCount] = useState(0);
|
||||
|
||||
// 교육과정 목록 가져오기
|
||||
useEffect(() => {
|
||||
async function fetchCourses() {
|
||||
try {
|
||||
const data = await getCourses();
|
||||
setCourses(data);
|
||||
} catch (error) {
|
||||
console.error('교육과정 목록 로드 오류:', error);
|
||||
setCourses([]);
|
||||
}
|
||||
}
|
||||
fetchCourses();
|
||||
}, []);
|
||||
|
||||
// 현재 사용자 정보 가져오기
|
||||
useEffect(() => {
|
||||
async function fetchCurrentUser() {
|
||||
try {
|
||||
const token = typeof window !== 'undefined'
|
||||
? (localStorage.getItem('token') || document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('token='))
|
||||
?.split('=')[1])
|
||||
: null;
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/me`
|
||||
: 'https://hrdi.coconutmeet.net/auth/me';
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.name) {
|
||||
setCurrentUser(data.name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 조회 오류:', error);
|
||||
}
|
||||
}
|
||||
fetchCurrentUser();
|
||||
}, []);
|
||||
|
||||
const totalCount = useMemo(() => lessons.length, [lessons]);
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
@@ -52,13 +108,13 @@ export default function AdminLessonsPage() {
|
||||
return sortedLessons.slice(startIndex, endIndex);
|
||||
}, [sortedLessons, currentPage]);
|
||||
|
||||
// 교육과정 옵션 - mockData에서 가져오기
|
||||
// 교육과정 옵션
|
||||
const courseOptions = useMemo(() =>
|
||||
MOCK_COURSES.map(course => ({
|
||||
courses.map(course => ({
|
||||
id: course.id,
|
||||
name: course.courseName
|
||||
}))
|
||||
, []);
|
||||
, [courses]);
|
||||
|
||||
// 외부 클릭 시 드롭다운 닫기
|
||||
useEffect(() => {
|
||||
@@ -129,7 +185,7 @@ export default function AdminLessonsPage() {
|
||||
lessonName,
|
||||
attachments,
|
||||
questionCount: questionFileCount,
|
||||
createdBy: MOCK_CURRENT_USER,
|
||||
createdBy: currentUser,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import MainLogoSvg from './svgs/mainlogosvg';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [userName, setUserName] = useState<string>('');
|
||||
@@ -149,9 +151,19 @@ export default function Home() {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (isMounted && data.name) {
|
||||
if (isMounted) {
|
||||
// 사용자 권한 확인
|
||||
const userRole = data.role || data.userRole;
|
||||
if (userRole === 'ADMIN' || userRole === 'admin') {
|
||||
// admin 권한이면 /admin/id로 리다이렉트
|
||||
router.push('/admin/id');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.name) {
|
||||
setUserName(data.name);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 조회 오류:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user