공지사항 삭제등록 등11

This commit is contained in:
2025-11-29 15:40:39 +09:00
parent 872a88866e
commit eb7871133d
10 changed files with 1663 additions and 412 deletions

View File

@@ -0,0 +1,65 @@
'use client';
import React from 'react';
type NoticeCancelModalProps = {
open: boolean;
onClose: () => void;
onConfirm: () => void;
};
/**
* 공지사항 작성 취소 확인 모달
*/
export default function NoticeCancelModal({
open,
onClose,
onConfirm,
}: NoticeCancelModalProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center">
<div
className="absolute inset-0 bg-black/40"
onClick={onClose}
aria-hidden="true"
/>
<div className="relative z-10 bg-white rounded-[8px] p-[24px] shadow-[0px_8px_20px_0px_rgba(0,0,0,0.06),0px_24px_60px_0px_rgba(0,0,0,0.12)] flex flex-col gap-[32px] items-end justify-end w-[400px]">
<div className="flex flex-col gap-[16px] items-start justify-center w-full">
<div className="flex gap-[8px] items-start w-full">
<p className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">
.
</p>
</div>
<div className="flex gap-[8px] items-start w-full">
<div className="text-[15px] font-normal leading-[1.5] text-[#4c5561]">
<p className="mb-0"> ?</p>
</div>
</div>
</div>
<div className="flex gap-[8px] items-center justify-end shrink-0">
<button
type="button"
onClick={onClose}
className="bg-[#f1f3f5] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#e9ecef] cursor-pointer transition-colors"
>
<p className="text-[16px] font-semibold leading-[1.5] text-[#4c5561] text-center whitespace-pre">
</p>
</button>
<button
type="button"
onClick={onConfirm}
className="bg-[#1f2b91] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#1a2478] cursor-pointer transition-colors"
>
<p className="text-[16px] font-semibold leading-[1.5] text-white text-center whitespace-pre">
</p>
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,70 @@
'use client';
import React from 'react';
type NoticeDeleteModalProps = {
open: boolean;
onClose: () => void;
onConfirm: () => void;
isDeleting?: boolean;
};
/**
* 공지사항 삭제 확인 모달
*/
export default function NoticeDeleteModal({
open,
onClose,
onConfirm,
isDeleting = false,
}: NoticeDeleteModalProps) {
if (!open) return null;
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center">
<div
className="absolute inset-0 bg-black/40"
onClick={onClose}
aria-hidden="true"
/>
<div className="relative z-10 bg-white rounded-[8px] p-[24px] shadow-[0px_8px_20px_0px_rgba(0,0,0,0.06),0px_24px_60px_0px_rgba(0,0,0,0.12)] flex flex-col gap-[32px] items-end justify-end w-[400px]">
<div className="flex flex-col gap-[16px] items-start justify-center w-full">
<div className="flex gap-[8px] items-start w-full">
<p className="text-[18px] font-semibold leading-[1.5] text-[#333c47]">
?
</p>
</div>
<div className="flex gap-[8px] items-start w-full">
<div className="text-[15px] font-normal leading-[1.5] text-[#4c5561]">
<p className="mb-0"> .</p>
<p> ?</p>
</div>
</div>
</div>
<div className="flex gap-[8px] items-center justify-end shrink-0">
<button
type="button"
onClick={onClose}
disabled={isDeleting}
className="bg-[#f1f3f5] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#e9ecef] cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<p className="text-[16px] font-semibold leading-[1.5] text-[#4c5561] text-center whitespace-pre">
</p>
</button>
<button
type="button"
onClick={onConfirm}
disabled={isDeleting}
className="bg-[#f64c4c] h-[40px] rounded-[8px] px-[8px] flex items-center justify-center shrink-0 w-[80px] hover:bg-[#e63939] cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<p className="text-[16px] font-semibold leading-[1.5] text-white text-center whitespace-pre">
{isDeleting ? '삭제 중...' : '삭제'}
</p>
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,441 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent, useEffect } from "react";
import { useRouter, useParams } from "next/navigation";
import AdminSidebar from "@/app/components/AdminSidebar";
import BackArrowSvg from "@/app/svgs/backarrow";
import CloseXOSvg from "@/app/svgs/closexo";
import apiService from "@/app/lib/apiService";
import NoticeValidationModal from "@/app/admin/notices/NoticeValidationModal";
import NoticeCancelModal from "@/app/admin/notices/NoticeCancelModal";
type Attachment = {
name: string;
size: string;
url?: string;
fileKey?: string;
};
export default function AdminNoticeEditPage() {
const params = useParams();
const router = useRouter();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [attachedFile, setAttachedFile] = useState<File | null>(null);
const [fileKey, setFileKey] = useState<string | null>(null);
const [existingAttachment, setExistingAttachment] = useState<Attachment | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const characterCount = useMemo(() => content.length, [content]);
// 공지사항 데이터 로드
useEffect(() => {
async function fetchNotice() {
if (!params?.id) {
setIsLoadingData(false);
return;
}
try {
setIsLoadingData(true);
const response = await apiService.getNotice(params.id);
const data = response.data;
// 제목 설정
setTitle(data.title || '');
// 내용 설정 (배열이면 join, 문자열이면 그대로)
if (data.content) {
if (Array.isArray(data.content)) {
setContent(data.content.join('\n'));
} else if (typeof data.content === 'string') {
setContent(data.content);
} else {
setContent(String(data.content));
}
}
// 기존 첨부파일 정보 설정
if (data.attachments && Array.isArray(data.attachments) && data.attachments.length > 0) {
const att = data.attachments[0];
setExistingAttachment({
name: att.name || att.fileName || att.filename || '첨부파일',
size: att.size || att.fileSize || '',
url: att.url || att.downloadUrl,
fileKey: att.fileKey || att.key || att.fileId,
});
// 기존 파일이 있으면 fileKey도 설정
if (att.fileKey || att.key || att.fileId) {
setFileKey(att.fileKey || att.key || att.fileId);
}
} else if (data.attachment) {
// 단일 첨부파일인 경우
setExistingAttachment({
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 (data.attachment.fileKey || data.attachment.key || data.attachment.fileId) {
setFileKey(data.attachment.fileKey || data.attachment.key || data.attachment.fileId);
}
}
} catch (error) {
console.error('공지사항 조회 오류:', error);
alert('공지사항을 불러오는 중 오류가 발생했습니다.');
router.push('/admin/notices');
} finally {
setIsLoadingData(false);
}
}
fetchNotice();
}, [params?.id, router]);
const handleBack = () => {
router.push(`/admin/notices/${params.id}`);
};
const handleFileAttach = () => {
fileInputRef.current?.click();
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 30 * 1024 * 1024) {
alert('30MB 미만의 파일만 첨부할 수 있습니다.');
return;
}
try {
setIsLoading(true);
// 단일 파일 업로드
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);
// 새 파일을 업로드하면 기존 파일 정보 제거
setExistingAttachment(null);
} else {
throw new Error('파일 키를 받아오지 못했습니다.');
}
} catch (error) {
console.error('파일 업로드 실패:', error);
alert('파일 업로드에 실패했습니다. 다시 시도해주세요.');
setAttachedFile(null);
setFileKey(null);
} finally {
setIsLoading(false);
// 파일 입력 초기화
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
}
};
const handleFileRemove = () => {
setAttachedFile(null);
setExistingAttachment(null);
setFileKey(null);
// 파일 입력 초기화
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleSave = async () => {
if (!title.trim() || !content.trim()) {
setIsValidationModalOpen(true);
return;
}
if (!params?.id) {
alert('공지사항 ID를 찾을 수 없습니다.');
return;
}
try {
setIsLoading(true);
// 공지사항 수정 API 호출
const noticeData: any = {
title: title.trim(),
content: content.trim(),
};
// fileKey와 파일 정보가 있으면 attachments 배열로 포함
if (fileKey) {
if (attachedFile) {
// 새로 업로드한 파일
noticeData.attachments = [
{
fileKey: fileKey,
filename: attachedFile.name,
mimeType: attachedFile.type || 'application/octet-stream',
size: attachedFile.size,
},
];
} else if (existingAttachment && existingAttachment.fileKey) {
// 기존 파일 유지
noticeData.attachments = [
{
fileKey: existingAttachment.fileKey,
filename: existingAttachment.name,
},
];
}
}
await apiService.updateNotice(params.id, noticeData);
alert('공지사항이 수정되었습니다.');
router.push(`/admin/notices/${params.id}`);
} catch (error) {
console.error('공지사항 수정 실패:', error);
alert('공지사항 수정에 실패했습니다. 다시 시도해주세요.');
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
if (title.trim() || content.trim() || attachedFile || fileKey) {
setIsCancelModalOpen(true);
} else {
handleBack();
}
};
const handleCancelConfirm = () => {
setIsCancelModalOpen(false);
handleBack();
};
if (isLoadingData) {
return (
<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">
<AdminSidebar />
</div>
<main className="w-[1120px] bg-white">
<div className="h-full flex flex-col">
<div className="h-[100px] flex items-center justify-center px-[32px]">
<p className="text-[16px] font-medium text-[#333c47]"> ...</p>
</div>
</div>
</main>
</div>
</div>
</div>
);
}
return (
<>
<NoticeValidationModal
open={isValidationModalOpen}
onClose={() => setIsValidationModalOpen(false)}
/>
<NoticeCancelModal
open={isCancelModalOpen}
onClose={() => setIsCancelModalOpen(false)}
onConfirm={handleCancelConfirm}
/>
<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">
<AdminSidebar />
</div>
{/* 메인 콘텐츠 */}
<main className="w-[1120px] bg-white">
<div className="h-full flex flex-col px-8">
{/* 작성 모드 헤더 */}
<div className="h-[100px] flex items-center">
<div className="flex gap-3 items-center">
<button
type="button"
onClick={handleBack}
className="flex items-center justify-center w-8 h-8 cursor-pointer"
aria-label="뒤로가기"
>
<BackArrowSvg width={32} height={32} />
</button>
<h1 className="text-[24px] font-bold leading-[1.5] text-[#1b2027]">
</h1>
</div>
</div>
{/* 작성 폼 */}
<div className="flex-1 flex flex-col gap-10 pb-20 pt-8 w-full">
<div className="flex flex-col gap-6 w-full">
{/* 제목 입력 */}
<div className="flex flex-col gap-2 items-start justify-center w-full">
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] w-[100px]">
</label>
<input
type="text"
value={title}
onChange={(e) => 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"
/>
</div>
{/* 내용 입력 */}
<div className="flex flex-col gap-2 items-start justify-center w-full">
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] w-[100px]">
</label>
<div className="relative w-full">
<textarea
value={content}
onChange={(e) => {
const newContent = e.target.value;
if (newContent.length <= 1000) {
setContent(newContent);
}
}}
placeholder="내용을 입력해 주세요. (최대 1,000자 이내)"
className="w-full h-[320px] px-3 py-2 rounded-[8px] border border-[#dee1e6] bg-white text-[16px] font-normal leading-[1.5] text-[#1b2027] placeholder:text-[#b1b8c0] resize-none focus:outline-none focus:ring-2 focus:ring-[#1f2b91] focus:border-transparent"
/>
<div className="absolute bottom-3 right-3">
<p className="text-[13px] font-normal leading-[1.4] text-[#6c7682] text-right">
{characterCount}/1000
</p>
</div>
</div>
</div>
{/* 첨부 파일 */}
<div className="flex flex-col gap-2 items-start justify-center w-full">
<div className="flex items-center justify-between h-8 w-full">
<div className="flex items-center gap-3 flex-1 min-w-0">
<label className="text-[15px] font-semibold leading-[1.5] text-[#6c7682] whitespace-nowrap">
{' '}
<span className="font-normal">
{(attachedFile || existingAttachment) ? 1 : 0}/1
</span>
</label>
<p className="text-[13px] font-normal leading-[1.4] text-[#8c95a1] whitespace-nowrap">
30MB
</p>
</div>
<button
type="button"
onClick={handleFileAttach}
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
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept="*/*"
/>
</div>
<div className="h-16 w-full rounded-[8px] border border-[#dee1e6] bg-gray-50 flex items-center justify-between px-4">
{attachedFile ? (
<>
<p className="text-[14px] font-normal leading-[1.5] text-[#1b2027] flex-1 min-w-0 truncate">
{attachedFile.name}
</p>
<button
type="button"
onClick={handleFileRemove}
className="flex items-center justify-center w-6 h-6 cursor-pointer hover:opacity-70 transition-opacity shrink-0 ml-2"
aria-label="파일 삭제"
>
<CloseXOSvg />
</button>
</>
) : existingAttachment ? (
<>
<p className="text-[14px] font-normal leading-[1.5] text-[#1b2027] flex-1 min-w-0 truncate">
{existingAttachment.name}
</p>
<button
type="button"
onClick={handleFileRemove}
className="flex items-center justify-center w-6 h-6 cursor-pointer hover:opacity-70 transition-opacity shrink-0 ml-2"
aria-label="파일 삭제"
>
<CloseXOSvg />
</button>
</>
) : (
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center w-full">
.
</p>
)}
</div>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex gap-3 items-center justify-end shrink-0 w-full">
<button
type="button"
onClick={handleCancel}
className="h-12 px-8 rounded-[10px] bg-[#f1f3f5] text-[16px] font-semibold leading-[1.5] text-[#4c5561] whitespace-nowrap hover:bg-[#e5e8eb] transition-colors cursor-pointer"
>
</button>
<button
type="button"
onClick={handleSave}
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>
</div>
</main>
</div>
</div>
</div>
</>
);
}

View File

@@ -9,6 +9,7 @@ import DownloadIcon from '@/app/svgs/downloadicon';
import PaperClipSvg from '@/app/svgs/paperclipsvg';
import apiService from '@/app/lib/apiService';
import type { Notice } from '@/app/admin/notices/mockData';
import NoticeDeleteModal from '@/app/admin/notices/NoticeDeleteModal';
type Attachment = {
name: string;
@@ -25,6 +26,8 @@ export default function AdminNoticeDetailPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [showToast, setShowToast] = useState(false);
useEffect(() => {
async function fetchNotice() {
@@ -57,7 +60,7 @@ export default function AdminNoticeDetailPage() {
// 첨부파일 정보 처리
if (data.attachments && Array.isArray(data.attachments)) {
setAttachments(data.attachments.map((att: any) => ({
name: att.name || att.fileName || '',
name: att.name || att.fileName || att.filename || '',
size: att.size || att.fileSize || '',
url: att.url || att.downloadUrl,
fileKey: att.fileKey || att.key || att.fileId,
@@ -65,7 +68,7 @@ export default function AdminNoticeDetailPage() {
} else if (transformedNotice.hasAttachment && data.attachment) {
// 단일 첨부파일인 경우
setAttachments([{
name: data.attachment.name || data.attachment.fileName || '첨부파일',
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,
@@ -88,6 +91,16 @@ export default function AdminNoticeDetailPage() {
fetchNotice();
}, [params?.id]);
// 토스트 자동 닫기
useEffect(() => {
if (showToast) {
const timer = setTimeout(() => {
setShowToast(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [showToast]);
const handleDownload = async (fileKey?: string, url?: string, fileName?: string) => {
if (url) {
// URL이 있으면 직접 다운로드
@@ -299,34 +312,12 @@ export default function AdminNoticeDetailPage() {
<div className="flex items-center justify-end gap-[12px]">
<button
type="button"
onClick={async () => {
if (!confirm('정말 삭제하시겠습니까?')) {
return;
}
if (!params?.id) {
alert('공지사항 ID를 찾을 수 없습니다.');
return;
}
try {
setIsDeleting(true);
await apiService.deleteNotice(params.id);
alert('공지사항이 삭제되었습니다.');
router.push('/admin/notices');
} catch (err) {
console.error('공지사항 삭제 오류:', err);
const errorMessage = err instanceof Error ? err.message : '공지사항 삭제에 실패했습니다.';
alert(errorMessage);
} finally {
setIsDeleting(false);
}
}}
onClick={() => setIsDeleteModalOpen(true)}
disabled={isDeleting}
className="bg-[#FEF2F2] h-[48px] rounded-[10px] px-[8px] shrink-0 min-w-[80px] flex items-center justify-center hover:bg-[#FEE2E2] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="text-[16px] font-semibold leading-[1.5] text-[#F64C4C] text-center">
{isDeleting ? '삭제 중...' : '삭제'}
</span>
</button>
<button
@@ -344,6 +335,55 @@ export default function AdminNoticeDetailPage() {
</main>
</div>
</div>
{/* 삭제 확인 모달 */}
<NoticeDeleteModal
open={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={async () => {
if (!params?.id) {
alert('공지사항 ID를 찾을 수 없습니다.');
setIsDeleteModalOpen(false);
return;
}
try {
setIsDeleting(true);
await apiService.deleteNotice(params.id);
setIsDeleteModalOpen(false);
setShowToast(true);
// 토스트 표시 후 목록 페이지로 이동
setTimeout(() => {
router.push('/admin/notices');
}, 1500);
} catch (err) {
console.error('공지사항 삭제 오류:', err);
const errorMessage = err instanceof Error ? err.message : '공지사항 삭제에 실패했습니다.';
alert(errorMessage);
setIsDeleteModalOpen(false);
} finally {
setIsDeleting(false);
}
}}
isDeleting={isDeleting}
/>
{/* 삭제 완료 토스트 */}
{showToast && (
<div className="fixed right-[60px] bottom-[60px] z-50">
<div className="bg-white border border-[#dee1e6] rounded-[8px] p-4 min-w-[360px] flex gap-[10px] items-center">
<div className="relative shrink-0 w-[16.667px] h-[16.667px]">
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8.5" cy="8.5" r="8.5" fill="#384FBF"/>
<path d="M5.5 8.5L7.5 10.5L11.5 6.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<p className="text-[15px] font-medium leading-[1.5] text-[#1b2027] text-nowrap">
.
</p>
</div>
</div>
)}
</div>
);
}

View File

@@ -8,6 +8,7 @@ import BackArrowSvg from "@/app/svgs/backarrow";
import { type Notice } from "@/app/admin/notices/mockData";
import apiService from "@/app/lib/apiService";
import NoticeValidationModal from "@/app/admin/notices/NoticeValidationModal";
import NoticeCancelModal from "@/app/admin/notices/NoticeCancelModal";
export default function AdminNoticesPage() {
const router = useRouter();
@@ -20,6 +21,7 @@ export default function AdminNoticesPage() {
const [fileKey, setFileKey] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 날짜를 yyyy-mm-dd 형식으로 포맷팅
@@ -223,13 +225,16 @@ export default function AdminNoticesPage() {
const handleCancel = () => {
if (title.trim() || content.trim() || attachedFile || fileKey) {
if (confirm('작성 중인 내용이 있습니다. 정말 취소하시겠습니까?')) {
handleBack();
}
setIsCancelModalOpen(true);
} else {
handleBack();
}
};
const handleCancelConfirm = () => {
setIsCancelModalOpen(false);
handleBack();
};
const ITEMS_PER_PAGE = 10;
const sortedNotices = useMemo(() => {
@@ -252,6 +257,11 @@ export default function AdminNoticesPage() {
open={isValidationModalOpen}
onClose={() => setIsValidationModalOpen(false)}
/>
<NoticeCancelModal
open={isCancelModalOpen}
onClose={() => setIsCancelModalOpen(false)}
onConfirm={handleCancelConfirm}
/>
<div className="min-h-screen flex flex-col bg-white">
{/* 메인 레이아웃 */}
<div className="flex flex-1 min-h-0 justify-center">
@@ -357,13 +367,13 @@ export default function AdminNoticesPage() {
accept="*/*"
/>
</div>
<div className="h-16 w-full rounded-[8px] border border-[#dee1e6] bg-gray-50 flex items-center justify-center">
<div className="h-16 w-full rounded-[8px] border border-[#dee1e6] bg-gray-50 flex items-center px-4">
{attachedFile ? (
<p className="text-[14px] font-normal leading-[1.5] text-[#1b2027]">
{attachedFile.name}
</p>
) : (
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center">
<p className="text-[14px] font-normal leading-[1.5] text-[#8c95a1] text-center w-full">
.
</p>
)}