메일페이지 수정중1
This commit is contained in:
427
src/app/page.tsx
427
src/app/page.tsx
@@ -3,93 +3,36 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import MainLogoSvg from './svgs/mainlogosvg';
|
||||
import apiService from './lib/apiService';
|
||||
import type { Notice } from './admin/notices/mockData';
|
||||
|
||||
interface Subject {
|
||||
id: string | number;
|
||||
title: string;
|
||||
imageKey?: string;
|
||||
instructor?: string;
|
||||
}
|
||||
|
||||
interface CourseCard {
|
||||
id: string | number;
|
||||
title: string;
|
||||
meta: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [userName, setUserName] = useState<string>('');
|
||||
|
||||
// 코스, 공지사항 더미 데이터
|
||||
const courseCards = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
id: 'c1',
|
||||
title: '원자력 운영 기초',
|
||||
meta: 'VOD • 초급 • 4시간 20분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c1/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
title: '반도체',
|
||||
meta: 'VOD • 중급 • 3시간 10분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c2/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
title: '방사선 안전',
|
||||
meta: 'VOD • 중급 • 4시간 20분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c3/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c4',
|
||||
title: '방사선 폐기물',
|
||||
meta: 'VOD • 중급 • 4시간 20분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c4/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c5',
|
||||
title: '원자력 운전 개론',
|
||||
meta: 'VOD • 초급 • 3시간 00분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c5/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c6',
|
||||
title: '안전 표지와 표준',
|
||||
meta: 'VOD • 초급 • 2시간 40분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c6/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c7',
|
||||
title: '발전소 운영',
|
||||
meta: 'VOD • 중급 • 4시간 20분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c7/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c8',
|
||||
title: '방사선 안전 실습',
|
||||
meta: 'VOD • 중급 • 3시간 30분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c8/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c9',
|
||||
title: '실험실 안전',
|
||||
meta: 'VOD • 초급 • 2시간 10분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c9/1200/800',
|
||||
},
|
||||
{
|
||||
id: 'c10',
|
||||
title: '기초 장비 운용',
|
||||
meta: 'VOD • 초급 • 2시간 50분',
|
||||
image: 'https://picsum.photos/seed/xrlms-c10/1200/800',
|
||||
},
|
||||
] as Array<{ id: string; title: string; meta: string; image: string }>,
|
||||
[]
|
||||
);
|
||||
const noticeRows = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ id: 5, title: '(공지)시스템 개선이 완료되었...', date: '2025-09-10', views: 1320, writer: '운영팀' },
|
||||
{ id: 4, title: '(공지)서버 점검 안내(9/10 새벽)', date: '2025-09-10', views: 1210, writer: '운영팀' },
|
||||
{ id: 3, title: '(공지)서비스 개선 안내', date: '2025-09-10', views: 1230, writer: '운영팀' },
|
||||
{ id: 2, title: '(공지)시장점검 공지', date: '2025-09-10', views: 1320, writer: '관리자' },
|
||||
{ id: 1, title: '뉴: 봉사시간 안내 및 한눈에 보는 현황 정리', date: '2025-08-28', views: 594, writer: '운영팀' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
const [subjects, setSubjects] = useState<Subject[]>([]);
|
||||
const [courseCards, setCourseCards] = useState<CourseCard[]>([]);
|
||||
const [loadingSubjects, setLoadingSubjects] = useState(true);
|
||||
const [totalSubjectsCount, setTotalSubjectsCount] = useState(0);
|
||||
const [notices, setNotices] = useState<Notice[]>([]);
|
||||
const [loadingNotices, setLoadingNotices] = useState(true);
|
||||
|
||||
// NOTE: 실제 이미지 자산 연결 시 해당 src를 교체하세요.
|
||||
const slides = useMemo(
|
||||
@@ -167,6 +110,176 @@ export default function Home() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 과목 리스트 가져오기
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function fetchSubjects() {
|
||||
try {
|
||||
setLoadingSubjects(true);
|
||||
const response = await apiService.getSubjects();
|
||||
|
||||
if (response.status !== 200 || !response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 데이터 구조 확인 및 배열 추출
|
||||
let subjectsData: any[] = [];
|
||||
let totalCount = 0;
|
||||
|
||||
if (Array.isArray(response.data)) {
|
||||
subjectsData = response.data;
|
||||
totalCount = response.data.length;
|
||||
} else if (response.data && typeof response.data === 'object') {
|
||||
// 다양한 응답 구조 처리
|
||||
subjectsData = response.data.items ||
|
||||
response.data.courses ||
|
||||
response.data.data ||
|
||||
response.data.list ||
|
||||
response.data.subjects ||
|
||||
response.data.subjectList ||
|
||||
[];
|
||||
|
||||
// 전체 개수는 total, totalCount, count 등의 필드에서 가져오거나 배열 길이 사용
|
||||
totalCount = response.data.total !== undefined ? response.data.total :
|
||||
response.data.totalCount !== undefined ? response.data.totalCount :
|
||||
response.data.count !== undefined ? response.data.count :
|
||||
subjectsData.length;
|
||||
}
|
||||
|
||||
console.log('과목 리스트 응답:', response.data);
|
||||
console.log('추출된 과목 데이터:', subjectsData);
|
||||
console.log('전체 과목 개수:', totalCount);
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
// 전체 과목 개수 저장
|
||||
setTotalSubjectsCount(totalCount);
|
||||
|
||||
// 상위 10개만 가져오기
|
||||
const top10Subjects = subjectsData.slice(0, 10);
|
||||
setSubjects(top10Subjects);
|
||||
|
||||
// 각 과목의 이미지 다운로드
|
||||
const courseCardsWithImages = await Promise.all(
|
||||
top10Subjects.map(async (subject: Subject) => {
|
||||
let imageUrl = 'https://picsum.photos/seed/xrlms-fallback-card/1200/800';
|
||||
|
||||
if (subject.imageKey) {
|
||||
try {
|
||||
const fileUrl = await apiService.getFile(subject.imageKey);
|
||||
if (fileUrl) {
|
||||
imageUrl = fileUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`이미지 다운로드 실패 (과목 ID: ${subject.id}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: subject.id,
|
||||
title: subject.title || '',
|
||||
meta: subject.instructor ? `강사: ${subject.instructor}` : 'VOD • 온라인',
|
||||
image: imageUrl,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (isMounted) {
|
||||
setCourseCards(courseCardsWithImages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('과목 리스트 조회 오류:', error);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoadingSubjects(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchSubjects();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 공지사항 리스트 가져오기
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function fetchNotices() {
|
||||
try {
|
||||
setLoadingNotices(true);
|
||||
const response = await apiService.getNotices();
|
||||
|
||||
if (response.status !== 200 || !response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
|
||||
let noticesArray: any[] = [];
|
||||
if (Array.isArray(response.data)) {
|
||||
noticesArray = response.data;
|
||||
} else if (response.data && typeof response.data === 'object') {
|
||||
noticesArray = response.data.items || response.data.notices || response.data.data || response.data.list || [];
|
||||
}
|
||||
|
||||
// 날짜를 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 응답 데이터를 Notice 형식으로 변환
|
||||
const transformedNotices: Notice[] = noticesArray.map((notice: any) => ({
|
||||
id: notice.id || notice.noticeId || 0,
|
||||
title: notice.title || '',
|
||||
date: formatDate(notice.date || notice.createdAt || notice.createdDate || new Date().toISOString().split('T')[0]),
|
||||
views: notice.views || notice.viewCount || 0,
|
||||
writer: notice.writer || notice.author || notice.createdBy || '관리자',
|
||||
content: notice.content ? (Array.isArray(notice.content) ? notice.content : [notice.content]) : undefined,
|
||||
hasAttachment: notice.hasAttachment || notice.attachment || false,
|
||||
}));
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
// 날짜 내림차순 정렬 (최신 날짜가 먼저)
|
||||
const sortedNotices = [...transformedNotices].sort((a, b) => b.date.localeCompare(a.date));
|
||||
setNotices(sortedNotices);
|
||||
} catch (error) {
|
||||
console.error('공지사항 리스트 조회 오류:', error);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoadingNotices(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchNotices();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const containerEl = containerRef.current;
|
||||
if (!containerEl) return;
|
||||
@@ -321,11 +434,11 @@ export default function Home() {
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="m-0 text-[24px] font-bold leading-normal text-[#1B2027]">교육 과정</h2>
|
||||
<div className="text-[15px] font-medium leading-normal text-[#333C47]">
|
||||
총 <span className="text-[#384FBF]">28</span>건
|
||||
총 <span className="text-[#384FBF]">{totalSubjectsCount}</span>건
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
<Link
|
||||
href="/course-list"
|
||||
className="flex items-center gap-0.5 text-[14px] font-medium leading-normal text-[#6C7682] no-underline"
|
||||
>
|
||||
전체보기
|
||||
@@ -339,42 +452,47 @@ export default function Home() {
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-5 gap-8">
|
||||
{courseCards.map((c) => (
|
||||
<article key={c.id} className="flex flex-col gap-4 h-[260px]">
|
||||
<div className="h-[166.4px] overflow-hidden rounded-[8px]">
|
||||
<img
|
||||
alt={c.title}
|
||||
src={c.image}
|
||||
className="w-full h-full object-cover block"
|
||||
onError={(e) => {
|
||||
const t = e.currentTarget as HTMLImageElement;
|
||||
if (!t.src.includes('picsum.photos')) t.src = 'https://picsum.photos/seed/xrlms-fallback-card/1200/800';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{c.id === 'c1' && (
|
||||
<span className="inline-flex h-[20px] items-center justify-center px-1 bg-[#E5F5EC] rounded-[4px] text-[13px] font-semibold leading-[1.4] text-[#0C9D61]">
|
||||
수강 중
|
||||
</span>
|
||||
)}
|
||||
<h5 className="m-0 text-[#333C47] font-semibold text-[18px] leading-normal truncate" title={c.title}>
|
||||
{c.title}
|
||||
</h5>
|
||||
<div className="flex items-center gap-1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden className="text-[#8C95A1]">
|
||||
<path d="M8 5v14l11-7z" fill="currentColor" />
|
||||
</svg>
|
||||
<p className="m-0 text-[#8C95A1] text-[13px] font-medium leading-[1.4]">{c.meta}</p>
|
||||
{loadingSubjects ? (
|
||||
<div className="flex items-center justify-center h-[260px]">
|
||||
<p className="text-[16px] font-medium text-[#6C7682]">로딩 중...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-5 gap-8">
|
||||
{courseCards.map((c) => (
|
||||
<article
|
||||
key={c.id}
|
||||
onClick={() => router.push(`/course-list/${c.id}`)}
|
||||
className="flex flex-col gap-4 h-[260px] cursor-pointer"
|
||||
>
|
||||
<div className="h-[166.4px] overflow-hidden rounded-[8px] flex items-center justify-center bg-[#F1F3F5] hover:shadow-lg transition-shadow">
|
||||
<img
|
||||
alt={c.title}
|
||||
src={c.image}
|
||||
className="h-full w-auto object-contain block"
|
||||
onError={(e) => {
|
||||
const t = e.currentTarget as HTMLImageElement;
|
||||
if (!t.src.includes('picsum.photos')) t.src = 'https://picsum.photos/seed/xrlms-fallback-card/1200/800';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h5 className="m-0 text-[#333C47] font-semibold text-[18px] leading-normal truncate" title={c.title}>
|
||||
{c.title}
|
||||
</h5>
|
||||
<div className="flex items-center gap-1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden className="text-[#8C95A1]">
|
||||
<path d="M8 5v14l11-7z" fill="currentColor" />
|
||||
</svg>
|
||||
<p className="m-0 text-[#8C95A1] text-[13px] font-medium leading-[1.4]">{c.meta}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 공지사항 */}
|
||||
@@ -383,11 +501,11 @@ export default function Home() {
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="m-0 text-[24px] font-bold leading-normal text-[#1B2027]">공지사항</h2>
|
||||
<div className="text-[15px] font-medium leading-normal text-[#333C47]">
|
||||
총 <span className="text-[#384FBF]">{noticeRows.length}</span>건
|
||||
총 <span className="text-[#384FBF]">{notices.length}</span>건
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
<Link
|
||||
href="/notices"
|
||||
className="flex items-center gap-0.5 text-[14px] font-medium leading-normal text-[#6C7682] no-underline"
|
||||
>
|
||||
전체보기
|
||||
@@ -401,38 +519,57 @@ export default function Home() {
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[8px] border border-[#DEE1E6] overflow-hidden bg-white">
|
||||
<div className="grid grid-cols-[57px_1fr_140px_140px_140px] bg-[#F9FAFB] h-[48px] text-[14px] font-semibold text-[#4C5561] border-b border-[#DEE1E6]">
|
||||
<div className="flex items-center justify-center px-2 whitespace-nowrap border-r border-[#DEE1E6]">번호</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">제목</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">게시일</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">조회수</div>
|
||||
<div className="flex items-center px-4">작성자</div>
|
||||
{loadingNotices ? (
|
||||
<div className="flex items-center justify-center h-[240px]">
|
||||
<p className="text-[16px] font-medium text-[#6C7682]">로딩 중...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[8px] border border-[#DEE1E6] overflow-hidden bg-white">
|
||||
<div className="grid grid-cols-[57px_1fr_140px_140px_140px] bg-[#F9FAFB] h-[48px] text-[14px] font-semibold text-[#4C5561] border-b border-[#DEE1E6]">
|
||||
<div className="flex items-center justify-center px-2 whitespace-nowrap border-r border-[#DEE1E6]">번호</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">제목</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">게시일</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">조회수</div>
|
||||
<div className="flex items-center px-4">작성자</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{noticeRows.map((r) => (
|
||||
<div
|
||||
key={r.id}
|
||||
className="grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6]"
|
||||
>
|
||||
<div className="flex items-center justify-center px-2 whitespace-nowrap border-r border-[#DEE1E6]">{r.id}</div>
|
||||
<div
|
||||
className="flex items-center px-4 border-r border-[#DEE1E6] whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
title={r.title}
|
||||
>
|
||||
{r.title}
|
||||
</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">{r.date}</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">{r.views.toLocaleString()}</div>
|
||||
<div className="flex items-center px-4">{r.writer}</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
{notices.map((notice, index) => {
|
||||
// 번호는 정렬된 목록에서의 순서 (최신이 1번)
|
||||
const noticeNumber = notices.length - index;
|
||||
return (
|
||||
<div
|
||||
key={notice.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/notices/${notice.id}`)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
router.push(`/notices/${notice.id}`);
|
||||
}
|
||||
}}
|
||||
className="grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6] hover:bg-[rgba(236,240,255,0.5)] cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center justify-center px-2 whitespace-nowrap border-r border-[#DEE1E6]">{noticeNumber}</div>
|
||||
<div
|
||||
className="flex items-center px-4 border-r border-[#DEE1E6] whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
title={notice.title}
|
||||
>
|
||||
{notice.title}
|
||||
</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">{notice.date}</div>
|
||||
<div className="flex items-center px-4 border-r border-[#DEE1E6]">{notice.views.toLocaleString()}</div>
|
||||
<div className="flex items-center px-4">{notice.writer}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user