@@ -26,74 +127,162 @@ export default function NoticesPage() {
{/* 총 건수 */}
- 총 {rows.length}건
+ 총 {totalCount}건
- {/* 표 */}
-
- {/* 헤더 */}
-
-
- 번호
-
-
- 제목
-
-
- 게시일
-
-
- 조회수
-
-
작성자
+ {loading ? (
+
+ ) : (
+ <>
+ {/* 표 */}
+
+ {/* 헤더 */}
+
+
+ 번호
+
+
+ 제목
+
+
+ 게시일
+
+
+ 조회수
+
+
작성자
+
- {/* 바디 */}
-
- {rows.map((r, index) => {
- // 번호는 정렬된 목록에서의 순서
- const noticeNumber = rows.length - index;
- return (
-
router.push(`/notices/${r.id}`)}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- router.push(`/notices/${r.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',
- ].join(' ')}
- >
-
- {noticeNumber}
+ {/* 바디 */}
+
+ {pagedNotices.length === 0 ? (
+
-
- {r.title}
- {r.hasAttachment && (
-
- )}
+ ) : (
+ pagedNotices.map((notice, index) => {
+ // 번호는 전체 목록에서의 순서 (최신이 1번)
+ const noticeNumber = totalCount - ((currentPage - 1) * ITEMS_PER_PAGE + index);
+ return (
+
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"
+ >
+
+ {noticeNumber}
+
+
+ {notice.title}
+ {notice.hasAttachment && (
+
+ )}
+
+
+ {notice.date}
+
+
+ {notice.views.toLocaleString()}
+
+
{notice.writer}
+
+ );
+ })
+ )}
+
+
+
+ {/* 페이지네이션 - 10개 초과일 때만 표시 */}
+ {totalCount > ITEMS_PER_PAGE && (
+
+
+ {/* First (맨 앞으로) */}
+
+
+ {/* Prev */}
+
+
+ {/* Numbers */}
+ {visiblePages.map((n) => {
+ const active = n === currentPage;
+ return (
+
+ );
+ })}
+
+ {/* Next */}
+
+
+ {/* Last (맨 뒤로) */}
+
-
- {r.date}
-
-
- {r.views.toLocaleString()}
-
-
{r.writer}
-
- );
- })}
-
-
+
+ )}
+ >
+ )}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 22eea48..b444c81 100644
--- a/src/app/page.tsx
+++ b/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
(null);
const [currentIndex, setCurrentIndex] = useState(0);
const [userName, setUserName] = useState('');
-
- // 코스, 공지사항 더미 데이터
- 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([]);
+ const [courseCards, setCourseCards] = useState([]);
+ const [loadingSubjects, setLoadingSubjects] = useState(true);
+ const [totalSubjectsCount, setTotalSubjectsCount] = useState(0);
+ const [notices, setNotices] = useState([]);
+ 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() {
교육 과정
- 총 28건
+ 총 {totalSubjectsCount}건
-
전체보기
@@ -339,42 +452,47 @@ export default function Home() {
strokeLinejoin="round"
/>
-
+
-
- {courseCards.map((c) => (
-
-
-

{
- const t = e.currentTarget as HTMLImageElement;
- if (!t.src.includes('picsum.photos')) t.src = 'https://picsum.photos/seed/xrlms-fallback-card/1200/800';
- }}
- />
-
-
- {c.id === 'c1' && (
-
- 수강 중
-
- )}
-
- {c.title}
-
-
-
-
{c.meta}
+ {loadingSubjects ? (
+
+ ) : (
+
+ {courseCards.map((c) => (
+
router.push(`/course-list/${c.id}`)}
+ className="flex flex-col gap-4 h-[260px] cursor-pointer"
+ >
+
+

{
+ const t = e.currentTarget as HTMLImageElement;
+ if (!t.src.includes('picsum.photos')) t.src = 'https://picsum.photos/seed/xrlms-fallback-card/1200/800';
+ }}
+ />
-
-
- ))}
-
+
+
+ ))}
+
+ )}
{/* 공지사항 */}
@@ -383,11 +501,11 @@ export default function Home() {
공지사항
- 총 {noticeRows.length}건
+ 총 {notices.length}건
-
전체보기
@@ -401,38 +519,57 @@ export default function Home() {
strokeLinejoin="round"
/>
-
+
-
-
-
번호
-
제목
-
게시일
-
조회수
-
작성자
+ {loadingNotices ? (
+
+ ) : (
+
+
+
번호
+
제목
+
게시일
+
조회수
+
작성자
+
-
- {noticeRows.map((r) => (
-
-
{r.id}
-
- {r.title}
-
-
{r.date}
-
{r.views.toLocaleString()}
-
{r.writer}
-
- ))}
+
+ {notices.map((notice, index) => {
+ // 번호는 정렬된 목록에서의 순서 (최신이 1번)
+ const noticeNumber = notices.length - index;
+ return (
+
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"
+ >
+
{noticeNumber}
+
+ {notice.title}
+
+
{notice.date}
+
{notice.views.toLocaleString()}
+
{notice.writer}
+
+ );
+ })}
+
-
+ )}
diff --git a/src/app/resources/[id]/page.tsx b/src/app/resources/[id]/page.tsx
index 2070c95..784d361 100644
--- a/src/app/resources/[id]/page.tsx
+++ b/src/app/resources/[id]/page.tsx
@@ -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
(null);
+ const [attachments, setAttachments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(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 (
+
+ );
+ }
+
+ if (error || !resource) {
+ return (
+
+
+
+
+
+
+
+
+ 학습 자료 상세
+
+
+
+
+ {error || '학습 자료를 찾을 수 없습니다.'}
+
+
+
+
+
+ );
+ }
+
+ const item = resource;
return (
@@ -119,7 +189,11 @@ export default async function ResourceDetailPage({
{item.writer}
게시일
-
{item.date}
+
+ {item.date.includes('T')
+ ? new Date(item.date).toISOString().split('T')[0]
+ : item.date}
+
조회수
{item.views.toLocaleString()}
@@ -132,21 +206,25 @@ export default async function ResourceDetailPage({
{/* 본문 */}
- {item.content.map((p, idx) => (
-
- {p}
-
- ))}
+ {item.content && item.content.length > 0 ? (
+ item.content.map((p, idx) => (
+
+ {p}
+
+ ))
+ ) : (
+
내용이 없습니다.
+ )}
{/* 첨부 파일 */}
- {item.attachments?.length ? (
+ {attachments.length > 0 && (
))}
- ) : null}
+ )}