공지사항 admin ok

This commit is contained in:
2025-11-29 23:57:31 +09:00
parent eb7871133d
commit 109ca05e23
8 changed files with 225 additions and 59 deletions

View File

@@ -0,0 +1,49 @@
'use client';
import React from 'react';
type NoChangesModalProps = {
open: boolean;
onClose: () => void;
};
/**
* 변경사항이 없을 때 표시되는 모달
*/
export default function NoChangesModal({
open,
onClose,
}: NoChangesModalProps) {
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 min-w-[320px]">
<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>
<div className="flex gap-[8px] items-center justify-end shrink-0">
<button
type="button"
onClick={onClose}
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

@@ -8,6 +8,7 @@ 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";
import NoChangesModal from "@/app/admin/notices/NoChangesModal";
type Attachment = {
name: string;
@@ -24,10 +25,14 @@ export default function AdminNoticeEditPage() {
const [attachedFile, setAttachedFile] = useState<File | null>(null);
const [fileKey, setFileKey] = useState<string | null>(null);
const [existingAttachment, setExistingAttachment] = useState<Attachment | null>(null);
const [originalTitle, setOriginalTitle] = useState<string>('');
const [originalContent, setOriginalContent] = useState<string>('');
const [originalFileKey, setOriginalFileKey] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
const [isNoChangesModalOpen, setIsNoChangesModalOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const characterCount = useMemo(() => content.length, [content]);
@@ -46,18 +51,23 @@ export default function AdminNoticeEditPage() {
const data = response.data;
// 제목 설정
setTitle(data.title || '');
const loadedTitle = data.title || '';
setTitle(loadedTitle);
setOriginalTitle(loadedTitle);
// 내용 설정 (배열이면 join, 문자열이면 그대로)
let loadedContent = '';
if (data.content) {
if (Array.isArray(data.content)) {
setContent(data.content.join('\n'));
loadedContent = data.content.join('\n');
} else if (typeof data.content === 'string') {
setContent(data.content);
loadedContent = data.content;
} else {
setContent(String(data.content));
loadedContent = String(data.content);
}
}
setContent(loadedContent);
setOriginalContent(loadedContent);
// 기존 첨부파일 정보 설정
if (data.attachments && Array.isArray(data.attachments) && data.attachments.length > 0) {
@@ -69,8 +79,10 @@ export default function AdminNoticeEditPage() {
fileKey: att.fileKey || att.key || att.fileId,
});
// 기존 파일이 있으면 fileKey도 설정
if (att.fileKey || att.key || att.fileId) {
setFileKey(att.fileKey || att.key || att.fileId);
const loadedFileKey = att.fileKey || att.key || att.fileId;
if (loadedFileKey) {
setFileKey(loadedFileKey);
setOriginalFileKey(loadedFileKey);
}
} else if (data.attachment) {
// 단일 첨부파일인 경우
@@ -80,8 +92,10 @@ export default function AdminNoticeEditPage() {
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);
const loadedFileKey = data.attachment.fileKey || data.attachment.key || data.attachment.fileId;
if (loadedFileKey) {
setFileKey(loadedFileKey);
setOriginalFileKey(loadedFileKey);
}
}
} catch (error) {
@@ -184,19 +198,36 @@ export default function AdminNoticeEditPage() {
try {
setIsLoading(true);
// 공지사항 수정 API 호출
const noticeData: any = {
title: title.trim(),
content: content.trim(),
};
// 변경된 필드만 포함하는 request body 생성
const noticeData: any = {};
// fileKey와 파일 정보가 있으면 attachments 배열로 포함
if (fileKey) {
// 제목이 변경되었는지 확인
const trimmedTitle = title.trim();
if (trimmedTitle !== originalTitle) {
noticeData.title = trimmedTitle;
}
// 내용이 변경되었는지 확인
const trimmedContent = content.trim();
if (trimmedContent !== originalContent) {
noticeData.content = trimmedContent;
}
// 파일 변경사항 확인
const currentFileKey = fileKey;
const hasFileChanged = currentFileKey !== originalFileKey;
// 파일이 삭제된 경우 (기존에 파일이 있었는데 지금 없음)
if (originalFileKey && !currentFileKey) {
noticeData.attachments = [];
}
// 파일이 변경되었거나 새로 추가된 경우
else if (hasFileChanged && currentFileKey) {
if (attachedFile) {
// 새로 업로드한 파일
noticeData.attachments = [
{
fileKey: fileKey,
fileKey: currentFileKey,
filename: attachedFile.name,
mimeType: attachedFile.type || 'application/octet-stream',
size: attachedFile.size,
@@ -213,10 +244,17 @@ export default function AdminNoticeEditPage() {
}
}
// 변경사항이 없으면 알림 후 리턴
if (Object.keys(noticeData).length === 0) {
setIsNoChangesModalOpen(true);
setIsLoading(false);
return;
}
await apiService.updateNotice(params.id, noticeData);
alert('공지사항이 수정되었습니다.');
router.push(`/admin/notices/${params.id}`);
// 성공 시 공지사항 리스트로 이동 (토스트는 리스트 페이지에서 표시)
router.push('/admin/notices?updated=true');
} catch (error) {
console.error('공지사항 수정 실패:', error);
alert('공지사항 수정에 실패했습니다. 다시 시도해주세요.');
@@ -270,6 +308,10 @@ export default function AdminNoticeEditPage() {
onClose={() => setIsCancelModalOpen(false)}
onConfirm={handleCancelConfirm}
/>
<NoChangesModal
open={isNoChangesModalOpen}
onClose={() => setIsNoChangesModalOpen(false)}
/>
<div className="min-h-screen flex flex-col bg-white">
{/* 메인 레이아웃 */}
<div className="flex flex-1 min-h-0 justify-center">

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useMemo, useRef, ChangeEvent, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import AdminSidebar from "@/app/components/AdminSidebar";
import ChevronDownSvg from "@/app/svgs/chevrondownsvg";
import BackArrowSvg from "@/app/svgs/backarrow";
@@ -12,6 +12,7 @@ import NoticeCancelModal from "@/app/admin/notices/NoticeCancelModal";
export default function AdminNoticesPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [notices, setNotices] = useState<Notice[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isWritingMode, setIsWritingMode] = useState(false);
@@ -22,6 +23,7 @@ export default function AdminNoticesPage() {
const [isLoading, setIsLoading] = useState(false);
const [isValidationModalOpen, setIsValidationModalOpen] = useState(false);
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
const [showToast, setShowToast] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 날짜를 yyyy-mm-dd 형식으로 포맷팅
@@ -86,6 +88,20 @@ export default function AdminNoticesPage() {
fetchNotices();
}, []);
// 수정 완료 쿼리 파라미터 확인 및 토스트 표시
useEffect(() => {
if (searchParams.get('updated') === 'true') {
setShowToast(true);
// URL에서 쿼리 파라미터 제거
router.replace('/admin/notices');
// 토스트 자동 닫기
const timer = setTimeout(() => {
setShowToast(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [searchParams, router]);
const totalCount = useMemo(() => notices.length, [notices]);
@@ -581,6 +597,23 @@ export default function AdminNoticesPage() {
</div>
</div>
</div>
{/* 수정 완료 토스트 */}
{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>
)}
</>
);
}