Files
xrlms/src/app/page.tsx

454 lines
19 KiB
TypeScript
Raw Normal View History

2025-11-18 06:19:26 +09:00
/* eslint-disable @next/next/no-img-element */
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
2025-11-18 06:19:26 +09:00
import MainLogoSvg from './svgs/mainlogosvg';
2025-11-15 23:46:47 +09:00
export default function Home() {
const router = useRouter();
2025-11-18 06:19:26 +09:00
const containerRef = useRef<HTMLDivElement | null>(null);
const [currentIndex, setCurrentIndex] = useState(0);
const [userName, setUserName] = useState<string>('');
2025-11-18 06:19:26 +09:00
// 코스, 공지사항 더미 데이터
const courseCards = useMemo(
() =>
[
{
id: 'c1',
title: '원자력 운영 기초',
meta: 'VOD • 초급 • 4시간 20분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c1/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c2',
title: '반도체',
meta: 'VOD • 중급 • 3시간 10분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c2/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c3',
title: '방사선 안전',
meta: 'VOD • 중급 • 4시간 20분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c3/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c4',
title: '방사선 폐기물',
meta: 'VOD • 중급 • 4시간 20분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c4/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c5',
title: '원자력 운전 개론',
meta: 'VOD • 초급 • 3시간 00분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c5/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c6',
title: '안전 표지와 표준',
meta: 'VOD • 초급 • 2시간 40분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c6/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c7',
title: '발전소 운영',
meta: 'VOD • 중급 • 4시간 20분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c7/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c8',
title: '방사선 안전 실습',
meta: 'VOD • 중급 • 3시간 30분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c8/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c9',
title: '실험실 안전',
meta: 'VOD • 초급 • 2시간 10분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c9/1200/800',
2025-11-18 06:19:26 +09:00
},
{
id: 'c10',
title: '기초 장비 운용',
meta: 'VOD • 초급 • 2시간 50분',
2025-11-18 07:53:09 +09:00
image: 'https://picsum.photos/seed/xrlms-c10/1200/800',
2025-11-18 06:19:26 +09:00
},
] 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: '운영팀' },
],
[]
);
// NOTE: 실제 이미지 자산 연결 시 해당 src를 교체하세요.
const slides = useMemo(
() => [
{
id: 1,
title: '시스템 점검 안내',
description: '11월 10일 새벽 2시~4시 시스템 점검이 진행됩니다.',
2025-11-18 07:53:09 +09:00
imageSrc: 'https://picsum.photos/seed/xrlms-slide1/1600/900',
2025-11-18 06:19:26 +09:00
},
{
id: 2,
title: '신규 과정 오픈',
description: '최신 커리큘럼으로 업스킬링하세요.',
2025-11-18 07:53:09 +09:00
imageSrc: 'https://picsum.photos/seed/xrlms-slide2/1600/900',
2025-11-18 06:19:26 +09:00
},
{
id: 3,
title: '수강 이벤트',
description: '이번 달 수강 혜택을 확인해보세요.',
2025-11-18 07:53:09 +09:00
imageSrc: 'https://picsum.photos/seed/xrlms-slide3/1600/900',
2025-11-18 06:19:26 +09:00
},
],
[]
);
// 사용자 정보 가져오기
useEffect(() => {
let isMounted = true;
async function fetchUserInfo() {
try {
const localStorageToken = localStorage.getItem('token');
const cookieToken = document.cookie
.split('; ')
.find(row => row.startsWith('token='))
?.split('=')[1];
const token = localStorageToken || cookieToken;
if (!token) {
return;
}
const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/me`
: 'https://hrdi.coconutmeet.net/auth/me';
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
return;
}
const data = await response.json();
if (isMounted) {
// 사용자 권한 확인
const userRole = data.role || data.userRole;
if (userRole === 'ADMIN' || userRole === 'admin') {
// admin 권한이면 /admin/id로 리다이렉트
router.push('/admin/id');
return;
}
if (data.name) {
setUserName(data.name);
}
}
} catch (error) {
console.error('사용자 정보 조회 오류:', error);
}
}
fetchUserInfo();
return () => {
isMounted = false;
};
}, []);
2025-11-18 06:19:26 +09:00
useEffect(() => {
const containerEl = containerRef.current;
if (!containerEl) return;
const handleScroll = () => {
const width = containerEl.clientWidth;
const index = Math.round(containerEl.scrollLeft / Math.max(width, 1));
setCurrentIndex(index);
};
containerEl.addEventListener('scroll', handleScroll, { passive: true });
return () => {
containerEl.removeEventListener('scroll', handleScroll);
};
}, []);
const scrollToIndex = (index: number) => {
const containerEl = containerRef.current;
if (!containerEl) return;
const clamped = Math.max(0, Math.min(index, slides.length - 1));
const width = containerEl.clientWidth;
containerEl.scrollTo({ left: clamped * width, behavior: 'smooth' });
};
const handlePrev = () => scrollToIndex(currentIndex - 1);
const handleNext = () => scrollToIndex(currentIndex + 1);
2025-11-15 23:46:47 +09:00
return (
2025-11-18 06:19:26 +09:00
<div className="w-full min-h-screen flex flex-col bg-white">
<main className="flex-1">
{/* 메인 컨테이너 */}
<div className="w-full flex justify-center">
2025-11-18 07:53:09 +09:00
<div className="w-full max-w-[1376px] px-3 py-6">
2025-11-18 06:19:26 +09:00
{/* 배너 + 사이드 */}
2025-11-18 07:53:09 +09:00
<div className="flex gap-8">
2025-11-18 06:19:26 +09:00
{/* 배너 */}
2025-11-18 07:53:09 +09:00
<section className="flex-none w-[944px]" aria-label="홈 상단 배너">
2025-11-18 06:19:26 +09:00
<div className="relative">
<div
ref={containerRef}
2025-11-18 07:53:09 +09:00
className="flex overflow-x-hidden overscroll-none overflow-y-hidden overscroll-y-none snap-x snap-mandatory scroll-smooth rounded-[12px] bg-[#F1F3F5]"
2025-11-18 06:19:26 +09:00
>
{slides.map((slide) => (
2025-11-18 07:53:09 +09:00
<div key={slide.id} className="flex-none w-full h-[510px] relative snap-start overflow-hidden">
<img
alt={slide.title}
src={slide.imageSrc}
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-slide/1600/900';
}}
/>
<div className="absolute left-0 right-0 bottom-0 h-[198px] bg-linear-to-b from-transparent to-black/50" />
<div className="absolute left-12 bottom-8 text-white">
<div className="font-bold text-[32px] tracking-[-0.32px] leading-normal mb-1.5">{slide.title}</div>
<div className="font-bold text-[24px] tracking-[-0.24px] leading-normal opacity-95">
2025-11-18 06:19:26 +09:00
{slide.description}
</div>
</div>
</div>
))}
</div>
{/* 좌/우 내비게이션 버튼 */}
<button
type="button"
onClick={handlePrev}
aria-label="이전 배너"
disabled={currentIndex <= 0}
2025-11-18 07:53:09 +09:00
className="absolute left-4 top-1/2 -translate-y-1/2 size-12 rounded-full bg-white/20 text-white flex items-center justify-center cursor-pointer disabled:opacity-60"
2025-11-18 06:19:26 +09:00
>
2025-11-18 07:53:09 +09:00
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden>
<path d="M15 6l-6 6 6 6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
2025-11-18 06:19:26 +09:00
</button>
<button
type="button"
onClick={handleNext}
aria-label="다음 배너"
disabled={currentIndex >= slides.length - 1}
2025-11-18 07:53:09 +09:00
className="absolute right-4 top-1/2 -translate-y-1/2 size-12 rounded-full bg-white/20 text-white flex items-center justify-center cursor-pointer disabled:opacity-60"
2025-11-18 06:19:26 +09:00
>
2025-11-18 07:53:09 +09:00
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden>
<path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
2025-11-18 06:19:26 +09:00
</button>
{/* 인디케이터 */}
2025-11-18 07:53:09 +09:00
<div className="absolute left-1/2 bottom-3 -translate-x-1/2 flex items-center gap-2" aria-hidden>
2025-11-18 06:19:26 +09:00
{slides.map((_, idx) => (
2025-11-18 07:53:09 +09:00
<span key={idx} className={idx === currentIndex ? 'h-2 w-8 rounded-full bg-white' : 'size-2 rounded-full bg-white/50'} />
2025-11-18 06:19:26 +09:00
))}
</div>
</div>
</section>
{/* 사이드 패널 (피그마 디자인 적용) */}
<aside className="flex-none w-[400px]">
<div className="bg-[#F1F3F5] rounded-[12px] overflow-hidden h-[510px]">
2025-11-18 07:53:09 +09:00
{/* 상단 환영 및 통계 (피그마 사이즈/간격 적용) */}
2025-11-18 06:19:26 +09:00
<div className="px-8 py-8">
<div className="mb-6">
2025-11-18 07:53:09 +09:00
<div className="flex items-center gap-2">
<span className="text-[18px] font-bold leading-normal text-[#333C47]">
{userName ? `${userName}` : '사용자님'}
</span>
2025-11-18 07:53:09 +09:00
<span className="text-[18px] font-bold leading-normal text-[#333C47]">.</span>
2025-11-18 06:19:26 +09:00
</div>
</div>
<div className="flex items-center justify-center gap-6">
{[
{ label: '수강중', value: 0 },
{ label: '수강 완료', value: 0 },
{ label: '문제 제출', value: 0 },
2025-11-18 07:53:09 +09:00
{ label: '수료증', value: 0 },
2025-11-18 06:19:26 +09:00
].map((s) => (
<div key={s.label} className="w-[64px] flex flex-col items-center justify-center gap-2">
<div className="size-16 rounded-full bg-white flex items-center justify-center">
<div className="text-[20px] font-bold text-[#333C47] leading-none">{s.value}</div>
</div>
<div className="text-[15px] font-semibold text-[#333C47] leading-none">{s.label}</div>
</div>
))}
</div>
</div>
2025-11-18 07:53:09 +09:00
{/* 구분선 (좌우 32px 여백, 1px 라인) */}
2025-11-18 06:19:26 +09:00
<div className="h-px bg-[#DEE1E6] mx-8" />
2025-11-18 07:53:09 +09:00
{/* 최근 수강 내역 헤더 + 빈 상태 영역 (피그마 텍스트/간격 일치) */}
<div className="px-8 pt-[12px] pb-[24px]">
2025-11-18 06:19:26 +09:00
<div className="h-[60px] w-full flex items-center justify-between">
<div className="text-[18px] font-bold text-[#1B2027]"> </div>
<a href="#" className="flex items-center gap-0.5 text-[14px] font-medium text-[#6C7682] no-underline">
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
<path d="M8 5l8 7-8 7" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
</div>
2025-11-18 07:53:09 +09:00
<div className="box-border w-full flex items-center justify-center px-[45px] py-[96px]">
<p className="m-0 text-[16px] font-medium text-[#6C7682] text-center"> .</p>
2025-11-18 06:19:26 +09:00
</div>
</div>
</div>
</aside>
</div>
{/* 교육 과정 */}
<section className="mt-8">
2025-11-18 07:53:09 +09:00
<div className="flex items-center justify-between h-[100px] px-0">
<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>
</div>
2025-11-18 06:19:26 +09:00
</div>
2025-11-18 07:53:09 +09:00
<a
href="#"
className="flex items-center gap-0.5 text-[14px] font-medium leading-normal text-[#6C7682] no-underline"
>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
<path
d="M8 5l8 7-8 7"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
2025-11-18 06:19:26 +09:00
</a>
</div>
2025-11-18 07:53:09 +09:00
<div className="grid grid-cols-5 gap-8">
2025-11-18 06:19:26 +09:00
{courseCards.map((c) => (
2025-11-18 07:53:09 +09:00
<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';
}}
/>
2025-11-18 06:19:26 +09:00
</div>
2025-11-18 07:53:09 +09:00
<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}>
2025-11-18 06:19:26 +09:00
{c.title}
2025-11-18 07:53:09 +09:00
</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>
2025-11-18 06:19:26 +09:00
</div>
</div>
</article>
))}
</div>
</section>
{/* 공지사항 */}
<section className="mt-9">
2025-11-18 07:53:09 +09:00
<div className="flex items-center justify-between mb-4">
<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>
</div>
2025-11-18 06:19:26 +09:00
</div>
2025-11-18 07:53:09 +09:00
<a
href="#"
className="flex items-center gap-0.5 text-[14px] font-medium leading-normal text-[#6C7682] no-underline"
>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
<path
d="M8 5l8 7-8 7"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
2025-11-18 06:19:26 +09:00
</a>
</div>
2025-11-18 07:53:09 +09:00
<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>
2025-11-18 06:19:26 +09:00
</div>
<div>
2025-11-18 07:53:09 +09:00
{noticeRows.map((r) => (
2025-11-18 06:19:26 +09:00
<div
key={r.id}
2025-11-18 07:53:09 +09:00
className="grid grid-cols-[57px_1fr_140px_140px_140px] h-[48px] text-[15px] font-medium text-[#1B2027] border-t border-[#DEE1E6]"
2025-11-18 06:19:26 +09:00
>
2025-11-18 07:53:09 +09:00
<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}
>
2025-11-18 06:19:26 +09:00
{r.title}
</div>
2025-11-18 07:53:09 +09:00
<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>
2025-11-18 06:19:26 +09:00
</div>
))}
</div>
</div>
</section>
</div>
</div>
</main>
2025-11-18 07:53:09 +09:00
{/* 전역 Footer는 layout.tsx에서 렌더링됩니다. */}
2025-11-15 23:46:47 +09:00
</div>
);
}