메일페이지 수정중1
This commit is contained in:
@@ -1,92 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import PaperClipSvg from '../../svgs/paperclipsvg';
|
||||
import BackCircleSvg from '../../svgs/backcirclesvg';
|
||||
import DownloadIcon from '../../svgs/downloadicon';
|
||||
import apiService from '../../lib/apiService';
|
||||
import type { Resource } from '../../admin/resources/mockData';
|
||||
|
||||
type ResourceRow = {
|
||||
id: number;
|
||||
title: string;
|
||||
date: string;
|
||||
views: number;
|
||||
writer: string;
|
||||
content: string[];
|
||||
attachments?: Array<{ name: string; size: string; url: string }>;
|
||||
type Attachment = {
|
||||
name: string;
|
||||
size: string;
|
||||
url?: string;
|
||||
fileKey?: string;
|
||||
};
|
||||
|
||||
const DATA: ResourceRow[] = [
|
||||
{
|
||||
id: 6,
|
||||
title: '방사선과 물질의 상호작용 관련 학습 자료',
|
||||
date: '2025-06-28',
|
||||
views: 1230,
|
||||
writer: '강민재',
|
||||
content: [
|
||||
'방사선(Radiation)이 물질 속을 지나갈 때 발생하는 다양한 상호작용(Interaction)의 기본적인 원리를 이해합니다.',
|
||||
'상호작용의 원리는 방사선 측정, 방사선 이용(의료, 산업), 방사선 차폐 등 방사선 관련 분야의 기본이 됨을 인식합니다.',
|
||||
'방사선의 종류(광자, 하전입자, 중성자 등) 및 에너지에 따라 물질과의 상호작용 형태가 어떻게 달라지는지 학습합니다.',
|
||||
],
|
||||
attachments: [
|
||||
{
|
||||
name: '[PPT] 방사선-물질 상호작용의 3가지 유형.pptx',
|
||||
size: '796.35 KB',
|
||||
url: '#',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '감마선과 베타선의 특성 및 차이 분석 자료',
|
||||
date: '2025-06-28',
|
||||
views: 594,
|
||||
writer: '강민재',
|
||||
content: [
|
||||
'감마선과 베타선의 발생 원리, 물질과의 상호작용 차이를 비교합니다.',
|
||||
'차폐 설계 시 고려해야 할 변수들을 사례와 함께 설명합니다.',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '방사선량 단위(Sv, Gy) 비교 및 계산 예제',
|
||||
date: '2025-06-28',
|
||||
views: 1230,
|
||||
writer: '강민재',
|
||||
content: ['방사선량 단위 변환 및 예제 계산을 통해 실무 감각을 익힙니다.'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '의료 영상 촬영 시 방사선 안전 수칙 가이드',
|
||||
date: '2025-06-28',
|
||||
views: 1230,
|
||||
writer: '강민재',
|
||||
content: ['촬영 환경에서의 방사선 안전 수칙을 체크리스트 형태로 정리합니다.'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'X선 발생 원리 및 특성에 대한 이해 자료',
|
||||
date: '2025-06-28',
|
||||
views: 1230,
|
||||
writer: '강민재',
|
||||
content: ['X선의 발생 원리와 에너지 스펙트럼의 특성을 개관합니다.'],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '방사선의 기초 개념과 물질과의 상호작용 정리 자료',
|
||||
date: '2025-06-28',
|
||||
views: 1230,
|
||||
writer: '강민재',
|
||||
content: ['방사선 기초 개념을 한눈에 정리한 입문용 자료입니다.'],
|
||||
},
|
||||
];
|
||||
export default function ResourceDetailPage() {
|
||||
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);
|
||||
|
||||
export default async function ResourceDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const numericId = Number(id);
|
||||
const item = DATA.find((r) => r.id === numericId);
|
||||
if (!item) return notFound();
|
||||
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]);
|
||||
|
||||
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="w-full bg-white">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-[1440px]">
|
||||
<div className="h-[100px] flex items-center justify-center px-8">
|
||||
<p className="text-[16px] font-medium text-[#333c47]">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !resource) {
|
||||
return (
|
||||
<div className="w-full bg-white">
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-[1440px]">
|
||||
<div className="h-[100px] flex items-center gap-3 px-8">
|
||||
<Link
|
||||
href="/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 px-8 pb-8">
|
||||
<p className="text-[16px] font-medium text-[#333c47]">
|
||||
{error || '학습 자료를 찾을 수 없습니다.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const item = resource;
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white">
|
||||
@@ -119,7 +189,11 @@ export default async function ResourceDetailPage({
|
||||
<span className="text-[#333C47]">{item.writer}</span>
|
||||
<span className="w-px h-4 bg-[#DEE1E6]" />
|
||||
<span className="text-[#8C95A1]">게시일</span>
|
||||
<span className="text-[#333C47]">{item.date}</span>
|
||||
<span className="text-[#333C47]">
|
||||
{item.date.includes('T')
|
||||
? new Date(item.date).toISOString().split('T')[0]
|
||||
: item.date}
|
||||
</span>
|
||||
<span className="w-px h-4 bg-[#DEE1E6]" />
|
||||
<span className="text-[#8C95A1]">조회수</span>
|
||||
<span className="text-[#333C47]">{item.views.toLocaleString()}</span>
|
||||
@@ -132,21 +206,25 @@ export default async function ResourceDetailPage({
|
||||
{/* 본문 */}
|
||||
<div className="p-8">
|
||||
<div className="text-[15px] leading-normal text-[#333C47] space-y-2">
|
||||
{item.content.map((p, idx) => (
|
||||
<p key={idx} className="m-0">
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
{item.content && item.content.length > 0 ? (
|
||||
item.content.map((p, idx) => (
|
||||
<p key={idx} className="m-0">
|
||||
{p}
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<p className="m-0 text-[#8C95A1]">내용이 없습니다.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 첨부 파일 */}
|
||||
{item.attachments?.length ? (
|
||||
{attachments.length > 0 && (
|
||||
<div className="p-8 pt-0">
|
||||
<div className="mb-2 text-[15px] font-semibold text-[#6C7682]">
|
||||
첨부 파일
|
||||
</div>
|
||||
{item.attachments.map((f, idx) => (
|
||||
{attachments.map((f, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white border border-[#DEE1E6] h-[64px] rounded-[6px] flex items-center gap-3 px-[17px]"
|
||||
@@ -158,31 +236,24 @@ export default async function ResourceDetailPage({
|
||||
<span className="text-[15px] text-[#1B2027] truncate">
|
||||
{f.name}
|
||||
</span>
|
||||
<span className="text-[13px] text-[#8C95A1] whitespace-nowrap">
|
||||
{f.size}
|
||||
</span>
|
||||
{f.size && (
|
||||
<span className="text-[13px] text-[#8C95A1] whitespace-nowrap">
|
||||
{f.size}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={f.url}
|
||||
className="h-8 px-4 rounded-[6px] border border-[#8C95A1] text-[13px] text-[#4C5561] inline-flex items-center gap-1 hover:bg-[#F9FAFB] no-underline"
|
||||
download
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDownload(f.fileKey, f.url, f.name)}
|
||||
className="h-8 px-4 rounded-[6px] border border-[#8C95A1] text-[13px] text-[#4C5561] inline-flex items-center gap-1 hover:bg-[#F9FAFB] cursor-pointer"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
|
||||
<path
|
||||
d="M12 3v12m0 0l-4-4m4 4l4-4M5 21h14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<DownloadIcon width={16} height={16} className="text-[#4C5561]" />
|
||||
다운로드
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user