292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter, useParams } from 'next/navigation';
|
|
import BackCircleSvg from '../../svgs/backcirclesvg';
|
|
import DownloadIcon from '../../svgs/downloadicon';
|
|
import apiService from '../../lib/apiService';
|
|
import type { Notice } from '../../admin/notices/mockData';
|
|
|
|
type Attachment = {
|
|
name: string;
|
|
size: string;
|
|
url?: string;
|
|
fileKey?: string;
|
|
};
|
|
|
|
export default function NoticeDetailPage() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const [notice, setNotice] = useState<Notice | null>(null);
|
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(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;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function fetchNotice() {
|
|
if (!params?.id) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const noticeId = Array.isArray(params.id) ? params.id[0] : params.id;
|
|
const response = await apiService.getNotice(noticeId);
|
|
const data = response.data;
|
|
|
|
// API 응답 데이터를 Notice 형식으로 변환
|
|
const transformedNotice: Notice = {
|
|
id: data.id || data.noticeId || Number(params.id),
|
|
title: data.title || '',
|
|
date: formatDate(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 (transformedNotice.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 (!transformedNotice.title) {
|
|
setError('공지사항을 찾을 수 없습니다.');
|
|
return;
|
|
}
|
|
|
|
setNotice(transformedNotice);
|
|
} catch (err) {
|
|
console.error('공지사항 조회 오류:', err);
|
|
setError('공지사항을 불러오는 중 오류가 발생했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
fetchNotice();
|
|
}, [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 (error) {
|
|
console.error('파일 다운로드 오류:', error);
|
|
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">
|
|
<p className="text-[16px] font-medium text-[#6C7682]">로딩 중...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !notice) {
|
|
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">
|
|
<p className="text-[16px] font-medium text-[#6C7682]">{error || '공지사항을 찾을 수 없습니다.'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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">
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
aria-label="뒤로 가기"
|
|
className="size-8 rounded-full inline-flex items-center justify-center text-[#8C95A1] hover:bg-black/5 no-underline cursor-pointer"
|
|
>
|
|
<BackCircleSvg width={32} height={32} />
|
|
</button>
|
|
<h1 className="m-0 text-[24px] font-bold leading-normal text-[#1B2027]">
|
|
공지사항 상세
|
|
</h1>
|
|
</div>
|
|
|
|
{/* 카드 */}
|
|
<section className="px-8 pb-8">
|
|
<div className="rounded-[8px] border border-[#DEE1E6] overflow-hidden bg-white">
|
|
{/* 헤더 */}
|
|
<div className="p-8">
|
|
<h2 className="m-0 text-[20px] font-bold leading-normal text-[#333C47]">
|
|
{notice.title}
|
|
</h2>
|
|
<div className="mt-2 flex items-center gap-4 text-[13px] leading-[1.4]">
|
|
<span className="text-[#8C95A1]">작성자</span>
|
|
<span className="text-[#333C47]">{notice.writer}</span>
|
|
<span className="w-px h-4 bg-[#DEE1E6]" />
|
|
<span className="text-[#8C95A1]">게시일</span>
|
|
<span className="text-[#333C47]">{notice.date}</span>
|
|
<span className="w-px h-4 bg-[#DEE1E6]" />
|
|
<span className="text-[#8C95A1]">조회수</span>
|
|
<span className="text-[#333C47]">{notice.views.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 구분선 */}
|
|
<div className="h-px bg-[#DEE1E6] w-full" />
|
|
|
|
{/* 본문 */}
|
|
<div className="p-8 flex flex-col gap-10">
|
|
<div className="text-[15px] leading-normal text-[#333C47]">
|
|
{notice.content && notice.content.length > 0 ? (
|
|
notice.content.map((p, idx) => (
|
|
<p key={idx} className="m-0 mb-2 last:mb-0">
|
|
{p}
|
|
</p>
|
|
))
|
|
) : (
|
|
<p className="m-0">내용이 없습니다.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 첨부파일 섹션 */}
|
|
{attachments.length > 0 && (
|
|
<div className="flex flex-col gap-8">
|
|
<div className="text-[15px] font-semibold leading-[1.5] text-[#6C7682]">
|
|
첨부 파일
|
|
</div>
|
|
<div className="pt-3">
|
|
{attachments.map((attachment, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="bg-white border border-[#DEE1E6] rounded-[6px] h-[64px] flex items-center gap-3 px-[17px]"
|
|
>
|
|
<div className="size-6 flex items-center justify-center">
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="text-[#8C95A1]"
|
|
>
|
|
<path
|
|
d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path
|
|
d="M14 2V8H20"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 flex items-center gap-2 min-w-0">
|
|
<p className="text-[15px] font-normal leading-[1.5] text-[#1B2027] truncate">
|
|
{attachment.name}
|
|
</p>
|
|
<p className="text-[13px] font-normal leading-[1.4] text-[#8C95A1] whitespace-nowrap">
|
|
{attachment.size}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDownload(attachment.fileKey, attachment.url, attachment.name)}
|
|
className="bg-white border border-[#8C95A1] rounded-[6px] h-8 px-4 flex items-center justify-center gap-1 hover:bg-[#F9FAFB] transition-colors"
|
|
>
|
|
<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>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|