메일페이지 수정중1

This commit is contained in:
2025-12-01 09:56:04 +09:00
parent 54d763e008
commit 8bdd615ec9
9 changed files with 1696 additions and 660 deletions

View File

@@ -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>