banner page
This commit is contained in:
390
src/app/admin/resources/[id]/page.tsx
Normal file
390
src/app/admin/resources/[id]/page.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import AdminSidebar from '@/app/components/AdminSidebar';
|
||||
import BackCircleSvg from '@/app/svgs/backcirclesvg';
|
||||
import DownloadIcon from '@/app/svgs/downloadicon';
|
||||
import PaperClipSvg from '@/app/svgs/paperclipsvg';
|
||||
import apiService from '@/app/lib/apiService';
|
||||
import type { Resource } from '@/app/admin/resources/mockData';
|
||||
import NoticeDeleteModal from '@/app/admin/notices/NoticeDeleteModal';
|
||||
|
||||
type Attachment = {
|
||||
name: string;
|
||||
size: string;
|
||||
url?: string;
|
||||
fileKey?: string;
|
||||
};
|
||||
|
||||
export default function AdminResourceDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [resource, setResource] = useState<Resource | null>(null);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchResource() {
|
||||
if (!params?.id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await apiService.getLibraryItem(params.id);
|
||||
const data = response.data;
|
||||
|
||||
// API 응답 데이터를 Resource 형식으로 변환
|
||||
const transformedResource: Resource = {
|
||||
id: data.id || data.resourceId || Number(params.id),
|
||||
title: data.title || '',
|
||||
date: data.date || data.createdAt || data.createdDate || new Date().toISOString().split('T')[0],
|
||||
views: data.views || data.viewCount || 0,
|
||||
writer: data.writer || data.author || data.createdBy || '관리자',
|
||||
content: data.content
|
||||
? (Array.isArray(data.content)
|
||||
? data.content
|
||||
: typeof data.content === 'string'
|
||||
? data.content.split('\n').filter((line: string) => line.trim())
|
||||
: [String(data.content)])
|
||||
: [],
|
||||
hasAttachment: data.hasAttachment || data.attachment || false,
|
||||
};
|
||||
|
||||
// 첨부파일 정보 처리
|
||||
if (data.attachments && Array.isArray(data.attachments)) {
|
||||
setAttachments(data.attachments.map((att: any) => ({
|
||||
name: att.name || att.fileName || att.filename || '',
|
||||
size: att.size || att.fileSize || '',
|
||||
url: att.url || att.downloadUrl,
|
||||
fileKey: att.fileKey || att.key || att.fileId,
|
||||
})));
|
||||
} else if (transformedResource.hasAttachment && data.attachment) {
|
||||
// 단일 첨부파일인 경우
|
||||
setAttachments([{
|
||||
name: data.attachment.name || data.attachment.fileName || data.attachment.filename || '첨부파일',
|
||||
size: data.attachment.size || data.attachment.fileSize || '',
|
||||
url: data.attachment.url || data.attachment.downloadUrl,
|
||||
fileKey: data.attachment.fileKey || data.attachment.key || data.attachment.fileId,
|
||||
}]);
|
||||
}
|
||||
|
||||
if (!transformedResource.title) {
|
||||
throw new Error('학습 자료를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
setResource(transformedResource);
|
||||
} catch (err) {
|
||||
console.error('학습 자료 조회 오류:', err);
|
||||
setError('학습 자료를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchResource();
|
||||
}, [params?.id]);
|
||||
|
||||
// 토스트 자동 닫기
|
||||
useEffect(() => {
|
||||
if (showToast) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowToast(false);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const handleDownload = async (fileKey?: string, url?: string, fileName?: string) => {
|
||||
if (url) {
|
||||
// URL이 있으면 직접 다운로드
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else if (fileKey) {
|
||||
// fileKey가 있으면 API를 통해 다운로드
|
||||
try {
|
||||
const fileUrl = await apiService.getFile(fileKey);
|
||||
if (fileUrl) {
|
||||
const link = document.createElement('a');
|
||||
link.href = fileUrl;
|
||||
link.download = fileName || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('파일 다운로드 실패:', err);
|
||||
alert('파일 다운로드에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
<div className="flex flex-1 min-h-0 justify-center">
|
||||
<div className="w-[1440px] flex min-h-0">
|
||||
<div className="flex">
|
||||
<AdminSidebar />
|
||||
</div>
|
||||
<main className="w-[1120px] bg-white">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-[100px] flex items-center justify-center px-[32px]">
|
||||
<p className="text-[16px] font-medium text-[#333c47]">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !resource || !resource.content || resource.content.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
<div className="flex flex-1 min-h-0 justify-center">
|
||||
<div className="w-[1440px] flex min-h-0">
|
||||
<div className="flex">
|
||||
<AdminSidebar />
|
||||
</div>
|
||||
<main className="w-[1120px] bg-white">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-[100px] flex items-center gap-[10px] px-[32px]">
|
||||
<Link
|
||||
href="/admin/resources"
|
||||
aria-label="뒤로 가기"
|
||||
className="size-8 rounded-full inline-flex items-center justify-center text-[#8C95A1] hover:bg-black/5 no-underline"
|
||||
>
|
||||
<BackCircleSvg width={32} height={32} />
|
||||
</Link>
|
||||
<h1 className="m-0 text-[24px] font-bold leading-normal text-[#1B2027]">
|
||||
학습 자료 상세
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p className="text-[16px] font-medium text-[#333c47]">
|
||||
{error || '학습 자료를 찾을 수 없습니다.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-white">
|
||||
<div className="flex flex-1 min-h-0 justify-center">
|
||||
<div className="w-[1440px] flex min-h-0">
|
||||
{/* 사이드바 */}
|
||||
<div className="flex">
|
||||
<AdminSidebar />
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 */}
|
||||
<main className="w-[1120px] bg-white">
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 상단 타이틀 */}
|
||||
<div className="h-[100px] flex items-center gap-[10px] px-[32px]">
|
||||
<Link
|
||||
href="/admin/resources"
|
||||
aria-label="뒤로 가기"
|
||||
className="size-8 rounded-full inline-flex items-center justify-center text-[#8C95A1] hover:bg-black/5 no-underline shrink-0"
|
||||
>
|
||||
<BackCircleSvg width={32} height={32} />
|
||||
</Link>
|
||||
<h1 className="m-0 text-[24px] font-bold leading-[1.5] text-[#1B2027]">
|
||||
학습 자료 상세
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* 카드 */}
|
||||
<section className="flex flex-col gap-[40px] px-[32px] py-[24px]">
|
||||
<div className="rounded-[8px] border border-[#DEE1E6] overflow-hidden bg-white">
|
||||
{/* 헤더 */}
|
||||
<div className="p-[32px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-[8px]">
|
||||
<h2 className="m-0 text-[20px] font-bold leading-[1.5] text-[#333C47]">
|
||||
{resource.title}
|
||||
</h2>
|
||||
<div className="flex items-center gap-[16px] text-[13px] font-medium leading-[1.4]">
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<span className="text-[#8C95A1]">작성자</span>
|
||||
<span className="text-[#333C47]">{resource.writer}</span>
|
||||
</div>
|
||||
<div className="h-[16px] w-0 flex items-center justify-center">
|
||||
<div className="h-0 w-[16px] border-t border-[#DEE1E6]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<span className="text-[#8C95A1]">게시일</span>
|
||||
<span className="text-[#333C47]">
|
||||
{resource.date.includes('T')
|
||||
? new Date(resource.date).toISOString().split('T')[0]
|
||||
: resource.date}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-[16px] w-0 flex items-center justify-center">
|
||||
<div className="h-0 w-[16px] border-t border-[#DEE1E6]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px]">
|
||||
<span className="text-[#8C95A1]">조회수</span>
|
||||
<span className="text-[#333C47]">{resource.views.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 구분선 */}
|
||||
<div className="h-px bg-[#DEE1E6] w-full" />
|
||||
|
||||
{/* 본문 및 첨부파일 */}
|
||||
<div className="flex flex-col gap-[40px] p-[32px]">
|
||||
{/* 본문 */}
|
||||
<div className="text-[15px] font-normal leading-[1.5] text-[#333C47]">
|
||||
{resource.content.map((p, idx) => (
|
||||
<p key={idx} className="mb-0 leading-[1.5]">
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 첨부파일 섹션 */}
|
||||
{attachments.length > 0 && (
|
||||
<div className="flex flex-col gap-[24px] w-full">
|
||||
<div className="flex flex-col gap-[8px] w-full">
|
||||
<div className="flex items-center gap-[12px] h-[32px]">
|
||||
<div className="flex items-center">
|
||||
<p className="text-[15px] font-semibold leading-[1.5] text-[#6C7682] m-0">
|
||||
첨부 파일
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{attachments.map((attachment, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white border border-[#DEE1E6] rounded-[6px] h-[64px] flex items-center gap-[12px] px-[17px] py-1 w-full"
|
||||
>
|
||||
<div className="size-[24px] shrink-0">
|
||||
<PaperClipSvg width={24} height={24} className="text-[#333C47]" />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-[8px] min-w-0">
|
||||
<p className="text-[15px] font-normal leading-[1.5] text-[#1B2027] truncate m-0">
|
||||
{attachment.name}
|
||||
</p>
|
||||
<p className="text-[13px] font-normal leading-[1.4] text-[#8C95A1] shrink-0 m-0">
|
||||
{attachment.size}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDownload(attachment.fileKey, attachment.url, attachment.name)}
|
||||
className="bg-white border border-[#8C95A1] rounded-[6px] h-[32px] flex items-center justify-center gap-[4px] px-[16px] py-[3px] shrink-0 hover:bg-[#F9FAFB] cursor-pointer"
|
||||
>
|
||||
<DownloadIcon width={16} height={16} className="text-[#4C5561]" />
|
||||
<span className="text-[13px] font-medium leading-[1.4] text-[#4C5561]">
|
||||
다운로드
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex items-center justify-end gap-[12px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
disabled={isDeleting}
|
||||
className="bg-[#FEF2F2] h-[48px] rounded-[10px] px-[8px] shrink-0 min-w-[80px] flex items-center justify-center hover:bg-[#FEE2E2] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="text-[16px] font-semibold leading-[1.5] text-[#F64C4C] text-center">
|
||||
삭제
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/admin/resources/${params.id}/edit`)}
|
||||
className="bg-[#F1F3F5] h-[48px] rounded-[10px] px-[16px] shrink-0 min-w-[90px] flex items-center justify-center hover:bg-[#E9ECEF] transition-colors"
|
||||
>
|
||||
<span className="text-[16px] font-semibold leading-[1.5] text-[#4C5561] text-center">
|
||||
수정하기
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
<NoticeDeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={async () => {
|
||||
if (!params?.id) {
|
||||
alert('학습 자료 ID를 찾을 수 없습니다.');
|
||||
setIsDeleteModalOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await apiService.deleteLibraryItem(params.id);
|
||||
setIsDeleteModalOpen(false);
|
||||
setShowToast(true);
|
||||
// 토스트 표시 후 목록 페이지로 이동
|
||||
setTimeout(() => {
|
||||
router.push('/admin/resources');
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
console.error('학습 자료 삭제 오류:', err);
|
||||
const errorMessage = err instanceof Error ? err.message : '학습 자료 삭제에 실패했습니다.';
|
||||
alert(errorMessage);
|
||||
setIsDeleteModalOpen(false);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
|
||||
{/* 삭제 완료 토스트 */}
|
||||
{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