공지사항, csv 표출 작업1

This commit is contained in:
2025-11-29 13:41:13 +09:00
parent 39d21a475b
commit 91d14fdabf
5 changed files with 960 additions and 91 deletions

View File

@@ -1,21 +1,90 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent } from "react";
import { useState, useMemo, useRef, ChangeEvent, useEffect } from "react";
import { useRouter } from "next/navigation";
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";
import { type Notice } from "@/app/admin/notices/mockData";
import apiService from "@/app/lib/apiService";
import NoticeValidationModal from "@/app/admin/notices/NoticeValidationModal";
export default function AdminNoticesPage() {
const [notices, setNotices] = useState<Notice[]>(MOCK_NOTICES);
const router = useRouter();
const [notices, setNotices] = useState<Notice[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isWritingMode, setIsWritingMode] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [attachedFile, setAttachedFile] = useState<File | null>(null);
const [fileKey, setFileKey] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(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;
}
};
// API에서 공지사항 목록 가져오기
useEffect(() => {
async function fetchNotices() {
try {
setIsLoading(true);
const response = await apiService.getNotices();
const data = response.data;
// API 응답이 배열이 아닌 경우 처리 (예: { items: [...] } 형태)
let noticesArray: any[] = [];
if (Array.isArray(data)) {
noticesArray = data;
} else if (data && typeof data === 'object') {
noticesArray = data.items || data.notices || data.data || data.list || [];
}
// API 응답 데이터를 Notice 형식으로 변환
const transformedNotices: Notice[] = noticesArray.map((notice: any) => ({
id: notice.id || notice.noticeId || 0,
title: notice.title || '',
date: 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,
}));
setNotices(transformedNotices);
} catch (error) {
console.error('공지사항 목록 조회 오류:', error);
// 에러 발생 시 빈 배열로 설정
setNotices([]);
} finally {
setIsLoading(false);
}
}
fetchNotices();
}, []);
const totalCount = useMemo(() => notices.length, [notices]);
const characterCount = useMemo(() => content.length, [content]);
@@ -25,6 +94,11 @@ export default function AdminNoticesPage() {
setTitle('');
setContent('');
setAttachedFile(null);
setFileKey(null);
// 파일 입력 초기화
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleFileAttach = () => {
@@ -39,43 +113,116 @@ export default function AdminNoticesPage() {
return;
}
try {
setIsLoading(true);
// 단일 파일 업로드
await apiService.uploadFile(file);
setAttachedFile(file);
const uploadResponse = await apiService.uploadFile(file);
// 응답에서 fileKey 추출
let extractedFileKey: string | null = null;
if (uploadResponse.data?.fileKey) {
extractedFileKey = uploadResponse.data.fileKey;
} else if (uploadResponse.data?.key) {
extractedFileKey = uploadResponse.data.key;
} else if (uploadResponse.data?.id) {
extractedFileKey = uploadResponse.data.id;
} else if (uploadResponse.data?.imageKey) {
extractedFileKey = uploadResponse.data.imageKey;
} else if (uploadResponse.data?.fileId) {
extractedFileKey = uploadResponse.data.fileId;
} else if (uploadResponse.data?.data && (uploadResponse.data.data.key || uploadResponse.data.data.fileKey)) {
extractedFileKey = uploadResponse.data.data.key || uploadResponse.data.data.fileKey;
} else if (uploadResponse.data?.results && Array.isArray(uploadResponse.data.results) && uploadResponse.data.results.length > 0) {
const result = uploadResponse.data.results[0];
if (result.ok && result.fileKey) {
extractedFileKey = result.fileKey;
}
}
if (extractedFileKey) {
setFileKey(extractedFileKey);
setAttachedFile(file);
} else {
throw new Error('파일 키를 받아오지 못했습니다.');
}
} catch (error) {
console.error('파일 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
setAttachedFile(null);
setFileKey(null);
} finally {
setIsLoading(false);
// 파일 입력 초기화
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
}
};
const handleSave = () => {
if (!title.trim()) {
alert('제목을 입력해주세요.');
return;
}
if (!content.trim()) {
alert('내용을 입력해주세요.');
const handleSave = async () => {
if (!title.trim() || !content.trim()) {
setIsValidationModalOpen(true);
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();
try {
setIsLoading(true);
// 공지사항 생성 API 호출
const noticeData: any = {
title: title.trim(),
content: content.trim(),
};
// fileKey와 파일 정보가 있으면 attachments 배열로 포함
if (fileKey && attachedFile) {
noticeData.attachments = [
{
fileKey: fileKey,
filename: attachedFile.name,
mimeType: attachedFile.type || 'application/octet-stream',
size: attachedFile.size,
},
];
}
const response = await apiService.createNotice(noticeData);
// API 응답 후 목록 새로고침
const fetchResponse = await apiService.getNotices();
const data = fetchResponse.data;
// API 응답이 배열이 아닌 경우 처리
let noticesArray: any[] = [];
if (Array.isArray(data)) {
noticesArray = data;
} else if (data && typeof data === 'object') {
noticesArray = data.items || data.notices || data.data || data.list || [];
}
// API 응답 데이터를 Notice 형식으로 변환
const transformedNotices: Notice[] = noticesArray.map((notice: any) => ({
id: notice.id || notice.noticeId || 0,
title: notice.title || '',
date: 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 || !!notice.fileKey || false,
}));
setNotices(transformedNotices);
handleBack();
} catch (error) {
console.error('공지사항 저장 실패:', error);
alert('공지사항 저장에 실패했습니다. 다시 시도해주세요.');
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
if (title.trim() || content.trim() || attachedFile) {
if (title.trim() || content.trim() || attachedFile || fileKey) {
if (confirm('작성 중인 내용이 있습니다. 정말 취소하시겠습니까?')) {
handleBack();
}
@@ -100,9 +247,14 @@ export default function AdminNoticesPage() {
}, [sortedNotices, currentPage]);
return (
<div className="min-h-screen flex flex-col bg-white">
{/* 메인 레이아웃 */}
<div className="flex flex-1 min-h-0 justify-center">
<>
<NoticeValidationModal
open={isValidationModalOpen}
onClose={() => setIsValidationModalOpen(false)}
/>
<div className="min-h-screen flex flex-col bg-white">
{/* 메인 레이아웃 */}
<div className="flex flex-1 min-h-0 justify-center">
<div className="w-[1440px] flex min-h-0">
{/* 사이드바 */}
<div className="flex">
@@ -190,10 +342,11 @@ export default function AdminNoticesPage() {
<button
type="button"
onClick={handleFileAttach}
className="h-[32px] w-[62px] px-[4px] py-[3px] rounded-[6px] border border-[#8c95a1] bg-white flex items-center justify-center cursor-pointer hover:bg-gray-50 transition-colors shrink-0"
disabled={isLoading}
className="h-[32px] w-[62px] px-[4px] py-[3px] rounded-[6px] border border-[#8c95a1] bg-white flex items-center justify-center cursor-pointer hover:bg-gray-50 transition-colors shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="text-[13px] font-medium leading-[1.4] text-[#4c5561] whitespace-nowrap">
{isLoading ? '업로드 중...' : '첨부'}
</span>
</button>
<input
@@ -230,9 +383,10 @@ export default function AdminNoticesPage() {
<button
type="button"
onClick={handleSave}
className="h-12 px-4 rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white whitespace-nowrap hover:bg-[#1a2478] transition-colors cursor-pointer"
disabled={isLoading}
className="h-12 px-4 rounded-[10px] bg-[#1f2b91] text-[16px] font-semibold leading-[1.5] text-white whitespace-nowrap hover:bg-[#1a2478] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '저장 중...' : '저장하기'}
</button>
</div>
</div>
@@ -264,7 +418,7 @@ export default function AdminNoticesPage() {
<div className="flex-1 pt-2 flex flex-col">
{notices.length === 0 ? (
<div className="rounded-lg border border-[#dee1e6] bg-white min-h-[400px] flex items-center justify-center">
<p className="text-[16px] font-medium leading-[1.5] text-[#333c47]">
<p className="text-[16px] font-medium leading-[1.5] text-[#333c47] text-center">
.
<br />
.
@@ -298,7 +452,8 @@ export default function AdminNoticesPage() {
return (
<tr
key={notice.id}
className="h-12 hover:bg-[#F5F7FF] transition-colors"
onClick={() => router.push(`/admin/notices/${notice.id}`)}
className="h-12 hover:bg-[#F5F7FF] transition-colors cursor-pointer"
>
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap text-center">
{noticeNumber}
@@ -307,7 +462,7 @@ export default function AdminNoticesPage() {
{notice.title}
</td>
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
{notice.date}
{formatDate(notice.date)}
</td>
<td className="border-t border-r border-[#dee1e6] px-4 text-[13px] leading-[1.5] text-[#1b2027] whitespace-nowrap">
{notice.views.toLocaleString()}
@@ -416,6 +571,7 @@ export default function AdminNoticesPage() {
</div>
</div>
</div>
</>
);
}