banner page
This commit is contained in:
@@ -1,21 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useRef, ChangeEvent } from "react";
|
||||
import { useState, useMemo, useRef, ChangeEvent, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import AdminSidebar from "@/app/components/AdminSidebar";
|
||||
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
|
||||
import BackArrowSvg from "@/app/svgs/backarrow";
|
||||
import { MOCK_RESOURCES, type Resource } from "@/app/admin/resources/mockData";
|
||||
import { type Resource } from "@/app/admin/resources/mockData";
|
||||
import apiService from "@/app/lib/apiService";
|
||||
|
||||
export default function AdminResourcesPage() {
|
||||
const [resources, setResources] = useState<Resource[]>(MOCK_RESOURCES);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [resources, setResources] = useState<Resource[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isWritingMode, setIsWritingMode] = useState(false);
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [attachedFile, setAttachedFile] = useState<File | null>(null);
|
||||
const [fileKey, setFileKey] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 날짜를 yyyy-mm-dd 형식으로 포맷팅
|
||||
const formatDate = (dateString: string): string => {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
// 이미 yyyy-mm-dd 형식인 경우 그대로 반환
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// API에서 학습 자료 목록 가져오기
|
||||
useEffect(() => {
|
||||
async function fetchResources() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiService.getLibrary();
|
||||
const data = response.data;
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||
let resourcesArray: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
resourcesArray = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
resourcesArray = data.items || data.resources || data.data || data.list || [];
|
||||
}
|
||||
|
||||
// API 응답 데이터를 Resource 형식으로 변환
|
||||
const transformedResources: Resource[] = resourcesArray.map((resource: any) => ({
|
||||
id: resource.id || resource.resourceId || 0,
|
||||
title: resource.title || '',
|
||||
date: resource.date || resource.createdAt || resource.createdDate || new Date().toISOString().split('T')[0],
|
||||
views: resource.views || resource.viewCount || 0,
|
||||
writer: resource.writer || resource.author || resource.createdBy || '관리자',
|
||||
content: resource.content ? (Array.isArray(resource.content) ? resource.content : [resource.content]) : undefined,
|
||||
hasAttachment: resource.hasAttachment || resource.attachment || false,
|
||||
}));
|
||||
|
||||
setResources(transformedResources);
|
||||
} catch (error) {
|
||||
console.error('학습 자료 목록 조회 오류:', error);
|
||||
// 에러 발생 시 빈 배열로 설정
|
||||
setResources([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchResources();
|
||||
}, []);
|
||||
|
||||
// 수정 완료 쿼리 파라미터 확인 및 토스트 표시
|
||||
useEffect(() => {
|
||||
if (searchParams.get('updated') === 'true') {
|
||||
setShowToast(true);
|
||||
// URL에서 쿼리 파라미터 제거
|
||||
router.replace('/admin/resources');
|
||||
// 토스트 자동 닫기
|
||||
const timer = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
const totalCount = useMemo(() => resources.length, [resources]);
|
||||
|
||||
const characterCount = useMemo(() => content.length, [content]);
|
||||
@@ -25,6 +108,11 @@ export default function AdminResourcesPage() {
|
||||
setTitle('');
|
||||
setContent('');
|
||||
setAttachedFile(null);
|
||||
setFileKey(null);
|
||||
// 파일 입력 초기화
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileAttach = () => {
|
||||
@@ -39,43 +127,116 @@ export default function AdminResourcesPage() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// 단일 파일 업로드
|
||||
await apiService.uploadFile(file);
|
||||
setAttachedFile(file);
|
||||
const uploadResponse = await apiService.uploadFile(file);
|
||||
|
||||
// 응답에서 fileKey 추출
|
||||
let extractedFileKey: string | null = null;
|
||||
if (uploadResponse.data?.fileKey) {
|
||||
extractedFileKey = uploadResponse.data.fileKey;
|
||||
} else if (uploadResponse.data?.key) {
|
||||
extractedFileKey = uploadResponse.data.key;
|
||||
} else if (uploadResponse.data?.id) {
|
||||
extractedFileKey = uploadResponse.data.id;
|
||||
} else if (uploadResponse.data?.imageKey) {
|
||||
extractedFileKey = uploadResponse.data.imageKey;
|
||||
} else if (uploadResponse.data?.fileId) {
|
||||
extractedFileKey = uploadResponse.data.fileId;
|
||||
} else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) {
|
||||
extractedFileKey = uploadResponse.data.data.key || uploadResponse.data.data.fileKey;
|
||||
} else if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results) && uploadResponse.data.results.length > 0) {
|
||||
const result = uploadResponse.data.results[0];
|
||||
if (result.ok && result.fileKey) {
|
||||
extractedFileKey = result.fileKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (extractedFileKey) {
|
||||
setFileKey(extractedFileKey);
|
||||
setAttachedFile(file);
|
||||
} else {
|
||||
throw new Error('파일 키를 받아오지 못했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('파일 업로드 실패:', error);
|
||||
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
|
||||
setAttachedFile(null);
|
||||
setFileKey(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// 파일 입력 초기화
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!title.trim()) {
|
||||
alert('제목을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!content.trim()) {
|
||||
alert('내용을 입력해주세요.');
|
||||
const handleSave = async () => {
|
||||
if (!title.trim() || !content.trim()) {
|
||||
alert('제목과 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 새 학습 자료 추가
|
||||
const newResource: Resource = {
|
||||
id: resources.length > 0 ? Math.max(...resources.map(r => r.id)) + 1 : 1,
|
||||
title: title.trim(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
views: 0,
|
||||
writer: '관리자', // TODO: 실제 작성자 정보 사용
|
||||
content: content.split('\n'),
|
||||
hasAttachment: attachedFile !== null,
|
||||
};
|
||||
|
||||
setResources([newResource, ...resources]);
|
||||
handleBack();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 학습 자료 생성 API 호출
|
||||
const resourceData: any = {
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
};
|
||||
|
||||
// fileKey와 파일 정보가 있으면 attachments 배열로 포함
|
||||
if (fileKey && attachedFile) {
|
||||
resourceData.attachments = [
|
||||
{
|
||||
fileKey: fileKey,
|
||||
filename: attachedFile.name,
|
||||
mimeType: attachedFile.type || 'application/octet-stream',
|
||||
size: attachedFile.size,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const response = await apiService.createLibraryItem(resourceData);
|
||||
|
||||
// API 응답 후 목록 새로고침
|
||||
const fetchResponse = await apiService.getLibrary();
|
||||
const data = fetchResponse.data;
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리
|
||||
let resourcesArray: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
resourcesArray = data;
|
||||
} else if (data && typeof data === 'object') {
|
||||
resourcesArray = data.items || data.resources || data.data || data.list || [];
|
||||
}
|
||||
|
||||
// API 응답 데이터를 Resource 형식으로 변환
|
||||
const transformedResources: Resource[] = resourcesArray.map((resource: any) => ({
|
||||
id: resource.id || resource.resourceId || 0,
|
||||
title: resource.title || '',
|
||||
date: resource.date || resource.createdAt || resource.createdDate || new Date().toISOString().split('T')[0],
|
||||
views: resource.views || resource.viewCount || 0,
|
||||
writer: resource.writer || resource.author || resource.createdBy || '관리자',
|
||||
content: resource.content ? (Array.isArray(resource.content) ? resource.content : [resource.content]) : undefined,
|
||||
hasAttachment: resource.hasAttachment || resource.attachment || !!resource.fileKey || false,
|
||||
}));
|
||||
|
||||
setResources(transformedResources);
|
||||
handleBack();
|
||||
} catch (error) {
|
||||
console.error('학습 자료 저장 실패:', error);
|
||||
alert('학습 자료 저장에 실패했습니다. 다시 시도해주세요.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (title.trim() || content.trim() || attachedFile) {
|
||||
if (title.trim() || content.trim() || attachedFile || fileKey) {
|
||||
if (confirm('작성 중인 내용이 있습니다. 정말 취소하시겠습니까?')) {
|
||||
handleBack();
|
||||
}
|
||||
@@ -190,10 +351,11 @@ export default function AdminResourcesPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFileAttach}
|
||||
className="h-[32px] w-[62px] px-[4px] py-[3px] rounded-[6px] border border-[#8c95a1] bg-white flex items-center justify-center cursor-pointer hover:bg-gray-50 transition-colors shrink-0"
|
||||
disabled={isLoading}
|
||||
className="h-[32px] w-[62px] px-[4px] py-[3px] rounded-[6px] border border-[#8c95a1] bg-white flex items-center justify-center cursor-pointer hover:bg-gray-50 transition-colors shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap">
|
||||
첨부
|
||||
{isLoading ? '업로드 중...' : '첨부'}
|
||||
</span>
|
||||
</button>
|
||||
<input
|
||||
@@ -204,13 +366,13 @@ export default function AdminResourcesPage() {
|
||||
accept="*/*"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-16 w-full rounded-[8px] border border-[#dee1e6] bg-gray-50 flex items-center justify-center">
|
||||
<div className="h-16 w-full rounded-[8px] border border-[#dee1e6] bg-gray-50 flex items-center px-4">
|
||||
{attachedFile ? (
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#1b2027]">
|
||||
{attachedFile.name}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center">
|
||||
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center w-full">
|
||||
파일을 첨부해주세요.
|
||||
</p>
|
||||
)}
|
||||
@@ -230,9 +392,10 @@ export default function AdminResourcesPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="h-12 px-4 rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white whitespace-nowrap hover:bg-[#1a2478] transition-colors cursor-pointer"
|
||||
disabled={isLoading}
|
||||
className="h-12 px-4 rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white whitespace-nowrap hover:bg-[#1a2478] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
저장하기
|
||||
{isLoading ? '저장 중...' : '저장하기'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,7 +427,7 @@ export default function AdminResourcesPage() {
|
||||
<div className="flex-1 pt-2 flex flex-col">
|
||||
{resources.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]">
|
||||
<p className="text-[16px] font-medium leading-[1.5] text-[#333c47] text-center">
|
||||
등록된 학습 자료가 없습니다.
|
||||
<br />
|
||||
학습 자료를 등록해주세요.
|
||||
@@ -298,7 +461,8 @@ export default function AdminResourcesPage() {
|
||||
return (
|
||||
<tr
|
||||
key={resource.id}
|
||||
className="h-12 hover:bg-[#F5F7FF] transition-colors"
|
||||
onClick={() => router.push(`/admin/resources/${resource.id}`)}
|
||||
className="h-12 hover:bg-[#F5F7FF] transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap text-center">
|
||||
{resourceNumber}
|
||||
@@ -307,7 +471,7 @@ export default function AdminResourcesPage() {
|
||||
{resource.title}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{resource.date}
|
||||
{formatDate(resource.date)}
|
||||
</td>
|
||||
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
|
||||
{resource.views.toLocaleString()}
|
||||
@@ -415,6 +579,23 @@ export default function AdminResourcesPage() {
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수정 완료 토스트 */}
|
||||
{showToast && (
|
||||
<div className="fixed right-[60px] bottom-[60px] z-50">
|
||||
<div className="bg-white border border-[#dee1e6] rounded-[8px] p-4 min-w-[360px] flex gap-[10px] items-center">
|
||||
<div className="relative shrink-0 w-[16.667px] h-[16.667px]">
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8.5" cy="8.5" r="8.5" fill="#384FBF"/>
|
||||
<path d="M5.5 8.5L7.5 10.5L11.5 6.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[15px] font-medium leading-[1.5] text-[#1b2027] text-nowrap">
|
||||
수정이 완료되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user