diff --git a/src/app/admin/courses/CourseRegistrationModal.tsx b/src/app/admin/courses/CourseRegistrationModal.tsx
index 4a92e69..6bfc4a6 100644
--- a/src/app/admin/courses/CourseRegistrationModal.tsx
+++ b/src/app/admin/courses/CourseRegistrationModal.tsx
@@ -173,10 +173,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
}
}}
placeholder="교육 과정명을 입력해 주세요."
- className={`h-[40px] px-3 py-2 border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:ring-2 ${
+ className={`h-[40px] px-3 py-2 border rounded-[8px] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none ${
errors.courseName
- ? "border-[#f64c4c] focus:ring-[#f64c4c] focus:border-[#f64c4c]"
- : "border-[#dee1e6] focus:ring-[#1f2b91] focus:border-transparent"
+ ? "border-[#f64c4c] focus:shadow-[inset_0_0_0_1px_#333c47]"
+ : "border-[#dee1e6] focus:shadow-[inset_0_0_0_1px_#333c47]"
}`}
/>
{errors.courseName && (
@@ -193,10 +193,10 @@ export default function CourseRegistrationModal({ open, onClose, onSave, onDelet
-
- 첨부
-
+
@@ -295,7 +483,7 @@ export default function AdminLessonsPage() {
{/* 액션 버튼 */}
-
+
저장하기
@@ -339,20 +528,72 @@ export default function AdminLessonsPage() {
{/* 콘텐츠 영역 */}
- {items.length === 0 ? (
+ {lessons.length === 0 ? (
- 등록된 강좌가 없습니다.
-
- 강좌를 등록해주세요.
+
+ 등록된 강좌가 없습니다.
+
+ 강좌를 등록해주세요.
+
) : (
<>
- {/* TODO: 테이블 또는 리스트를 여기에 추가 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 교육과정명 |
+ 강좌명 |
+ 첨부파일 |
+ 학습 평가 문제 수 |
+ 등록자 |
+ 등록일 |
+
+
+
+ {paginatedLessons.map((lesson) => (
+
+ |
+ {lesson.courseName}
+ |
+
+ {lesson.lessonName}
+ |
+
+ {lesson.attachments}
+ |
+
+ {lesson.questionCount}개
+ |
+
+ {lesson.createdBy}
+ |
+
+ {lesson.createdAt}
+ |
+
+ ))}
+
+
+
+
{/* 페이지네이션 - 10개 초과일 때만 표시 */}
- {items.length > ITEMS_PER_PAGE && (() => {
+ {lessons.length > ITEMS_PER_PAGE && (() => {
// 페이지 번호를 10단위로 표시
const pageGroup = Math.floor((currentPage - 1) / 10);
const startPage = pageGroup * 10 + 1;
diff --git a/src/app/admin/notices/mockData.ts b/src/app/admin/notices/mockData.ts
new file mode 100644
index 0000000..8ef04cf
--- /dev/null
+++ b/src/app/admin/notices/mockData.ts
@@ -0,0 +1,37 @@
+export type Notice = {
+ id: number;
+ title: string;
+ date: string; // 게시일
+ views: number; // 조회수
+ writer: string; // 작성자
+ content?: string[]; // 본문 내용 (상세 페이지용)
+ hasAttachment?: boolean; // 첨부파일 여부
+};
+
+// TODO: 나중에 DB에서 가져오도록 변경
+export const MOCK_NOTICES: Notice[] = [
+ {
+ id: 2,
+ title: '공지사항 제목이 노출돼요',
+ date: '2025-09-10',
+ views: 1230,
+ writer: '문지호',
+ content: [
+ '사이트 이용 관련 주요 변경 사항을 안내드립니다.',
+ '변경되는 내용은 공지일자로부터 즉시 적용됩니다.',
+ ],
+ },
+ {
+ id: 1,
+ title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지',
+ date: '2025-06-28',
+ views: 594,
+ writer: '문지호',
+ hasAttachment: true,
+ content: [
+ '온라인 강의 수강 방법과 필수 확인 사항을 안내드립니다.',
+ '수강 기간 및 출석, 과제 제출 관련 정책을 반드시 확인해 주세요.',
+ ],
+ },
+];
+
diff --git a/src/app/admin/notices/page.tsx b/src/app/admin/notices/page.tsx
index d403c4d..08e7092 100644
--- a/src/app/admin/notices/page.tsx
+++ b/src/app/admin/notices/page.tsx
@@ -1,21 +1,95 @@
'use client';
-import { useState, useMemo } from "react";
+import { useState, useMemo, useRef, ChangeEvent } from "react";
import AdminSidebar from "@/app/components/AdminSidebar";
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
+import BackArrowSvg from "@/app/svgs/backarrow";
+import { MOCK_NOTICES, type Notice } from "@/app/admin/notices/mockData";
export default function AdminNoticesPage() {
- // TODO: 나중에 실제 데이터로 교체
- const items: any[] = [];
+ const [notices, setNotices] = useState
(MOCK_NOTICES);
const [currentPage, setCurrentPage] = useState(1);
+ const [isWritingMode, setIsWritingMode] = useState(false);
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [attachedFile, setAttachedFile] = useState(null);
+ const fileInputRef = useRef(null);
+
+ const totalCount = useMemo(() => notices.length, [notices]);
+
+ const characterCount = useMemo(() => content.length, [content]);
+
+ const handleBack = () => {
+ setIsWritingMode(false);
+ setTitle('');
+ setContent('');
+ setAttachedFile(null);
+ };
+
+ const handleFileAttach = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileChange = (e: ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ if (file.size > 30 * 1024 * 1024) {
+ alert('30MB 미만의 파일만 첨부할 수 있습니다.');
+ return;
+ }
+ setAttachedFile(file);
+ }
+ };
+
+ const handleSave = () => {
+ if (!title.trim()) {
+ alert('제목을 입력해주세요.');
+ return;
+ }
+ if (!content.trim()) {
+ alert('내용을 입력해주세요.');
+ return;
+ }
+
+ // 새 공지사항 추가
+ const newNotice: Notice = {
+ id: notices.length > 0 ? Math.max(...notices.map(n => n.id)) + 1 : 1,
+ title: title.trim(),
+ date: new Date().toISOString().split('T')[0],
+ views: 0,
+ writer: '관리자', // TODO: 실제 작성자 정보 사용
+ content: content.split('\n'),
+ hasAttachment: attachedFile !== null,
+ };
+
+ setNotices([newNotice, ...notices]);
+ handleBack();
+ };
+
+ const handleCancel = () => {
+ if (title.trim() || content.trim() || attachedFile) {
+ if (confirm('작성 중인 내용이 있습니다. 정말 취소하시겠습니까?')) {
+ handleBack();
+ }
+ } else {
+ handleBack();
+ }
+ };
const ITEMS_PER_PAGE = 10;
- const totalPages = Math.ceil(items.length / ITEMS_PER_PAGE);
- const paginatedItems = useMemo(() => {
+ const sortedNotices = useMemo(() => {
+ return [...notices].sort((a, b) => {
+ // 생성일 내림차순 정렬 (최신 날짜가 먼저)
+ return b.date.localeCompare(a.date);
+ });
+ }, [notices]);
+
+ const totalPages = Math.ceil(sortedNotices.length / ITEMS_PER_PAGE);
+ const paginatedNotices = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
- return items.slice(startIndex, endIndex);
- }, [items, currentPage]);
+ return sortedNotices.slice(startIndex, endIndex);
+ }, [sortedNotices, currentPage]);
return (
@@ -30,27 +104,219 @@ export default function AdminNoticesPage() {
{/* 메인 콘텐츠 */}
- {/* 제목 영역 */}
-
-
- 공지사항
-
-
+ {isWritingMode ? (
+ <>
+ {/* 작성 모드 헤더 */}
+
+
+
+
+
+
+ 공지사항 작성
+
+
+
- {/* 콘텐츠 영역 */}
-
- {items.length === 0 ? (
+ {/* 작성 폼 */}
+
+
+ {/* 제목 입력 */}
+
+
+ setTitle(e.target.value)}
+ placeholder="제목을 입력해 주세요."
+ className="w-full h-[40px] px-3 py-2 rounded-[8px] border border-[#dee1e6] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] focus:outline-none focus:ring-2 focus:ring-[#1f2b91] focus:border-transparent"
+ />
+
+
+ {/* 내용 입력 */}
+
+
+ {/* 첨부 파일 */}
+
+
+
+
+
+ 30MB 미만 파일
+
+
+
+
+ 첨부
+
+
+
+
+
+ {attachedFile ? (
+
+ {attachedFile.name}
+
+ ) : (
+
+ 파일을 첨부해주세요.
+
+ )}
+
+
+
+
+ {/* 액션 버튼 */}
+
+
+ 취소
+
+
+ 저장하기
+
+
+
+ >
+ ) : (
+ <>
+ {/* 제목 영역 */}
+
+
+ 공지사항
+
+
+
+ {/* 헤더 영역 (제목과 콘텐츠 사이) */}
+
+
+ 총 {totalCount}건
+
+
setIsWritingMode(true)}
+ className="h-[40px] px-4 rounded-[8px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white whitespace-nowrap hover:bg-[#1a2478] transition-colors cursor-pointer"
+ >
+ 작성하기
+
+
+
+ {/* 콘텐츠 영역 */}
+
+ {notices.length === 0 ? (
- 현재 관리할 수 있는 항목이 없습니다.
+ 등록된 공지사항이 없습니다.
+
+ 공지사항을 등록해주세요.
) : (
<>
- {/* TODO: 테이블 또는 리스트를 여기에 추가 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ | 번호 |
+ 제목 |
+ 게시일 |
+ 조회수 |
+ 작성자 |
+
+
+
+ {paginatedNotices.map((notice, index) => {
+ // 번호는 전체 목록에서의 순서 (정렬된 목록 기준)
+ const noticeNumber = sortedNotices.length - (currentPage - 1) * ITEMS_PER_PAGE - index;
+ return (
+
+ |
+ {noticeNumber}
+ |
+
+ {notice.title}
+ |
+
+ {notice.date}
+ |
+
+ {notice.views.toLocaleString()}
+ |
+
+ {notice.writer}
+ |
+
+ );
+ })}
+
+
+
+
{/* 페이지네이션 - 10개 초과일 때만 표시 */}
- {items.length > ITEMS_PER_PAGE && (() => {
+ {notices.length > ITEMS_PER_PAGE && (() => {
// 페이지 번호를 10단위로 표시
const pageGroup = Math.floor((currentPage - 1) / 10);
const startPage = pageGroup * 10 + 1;
@@ -134,7 +400,9 @@ export default function AdminNoticesPage() {
})()}
>
)}
-
+
+ >
+ )}
diff --git a/src/app/notices/[id]/page.tsx b/src/app/notices/[id]/page.tsx
index 0a28ff6..fcc25bc 100644
--- a/src/app/notices/[id]/page.tsx
+++ b/src/app/notices/[id]/page.tsx
@@ -1,40 +1,7 @@
import Link from 'next/link';
import { notFound } from 'next/navigation';
import BackCircleSvg from '../../svgs/backcirclesvg';
-
-type NoticeItem = {
- id: number;
- title: string;
- date: string;
- views: number;
- writer: string;
- content: string[];
-};
-
-const DATA: NoticeItem[] = [
- {
- id: 2,
- title: '공지사항 제목이 노출돼요',
- date: '2025-09-10',
- views: 1230,
- writer: '문지호',
- content: [
- '사이트 이용 관련 주요 변경 사항을 안내드립니다.',
- '변경되는 내용은 공지일자로부터 즉시 적용됩니다.',
- ],
- },
- {
- id: 1,
- title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지',
- date: '2025-06-28',
- views: 594,
- writer: '문지호',
- content: [
- '온라인 강의 수강 방법과 필수 확인 사항을 안내드립니다.',
- '수강 기간 및 출석, 과제 제출 관련 정책을 반드시 확인해 주세요.',
- ],
- },
-];
+import { MOCK_NOTICES } from '../../admin/notices/mockData';
export default async function NoticeDetailPage({
params,
@@ -43,8 +10,8 @@ export default async function NoticeDetailPage({
}) {
const { id } = await params;
const numericId = Number(id);
- const item = DATA.find((r) => r.id === numericId);
- if (!item) return notFound();
+ const item = MOCK_NOTICES.find((r) => r.id === numericId);
+ if (!item || !item.content) return notFound();
return (
diff --git a/src/app/notices/page.tsx b/src/app/notices/page.tsx
index 95d5d04..671b305 100644
--- a/src/app/notices/page.tsx
+++ b/src/app/notices/page.tsx
@@ -2,36 +2,13 @@
import { useRouter } from 'next/navigation';
import PaperClipSvg from '../svgs/paperclipsvg';
-
-type NoticeRow = {
- id: number;
- title: string;
- date: string;
- views: number;
- writer: string;
- hasAttachment?: boolean;
-};
-
-const rows: NoticeRow[] = [
- {
- id: 2,
- title: '공지사항 제목이 노출돼요',
- date: '2025-09-10',
- views: 1230,
- writer: '문지호',
- },
- {
- id: 1,
- title: '📢 방사선학 온라인 강의 수강 안내 및 필수 공지',
- date: '2025-06-28',
- views: 594,
- writer: '문지호',
- hasAttachment: true,
- },
-];
+import { MOCK_NOTICES } from '../admin/notices/mockData';
export default function NoticesPage() {
const router = useRouter();
+
+ // 날짜 내림차순 정렬 (최신 날짜가 먼저)
+ const rows = [...MOCK_NOTICES].sort((a, b) => b.date.localeCompare(a.date));
return (
@@ -74,25 +51,28 @@ export default function NoticesPage() {
{/* 바디 */}
- {rows.map((r) => (
-
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(' ')}
- >
-
- {r.id}
-
+ {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}
+
- ))}
+
+ );
+ })}
diff --git a/src/app/svgs/closexo.tsx b/src/app/svgs/closexo.tsx
new file mode 100644
index 0000000..cb53fef
--- /dev/null
+++ b/src/app/svgs/closexo.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+
+type CloseXOSvgProps = React.SVGProps
;
+
+export default function CloseXOSvg(props: CloseXOSvgProps) {
+ return (
+
+ );
+}