From a8d6982851ca616228bc1d72f348d687985b73b4 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 15:17:03 +0900 Subject: [PATCH 01/10] =?UTF-8?q?#235=20feat:=20=EC=84=B8=EB=AF=B8?= =?UTF-8?q?=EB=82=98=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/AppContent.tsx | 6 +- .../Header/Navigation/Navigation.tsx | 2 +- frontend/src/components/Header/constants.ts | 2 +- frontend/src/config/apiConfig.ts | 30 ++ frontend/src/pages/Seminar/SeminarCreate.tsx | 1 + frontend/src/pages/Seminar/SeminarDetail.tsx | 1 + frontend/src/pages/Seminar/SeminarList.tsx | 319 ++++++++++++++++++ frontend/src/pages/Seminar/SeminarStyle.ts | 1 + .../components/SeminarTable/SeminarTable.tsx | 1 + .../SeminarTable/SeminarTableStyle.ts | 1 + .../src/pages/Seminar/types/seminar.types.ts | 1 + 11 files changed, 359 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/Seminar/SeminarCreate.tsx create mode 100644 frontend/src/pages/Seminar/SeminarDetail.tsx create mode 100644 frontend/src/pages/Seminar/SeminarList.tsx create mode 100644 frontend/src/pages/Seminar/SeminarStyle.ts create mode 100644 frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx create mode 100644 frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts create mode 100644 frontend/src/pages/Seminar/types/seminar.types.ts diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx index e966cb19..5db0bbbf 100644 --- a/frontend/src/AppContent.tsx +++ b/frontend/src/AppContent.tsx @@ -31,6 +31,7 @@ import ThesisDetail from './pages/News/Thesis/ThesisDetail'; import Organization from './pages/About/Organization/Organization'; import Curriculum from './pages/Undergraduate/Curriculum/Curriculum'; import NotFound from './components/Notfound/NotFound'; +import SeminarList from './pages/Seminar/SeminarList'; interface PageTransitionProps { children: React.ReactNode; @@ -190,16 +191,13 @@ function AppContent() { path="/undergraduate/curriculum" element={} /> - {/* graduate */} } /> - {/* about */} } /> } /> } /> } /> - {/* news */} } /> } /> @@ -207,9 +205,9 @@ function AppContent() { path="/seminar-rooms/reservation" element={} /> + } /> } /> } /> - {/* 어드민 권한 보호 Routes */} FormData; } +export interface SeminarDto { + name: string; + writer: string; + place: string; + startDate: string; + endDate: string; + speaker: string; + company: string; +} + export const apiEndpoints = { thesis: { list: `${API_URL}/api/thesis`, @@ -236,6 +246,26 @@ export const apiEndpoints = { getByCategory: (category: string, page: number, size: number) => `${API_URL}/api/board/category/${category}?page=${page}&size=${size}`, }, + + seminar: { + list: `${API_URL}/api/seminar`, + listWithPage: (page: number, size: number, sortDirection?: string) => { + const params = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + }); + if (sortDirection) { + params.append('sortDirection', sortDirection); + } + return `${API_URL}/api/seminar?${params.toString()}`; + }, + get: (seminarId: string | number) => `${API_URL}/api/seminar/${seminarId}`, + create: `${API_URL}/api/seminar`, + update: (seminarId: string | number) => + `${API_URL}/api/seminar/${seminarId}`, + delete: (seminarId: string | number) => + `${API_URL}/api/seminar/${seminarId}`, + }, }; export default API_URL; diff --git a/frontend/src/pages/Seminar/SeminarCreate.tsx b/frontend/src/pages/Seminar/SeminarCreate.tsx new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/SeminarCreate.tsx @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/pages/Seminar/SeminarList.tsx b/frontend/src/pages/Seminar/SeminarList.tsx new file mode 100644 index 00000000..657cb954 --- /dev/null +++ b/frontend/src/pages/Seminar/SeminarList.tsx @@ -0,0 +1,319 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import styled from 'styled-components'; +import { AuthContext } from '../../context/AuthContext'; +import { apiEndpoints } from '../../config/apiConfig'; + +interface SeminarItem { + id: number; + name: string; + writer: string; + place: string; + startDate: string; + endDate: string; + speaker: string; + company: string; +} + +interface PageResponse { + message: string; + page: number; + totalPage: number; + data: SeminarItem[]; +} + +const SeminarList = () => { + const navigate = useNavigate(); + const auth = useContext(AuthContext); + const [seminars, setSeminars] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [pageInfo, setPageInfo] = useState({ + currentPage: 0, + totalPages: 0, + }); + const [sortDirection, setSortDirection] = useState<'ASC' | 'DESC'>('DESC'); + + useEffect(() => { + fetchSeminars(); + }, [pageInfo.currentPage, sortDirection]); + + const fetchSeminars = async () => { + try { + setLoading(true); + const response = await axios.get( + apiEndpoints.seminar.listWithPage( + pageInfo.currentPage, + 10, + sortDirection, + ), + ); + + setSeminars(response.data.data); + setPageInfo({ + currentPage: response.data.page, + totalPages: response.data.totalPage, + }); + } catch (err) { + setError('세미나 목록을 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + const handlePageChange = (newPage: number) => { + setPageInfo((prev) => ({ + ...prev, + currentPage: newPage, + })); + }; + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date + .toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + .replace(/\. /g, '-') + .replace('.', ''); + }; + + const handleCreateClick = () => { + navigate('/seminar/create'); + }; + + if (loading) return Loading...; + if (error) return {error}; + + return ( + +
+ 세미나 목록 + {auth?.isAuthenticated && ( + 세미나 등록 + )} +
+ + + + + + + + + + + setSortDirection((prev) => (prev === 'ASC' ? 'DESC' : 'ASC')) + } + isActive={true} + sortDirection={sortDirection.toLowerCase() as 'asc' | 'desc'} + > + 날짜 + + + + + {seminars.map((seminar) => ( + navigate(`/seminar/${seminar.id}`)} + > + + {seminar.name} + + + + + + ))} + +
번호세미나명발표자소속장소
{seminar.id}{seminar.speaker}{seminar.company}{seminar.place}{formatDate(seminar.startDate)}
+ + {seminars.length === 0 && ( + 등록된 세미나가 없습니다. + )} + + + handlePageChange(0)} + disabled={pageInfo.currentPage === 0} + > + ⟪ + + handlePageChange(pageInfo.currentPage - 1)} + disabled={pageInfo.currentPage === 0} + > + ⟨ + + {Array.from({ length: pageInfo.totalPages }, (_, i) => ( + handlePageChange(i)} + isActive={i === pageInfo.currentPage} + > + {i + 1} + + ))} + handlePageChange(pageInfo.currentPage + 1)} + disabled={pageInfo.currentPage === pageInfo.totalPages - 1} + > + ⟩ + + handlePageChange(pageInfo.totalPages - 1)} + disabled={pageInfo.currentPage === pageInfo.totalPages - 1} + > + ⟫ + + +
+ ); +}; + +const Container = styled.div` + max-width: 1400px; + width: 95%; + margin: 0 auto; + padding: 40px 20px; +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +`; + +const Title = styled.h1` + font-size: 1.8rem; + color: #1a202c; + margin: 0; +`; + +const Table = styled.table` + width: 100%; + border-collapse: collapse; + font-size: 1rem; + background-color: #fff; +`; + +const Th = styled.th` + padding: 1rem; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + background-color: #f8f9fa; + font-weight: 600; + text-align: center; +`; + +const SortableTh = styled(Th)<{ + isActive?: boolean; + sortDirection?: 'asc' | 'desc'; +}>` + cursor: pointer; + position: relative; + padding-right: 25px; + + &:after { + content: '${(props) => (props.sortDirection === 'asc' ? '↑' : '↓')}'; + position: absolute; + right: 8px; + } + + &:hover { + background-color: #f1f3f5; + } +`; + +const Tr = styled.tr` + cursor: pointer; + &:hover { + background-color: #f8f9fa; + } +`; + +const Td = styled.td` + padding: 1rem; + border-bottom: 1px solid #ddd; + text-align: center; +`; + +const TitleTd = styled(Td)` + text-align: left; + font-weight: 500; +`; + +const CreateButton = styled.button` + padding: 0.5rem 1rem; + background-color: #b71c1c; + color: white; + border: none; + border-radius: 4px; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #8b0000; + } +`; + +const PaginationContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 2rem; +`; + +const PageButton = styled.button<{ isActive?: boolean }>` + padding: 0.5rem 1rem; + border: 1px solid ${(props) => (props.isActive ? '#666' : '#ddd')}; + background-color: ${(props) => (props.isActive ? '#666' : 'white')}; + color: ${(props) => (props.isActive ? 'white' : '#333')}; + cursor: pointer; + min-width: 40px; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &:hover:not(:disabled) { + background-color: ${(props) => (props.isActive ? '#555' : '#f8f9fa')}; + } +`; + +const LoadingSpinner = styled.div` + text-align: center; + padding: 2rem; + color: #666; +`; + +const ErrorMessage = styled.div` + text-align: center; + padding: 1rem; + margin: 1rem 0; + background-color: #fff5f5; + color: #e53e3e; + border-radius: 4px; + border: 1px solid #feb2b2; +`; + +const EmptyMessage = styled.div` + text-align: center; + padding: 2rem; + color: #666; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 1rem; +`; + +export default SeminarList; diff --git a/frontend/src/pages/Seminar/SeminarStyle.ts b/frontend/src/pages/Seminar/SeminarStyle.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/SeminarStyle.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/src/pages/Seminar/types/seminar.types.ts b/frontend/src/pages/Seminar/types/seminar.types.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/frontend/src/pages/Seminar/types/seminar.types.ts @@ -0,0 +1 @@ +export {}; From d7c4bf4b3a5b25186f284eae4fc9d8e747dc50af Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 15:34:28 +0900 Subject: [PATCH 02/10] =?UTF-8?q?#235=20feat:=20=EC=84=B8=EB=AF=B8?= =?UTF-8?q?=EB=82=98=20=EC=83=81=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/AppContent.tsx | 2 + frontend/src/pages/Seminar/SeminarDetail.tsx | 240 ++++++++++++++++++- frontend/src/pages/Seminar/SeminarList.tsx | 4 +- 3 files changed, 243 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx index 5db0bbbf..d7b325d2 100644 --- a/frontend/src/AppContent.tsx +++ b/frontend/src/AppContent.tsx @@ -32,6 +32,7 @@ import Organization from './pages/About/Organization/Organization'; import Curriculum from './pages/Undergraduate/Curriculum/Curriculum'; import NotFound from './components/Notfound/NotFound'; import SeminarList from './pages/Seminar/SeminarList'; +import SeminarDetail from './pages/Seminar/SeminarDetail'; interface PageTransitionProps { children: React.ReactNode; @@ -206,6 +207,7 @@ function AppContent() { element={} /> } /> + } /> } /> } /> {/* 어드민 권한 보호 Routes */} diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx index cb0ff5c3..4a2f1398 100644 --- a/frontend/src/pages/Seminar/SeminarDetail.tsx +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -1 +1,239 @@ -export {}; +import React, { useState, useEffect, useContext } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import styled from 'styled-components'; +import { AuthContext } from '../../context/AuthContext'; +import { apiEndpoints } from '../../config/apiConfig'; + +interface SeminarDetail { + id: number; + name: string; + writer: string; + place: string; + startDate: string; + endDate: string; + speaker: string; + company: string; +} + +const SeminarDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const auth = useContext(AuthContext); + const [seminar, setSeminar] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchSeminarDetail = async () => { + if (!id) return; + + setLoading(true); + setError(null); + + try { + const response = await axios.get( + apiEndpoints.seminar.get(id), + ); + setSeminar(response.data); + } catch (error) { + console.error('Failed to fetch seminar details:', error); + setError('세미나 정보를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + fetchSeminarDetail(); + }, [id]); + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date + .toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + .replace(/\. /g, '-') + .replace('.', ''); + }; + + if (loading) return Loading...; + if (error) return {error}; + if (!seminar) return 세미나를 찾을 수 없습니다.; + + return ( + + +
+ {seminar.name} + {auth?.isAuthenticated && ( + + navigate(`/seminar/edit/${seminar.id}`)} + > + 수정 + + {}}>삭제 + + )} +
+ + + + 발표자 + {seminar.speaker} + + + 소속 + {seminar.company} + + + 장소 + {seminar.place} + + + 일시 + + {formatDate(seminar.startDate)} + {seminar.startDate !== seminar.endDate && + ` ~ ${formatDate(seminar.endDate)}`} + + + + 작성자 + {seminar.writer} + + + + + navigate('/news/seminar')}> + 목록으로 + + +
+
+ ); +}; + +const Container = styled.div` + max-width: 1400px; + width: 95%; + margin: 0 auto; + padding: 40px 20px; +`; + +const Card = styled.div` + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 2rem; +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e2e8f0; +`; + +const Title = styled.h1` + font-size: 1.8rem; + font-weight: bold; + color: #1a202c; + margin: 0; +`; + +const InfoGrid = styled.div` + display: grid; + gap: 1rem; +`; + +const InfoRow = styled.div` + display: grid; + grid-template-columns: 120px 1fr; + gap: 1rem; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #e2e8f0; + + &:last-child { + border-bottom: none; + } +`; + +const InfoLabel = styled.span` + font-weight: 600; + color: #4a5568; +`; + +const InfoValue = styled.span` + color: #2d3748; +`; + +const ButtonGroup = styled.div` + display: flex; + gap: 0.5rem; +`; + +const Button = styled.button` + padding: 0.5rem 1rem; + font-size: 0.9rem; + border: 1px solid #ddd; + background-color: white; + color: #333; + cursor: pointer; + transition: all 0.2s ease-in-out; + min-width: 80px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + + &:hover { + background-color: #f8f9fa; + border-color: #ccc; + } +`; + +const BackButton = styled(Button)` + background-color: white; +`; + +const EditButton = styled(Button)` + background-color: white; +`; + +const DeleteButton = styled(Button)` + background-color: white; + color: #dc3545; + + &:hover { + background-color: #fff5f5; + border-color: #dc3545; + } +`; + +const LoadingSpinner = styled.div` + text-align: center; + padding: 2rem; + color: #666; + font-size: 1.1rem; +`; + +const ErrorMessage = styled.div` + text-align: center; + padding: 1rem; + margin: 1rem 0; + background-color: #fff5f5; + color: #e53e3e; + border-radius: 4px; + font-size: 1rem; + border: 1px solid #feb2b2; +`; + +export default SeminarDetail; diff --git a/frontend/src/pages/Seminar/SeminarList.tsx b/frontend/src/pages/Seminar/SeminarList.tsx index 657cb954..094ef9a6 100644 --- a/frontend/src/pages/Seminar/SeminarList.tsx +++ b/frontend/src/pages/Seminar/SeminarList.tsx @@ -82,7 +82,7 @@ const SeminarList = () => { }; const handleCreateClick = () => { - navigate('/seminar/create'); + navigate('/news/seminar/create'); }; if (loading) return Loading...; @@ -120,7 +120,7 @@ const SeminarList = () => { {seminars.map((seminar) => ( navigate(`/seminar/${seminar.id}`)} + onClick={() => navigate(`/news/seminar/${seminar.id}`)} > {seminar.id} {seminar.name} From f938eb49ffedd5301bf31be487e9cd273da0e3df Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 15:45:13 +0900 Subject: [PATCH 03/10] =?UTF-8?q?#235=20style:=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/AppContent.tsx | 17 + frontend/src/pages/Seminar/SeminarCreate.tsx | 344 ++++++++++++++++++- frontend/src/pages/Seminar/SeminarDetail.tsx | 10 +- 3 files changed, 368 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx index d7b325d2..6c4f4c24 100644 --- a/frontend/src/AppContent.tsx +++ b/frontend/src/AppContent.tsx @@ -33,6 +33,7 @@ import Curriculum from './pages/Undergraduate/Curriculum/Curriculum'; import NotFound from './components/Notfound/NotFound'; import SeminarList from './pages/Seminar/SeminarList'; import SeminarDetail from './pages/Seminar/SeminarDetail'; +import SeminarCreate from './pages/Seminar/SeminarCreate'; interface PageTransitionProps { children: React.ReactNode; @@ -259,6 +260,22 @@ function AppContent() { } /> + + + + } + /> + + + + } + /> } /> diff --git a/frontend/src/pages/Seminar/SeminarCreate.tsx b/frontend/src/pages/Seminar/SeminarCreate.tsx index cb0ff5c3..97676a3c 100644 --- a/frontend/src/pages/Seminar/SeminarCreate.tsx +++ b/frontend/src/pages/Seminar/SeminarCreate.tsx @@ -1 +1,343 @@ -export {}; +import React, { useState, useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import styled from 'styled-components'; +import { AuthContext } from '../../context/AuthContext'; +import { apiEndpoints, SeminarDto } from '../../config/apiConfig'; +import { Modal, useModal } from '../../components/Modal'; +import { AlertTriangle, CheckCircle } from 'lucide-react'; + +const SeminarCreate = () => { + const navigate = useNavigate(); + const auth = useContext(AuthContext); + const { openModal } = useModal(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ + name: '', + writer: auth?.user || '', + place: '', + startDate: '', + endDate: '', + speaker: '', + company: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const showErrorModal = (message: string) => { + openModal( + <> + + + 등록 실패 + + +

{message}

+
+ + + + , + ); + }; + + const showSuccessModal = () => { + openModal( + <> + + + 등록 완료 + + +

세미나가 성공적으로 등록되었습니다.

+
+ + navigate('/news/seminar')} /> + + , + ); + }; + + const validateForm = () => { + const requiredFields: (keyof SeminarDto)[] = [ + 'name', + 'place', + 'startDate', + 'endDate', + 'speaker', + 'company', + ]; + const emptyFields = requiredFields.filter((field) => !formData[field]); + + if (emptyFields.length > 0) { + showErrorModal('모든 필드를 입력해주세요.'); + return false; + } + + if (new Date(formData.startDate) > new Date(formData.endDate)) { + showErrorModal('종료일은 시작일보다 빠를 수 없습니다.'); + return false; + } + + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + try { + setIsSubmitting(true); + await axios.post(apiEndpoints.seminar.create, formData); + showSuccessModal(); + } catch (error) { + console.error('Error creating seminar:', error); + showErrorModal('세미나 등록 중 오류가 발생했습니다.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + +
+

세미나 등록

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + navigate('/news/seminar')} + > + 취소 + + + {isSubmitting ? '등록 중...' : '세미나 등록'} + + + +
+
+ ); +}; + +const Container = styled.div` + max-width: 1400px; + width: 95%; + margin: 2rem auto; + padding: 40px 20px; + + @media (max-width: 768px) { + width: 100%; + padding: 20px; + } +`; +const ContentWrapper = styled.div` + background-color: white; + border: 1px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + max-width: 1000px; + margin: 0 auto; +`; +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 3rem; // 24px에서 2rem 3rem으로 변경 + background-color: #f8fafc; + border-bottom: 1px solid #e2e8f0; + + h1 { + font-size: 1.8rem; + color: #1a202c; + margin: 0; + font-weight: 600; + } + + @media (max-width: 768px) { + padding: 1.5rem; + } +`; + +const FormSection = styled.form` + padding: 32px 24px; +`; + +const FormGroup = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } +`; + +const FormRow = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-bottom: 24px; +`; + +const Label = styled.label` + font-size: 1rem; + font-weight: 600; + color: #2d3748; +`; + +const Input = styled.input` + width: 100%; + padding: 12px 16px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 1rem; + transition: all 0.2s ease-in-out; + + &:focus { + outline: none; + border-color: #3182ce; + box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1); + } +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid #e2e8f0; +`; + +const Button = styled.button` + padding: 0.5rem 1rem; + font-size: 0.9rem; + border: 1px solid #ddd; + background-color: white; + color: #333; + cursor: pointer; + transition: all 0.2s ease-in-out; + min-width: 80px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + + &:hover { + background-color: #f8f9fa; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.7; + } +`; + +const CancelButton = styled(Button)` + background-color: white; + border-color: #ddd; + + &:hover { + background-color: #f8f9fa; + border-color: #ccc; + } +`; + +const SubmitButton = styled(Button)` + background-color: #b71c1c; + border-color: #b71c1c; + color: white; + + &:hover { + background-color: #8b0000; + border-color: #8b0000; + } + + &:disabled { + background-color: #d32f2f; + border-color: #d32f2f; + color: white; + } +`; + +export default SeminarCreate; diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx index 4a2f1398..c81dd692 100644 --- a/frontend/src/pages/Seminar/SeminarDetail.tsx +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -126,9 +126,15 @@ const Container = styled.div` const Card = styled.div` background: white; - border-radius: 8px; + border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - padding: 2rem; + padding: 3rem; + max-width: 1000px; + margin: 0 auto; + + @media (max-width: 768px) { + padding: 1.5rem; + } `; const Header = styled.div` From d78ded1509fe9dc6af085e9bf72eba8451fa60cd Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 17:15:13 +0900 Subject: [PATCH 04/10] =?UTF-8?q?#235=20feat:=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/AppContent.tsx | 3 +- frontend/src/constants/pageContents.ts | 6 + frontend/src/pages/Seminar/SeminarDetail.tsx | 2 +- frontend/src/pages/Seminar/SeminarEdit.tsx | 391 +++++++++++++++++++ frontend/src/pages/Seminar/SeminarList.tsx | 140 +++++-- 5 files changed, 497 insertions(+), 45 deletions(-) create mode 100644 frontend/src/pages/Seminar/SeminarEdit.tsx diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx index 6c4f4c24..a1ca9214 100644 --- a/frontend/src/AppContent.tsx +++ b/frontend/src/AppContent.tsx @@ -34,6 +34,7 @@ import NotFound from './components/Notfound/NotFound'; import SeminarList from './pages/Seminar/SeminarList'; import SeminarDetail from './pages/Seminar/SeminarDetail'; import SeminarCreate from './pages/Seminar/SeminarCreate'; +import SeminarEdit from './pages/Seminar/SeminarEdit'; interface PageTransitionProps { children: React.ReactNode; @@ -272,7 +273,7 @@ function AppContent() { path="/news/seminar/edit/:id" element={ - + } /> diff --git a/frontend/src/constants/pageContents.ts b/frontend/src/constants/pageContents.ts index 285da182..efbea833 100644 --- a/frontend/src/constants/pageContents.ts +++ b/frontend/src/constants/pageContents.ts @@ -87,6 +87,12 @@ export const PAGE_CONTENTS = { image: newsImg, path: '/news/thesis', }, + seminar: { + title: '세미나', + description: '바이오융합공학전공 세미나 조회', + image: newsImg, + path: '/news/seminar', + }, }, }, seminar: { diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx index c81dd692..65f39d4b 100644 --- a/frontend/src/pages/Seminar/SeminarDetail.tsx +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -71,7 +71,7 @@ const SeminarDetail: React.FC = () => { {auth?.isAuthenticated && ( navigate(`/seminar/edit/${seminar.id}`)} + onClick={() => navigate(`/news/seminar/edit/${seminar.id}`)} > 수정 diff --git a/frontend/src/pages/Seminar/SeminarEdit.tsx b/frontend/src/pages/Seminar/SeminarEdit.tsx new file mode 100644 index 00000000..e6b52475 --- /dev/null +++ b/frontend/src/pages/Seminar/SeminarEdit.tsx @@ -0,0 +1,391 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from 'axios'; +import styled from 'styled-components'; +import { AuthContext } from '../../context/AuthContext'; +import { apiEndpoints, SeminarDto } from '../../config/apiConfig'; +import { Modal, useModal } from '../../components/Modal'; +import { AlertTriangle, CheckCircle } from 'lucide-react'; +import { SEJONG_COLORS } from '../../constants/colors'; + +const SeminarEdit = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const auth = useContext(AuthContext); + const { openModal } = useModal(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [loading, setLoading] = useState(true); + + const [formData, setFormData] = useState({ + name: '', + writer: '', + place: '', + startDate: '', + endDate: '', + speaker: '', + company: '', + }); + + useEffect(() => { + const fetchSeminar = async () => { + try { + const response = await axios.get(apiEndpoints.seminar.get(id!)); + const seminarData = response.data; + setFormData({ + name: seminarData.name, + writer: seminarData.writer, + place: seminarData.place, + startDate: seminarData.startDate, + endDate: seminarData.endDate, + speaker: seminarData.speaker, + company: seminarData.company, + }); + } catch (error) { + showErrorModal('세미나 정보를 불러오는데 실패했습니다.'); + navigate('/news/seminar'); + } finally { + setLoading(false); + } + }; + + if (id) { + fetchSeminar(); + } + }, [id, navigate]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const showErrorModal = (message: string) => { + openModal( + <> + + + 수정 실패 + + +

{message}

+
+ + + + , + ); + }; + + const showSuccessModal = () => { + openModal( + <> + + + 수정 완료 + + +

세미나가 성공적으로 수정되었습니다.

+
+ + navigate(`/news/seminar/${id}`)} /> + + , + ); + }; + + const validateForm = () => { + const requiredFields: (keyof SeminarDto)[] = [ + 'name', + 'place', + 'startDate', + 'endDate', + 'speaker', + 'company', + ]; + const emptyFields = requiredFields.filter((field) => !formData[field]); + + if (emptyFields.length > 0) { + showErrorModal('모든 필드를 입력해주세요.'); + return false; + } + + if (new Date(formData.startDate) > new Date(formData.endDate)) { + showErrorModal('종료일은 시작일보다 빠를 수 없습니다.'); + return false; + } + + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + try { + setIsSubmitting(true); + await axios.post(apiEndpoints.seminar.update(id!), formData); + showSuccessModal(); + } catch (error) { + console.error('Error updating seminar:', error); + showErrorModal('세미나 수정 중 오류가 발생했습니다.'); + } finally { + setIsSubmitting(false); + } + }; + + if (loading) return <>Loading...; + + return ( + + +
+

세미나 수정

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + navigate(`/news/seminar/${id}`)} + > + 취소 + + + {isSubmitting ? '수정 중...' : '수정하기'} + + + +
+
+ ); +}; + +const Container = styled.div` + max-width: 1400px; + width: 95%; + margin: 2rem auto; + padding: 2rem; + + @media (max-width: 768px) { + width: 100%; + padding: 1rem; + } +`; + +const ContentWrapper = styled.div` + background-color: white; + border-radius: 12px; + overflow: hidden; + max-width: 1000px; + margin: 0 auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +`; + +const Header = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 3rem; + background-color: ${SEJONG_COLORS.COOL_GRAY}10; + border-bottom: 2px solid ${SEJONG_COLORS.COOL_GRAY}; + + h1 { + font-size: 2rem; + color: ${SEJONG_COLORS.CRIMSON_RED}; + margin: 0; + font-weight: 600; + } + + @media (max-width: 768px) { + padding: 1.5rem; + } +`; + +const FormSection = styled.form` + padding: 3rem; + + @media (max-width: 768px) { + padding: 1.5rem; + } +`; + +const FormGroup = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 32px; + + &:last-child { + margin-bottom: 0; + } +`; + +const FormRow = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-bottom: 32px; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +`; + +const Label = styled.label` + font-size: 1rem; + font-weight: 600; + color: ${SEJONG_COLORS.GRAY}; +`; + +const Input = styled.input` + width: 100%; + padding: 14px 18px; + border: 1px solid ${SEJONG_COLORS.COOL_GRAY}; + border-radius: 8px; + font-size: 1rem; + transition: all 0.2s ease-in-out; + color: ${SEJONG_COLORS.GRAY}; + + &:focus { + outline: none; + border-color: ${SEJONG_COLORS.CRIMSON_RED}; + box-shadow: 0 0 0 3px ${SEJONG_COLORS.CRIMSON_RED}20; + } + + &::placeholder { + color: ${SEJONG_COLORS.COOL_GRAY}; + } +`; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid ${SEJONG_COLORS.COOL_GRAY}20; +`; + +const Button = styled.button` + padding: 0.75rem 1.5rem; + font-size: 1rem; + border: 1px solid transparent; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease-in-out; + font-weight: 500; + min-width: 120px; + display: flex; + align-items: center; + justify-content: center; + + &:disabled { + cursor: not-allowed; + opacity: 0.7; + } +`; + +const CancelButton = styled(Button)` + background-color: white; + border-color: ${SEJONG_COLORS.COOL_GRAY}; + color: ${SEJONG_COLORS.GRAY}; + + &:hover:not(:disabled) { + background-color: ${SEJONG_COLORS.COOL_GRAY}10; + border-color: ${SEJONG_COLORS.GRAY}; + } + + &:active:not(:disabled) { + transform: translateY(1px); + } +`; + +const SubmitButton = styled(Button)` + background-color: ${SEJONG_COLORS.CRIMSON_RED}; + color: white; + + &:hover:not(:disabled) { + background-color: #8b0000; + box-shadow: 0 2px 4px rgba(139, 0, 0, 0.2); + } + + &:active:not(:disabled) { + transform: translateY(1px); + box-shadow: none; + } + + &:disabled { + background-color: ${SEJONG_COLORS.COOL_GRAY}; + } +`; + +export default SeminarEdit; diff --git a/frontend/src/pages/Seminar/SeminarList.tsx b/frontend/src/pages/Seminar/SeminarList.tsx index 094ef9a6..645e0242 100644 --- a/frontend/src/pages/Seminar/SeminarList.tsx +++ b/frontend/src/pages/Seminar/SeminarList.tsx @@ -4,6 +4,7 @@ import axios from 'axios'; import styled from 'styled-components'; import { AuthContext } from '../../context/AuthContext'; import { apiEndpoints } from '../../config/apiConfig'; +import { SEJONG_COLORS } from '../../constants/colors'; interface SeminarItem { id: number; @@ -180,36 +181,76 @@ const Container = styled.div` max-width: 1400px; width: 95%; margin: 0 auto; - padding: 40px 20px; + padding: 2rem; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); `; const Header = styled.div` display: flex; justify-content: space-between; align-items: center; - margin-bottom: 2rem; + margin-bottom: 2.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid ${SEJONG_COLORS.COOL_GRAY}; `; const Title = styled.h1` - font-size: 1.8rem; - color: #1a202c; + font-size: 2rem; + color: ${SEJONG_COLORS.CRIMSON_RED}; margin: 0; + font-weight: 600; +`; + +const CreateButton = styled.button` + padding: 0.75rem 1.5rem; + background-color: ${SEJONG_COLORS.CRIMSON_RED}; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + background-color: #8b0000; + box-shadow: 0 2px 4px rgba(139, 0, 0, 0.2); + } + + &:active { + transform: translateY(1px); + box-shadow: none; + } `; const Table = styled.table` width: 100%; - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; font-size: 1rem; background-color: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); `; const Th = styled.th` - padding: 1rem; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - background-color: #f8f9fa; + padding: 1.25rem 1rem; + background-color: ${SEJONG_COLORS.COOL_GRAY}20; + color: ${SEJONG_COLORS.GRAY}; font-weight: 600; text-align: center; + border-bottom: 2px solid ${SEJONG_COLORS.COOL_GRAY}; + + &:first-child { + border-top-left-radius: 8px; + } + + &:last-child { + border-top-right-radius: 8px; + } `; const SortableTh = styled(Th)<{ @@ -219,48 +260,44 @@ const SortableTh = styled(Th)<{ cursor: pointer; position: relative; padding-right: 25px; + transition: background-color 0.2s; &:after { content: '${(props) => (props.sortDirection === 'asc' ? '↑' : '↓')}'; position: absolute; right: 8px; + color: ${SEJONG_COLORS.CRIMSON_RED}; } &:hover { - background-color: #f1f3f5; + background-color: ${SEJONG_COLORS.COOL_GRAY}30; } `; const Tr = styled.tr` cursor: pointer; + transition: all 0.2s ease-in-out; + &:hover { - background-color: #f8f9fa; + background-color: ${SEJONG_COLORS.COOL_GRAY}10; } `; const Td = styled.td` - padding: 1rem; - border-bottom: 1px solid #ddd; + padding: 1.25rem 1rem; text-align: center; + border-bottom: 1px solid ${SEJONG_COLORS.COOL_GRAY}20; + color: ${SEJONG_COLORS.GRAY}; `; const TitleTd = styled(Td)` text-align: left; font-weight: 500; -`; - -const CreateButton = styled.button` - padding: 0.5rem 1rem; - background-color: #b71c1c; - color: white; - border: none; - border-radius: 4px; - font-size: 0.9rem; - cursor: pointer; - transition: background-color 0.2s; + color: ${SEJONG_COLORS.CRIMSON_RED}; &:hover { - background-color: #8b0000; + text-decoration: underline; + text-underline-offset: 2px; } `; @@ -269,16 +306,23 @@ const PaginationContainer = styled.div` justify-content: center; align-items: center; gap: 0.5rem; - margin-top: 2rem; + margin-top: 2.5rem; + padding-top: 1.5rem; + border-top: 1px solid ${SEJONG_COLORS.COOL_GRAY}20; `; const PageButton = styled.button<{ isActive?: boolean }>` padding: 0.5rem 1rem; - border: 1px solid ${(props) => (props.isActive ? '#666' : '#ddd')}; - background-color: ${(props) => (props.isActive ? '#666' : 'white')}; - color: ${(props) => (props.isActive ? 'white' : '#333')}; + border: 1px solid + ${(props) => + props.isActive ? SEJONG_COLORS.CRIMSON_RED : SEJONG_COLORS.COOL_GRAY}; + background-color: ${(props) => + props.isActive ? SEJONG_COLORS.CRIMSON_RED : 'white'}; + color: ${(props) => (props.isActive ? 'white' : SEJONG_COLORS.GRAY)}; cursor: pointer; min-width: 40px; + border-radius: 4px; + transition: all 0.2s ease-in-out; &:disabled { cursor: not-allowed; @@ -286,34 +330,44 @@ const PageButton = styled.button<{ isActive?: boolean }>` } &:hover:not(:disabled) { - background-color: ${(props) => (props.isActive ? '#555' : '#f8f9fa')}; + background-color: ${(props) => + props.isActive ? '#8b0000' : SEJONG_COLORS.COOL_GRAY}10; + border-color: ${(props) => + props.isActive ? '#8b0000' : SEJONG_COLORS.CRIMSON_RED}; + } + + &:active:not(:disabled) { + transform: translateY(1px); } `; const LoadingSpinner = styled.div` text-align: center; - padding: 2rem; - color: #666; + padding: 3rem; + color: ${SEJONG_COLORS.GRAY}; + font-size: 1.1rem; `; const ErrorMessage = styled.div` text-align: center; - padding: 1rem; - margin: 1rem 0; + padding: 1.5rem; + margin: 1.5rem 0; background-color: #fff5f5; - color: #e53e3e; - border-radius: 4px; - border: 1px solid #feb2b2; + color: ${SEJONG_COLORS.CRIMSON_RED}; + border-radius: 8px; + border: 1px solid ${SEJONG_COLORS.CRIMSON_RED}20; + font-weight: 500; `; const EmptyMessage = styled.div` text-align: center; - padding: 2rem; - color: #666; - background-color: #f8f9fa; - border: 1px solid #ddd; - border-radius: 4px; - margin-top: 1rem; + padding: 3rem; + color: ${SEJONG_COLORS.GRAY}; + background-color: ${SEJONG_COLORS.COOL_GRAY}10; + border: 1px solid ${SEJONG_COLORS.COOL_GRAY}; + border-radius: 8px; + margin-top: 1.5rem; + font-size: 1.1rem; `; export default SeminarList; From e72191ac3f3b6b5537f0f867ed7c1b9983c6337c Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 18:47:25 +0900 Subject: [PATCH 05/10] =?UTF-8?q?#235=20style:=20SeminarDetail=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Seminar/SeminarDetail.tsx | 260 ++++++++++++------- 1 file changed, 160 insertions(+), 100 deletions(-) diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx index 65f39d4b..96e7fd3b 100644 --- a/frontend/src/pages/Seminar/SeminarDetail.tsx +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -64,74 +64,93 @@ const SeminarDetail: React.FC = () => { if (!seminar) return 세미나를 찾을 수 없습니다.; return ( - - -
- {seminar.name} - {auth?.isAuthenticated && ( - - navigate(`/news/seminar/edit/${seminar.id}`)} - > - 수정 - - {}}>삭제 - - )} -
- - - - 발표자 - {seminar.speaker} - - - 소속 - {seminar.company} - - - 장소 - {seminar.place} - - - 일시 - - {formatDate(seminar.startDate)} - {seminar.startDate !== seminar.endDate && - ` ~ ${formatDate(seminar.endDate)}`} - - - - 작성자 - {seminar.writer} - - - - - navigate('/news/seminar')}> - 목록으로 - - -
-
+ + + +
+ + {seminar.name} + + + 작성자: + {seminar.writer} + + + {formatDate(seminar.startDate)} + {seminar.startDate !== seminar.endDate && + ` ~ ${formatDate(seminar.endDate)}`} + + + + {auth?.isAuthenticated && ( + + navigate(`/news/seminar/edit/${seminar.id}`)} + > + 수정 + + {}}>삭제 + + )} +
+ + + + + + 발표자 + {seminar.speaker} + + + 소속 + {seminar.company} + + + 장소 + {seminar.place} + + + + + +
+ navigate('/news/seminar')}> + 목록으로 + +
+
+
+
); }; -const Container = styled.div` - max-width: 1400px; - width: 95%; - margin: 0 auto; +const PageContainer = styled.div` + min-height: 100vh; + background-color: #fff; padding: 40px 20px; `; +const ContentWrapper = styled.div` + width: 1000px; + margin: 0 auto; + + @media (max-width: 1024px) { + width: 100%; + max-width: 1000px; + } +`; + const Card = styled.div` background: white; - border-radius: 12px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - padding: 3rem; - max-width: 1000px; + border-radius: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + padding: 3rem 4rem; margin: 0 auto; + @media (max-width: 1024px) { + padding: 2rem; + } + @media (max-width: 768px) { padding: 1.5rem; } @@ -141,16 +160,49 @@ const Header = styled.div` display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 2rem; - padding-bottom: 1rem; + margin-bottom: 3rem; + padding-bottom: 2rem; border-bottom: 2px solid #e2e8f0; `; +const TitleSection = styled.div` + flex: 1; +`; + const Title = styled.h1` - font-size: 1.8rem; - font-weight: bold; + font-size: 2.5rem; + font-weight: 700; color: #1a202c; - margin: 0; + margin: 0 0 1rem 0; + line-height: 1.2; + + @media (max-width: 768px) { + font-size: 2rem; + } +`; + +const SubInfo = styled.div` + display: flex; + gap: 2rem; + color: #64748b; + font-size: 0.95rem; +`; + +const WriterInfo = styled.div` + display: flex; + gap: 0.5rem; +`; + +const DateInfo = styled.div` + color: #64748b; +`; + +const MainContent = styled.div` + margin: 2rem 0; +`; + +const InfoSection = styled.section` + padding: 2rem 0; `; const InfoGrid = styled.div` @@ -159,87 +211,95 @@ const InfoGrid = styled.div` `; const InfoRow = styled.div` - display: grid; - grid-template-columns: 120px 1fr; - gap: 1rem; - align-items: center; - padding: 0.75rem 0; - border-bottom: 1px solid #e2e8f0; - - &:last-child { - border-bottom: none; - } + display: flex; + flex-direction: column; + gap: 0.5rem; `; const InfoLabel = styled.span` font-weight: 600; - color: #4a5568; + color: #64748b; + font-size: 0.9rem; `; const InfoValue = styled.span` - color: #2d3748; + color: #1e293b; + font-size: 1.1rem; + padding: 0.5rem 0; +`; + +const Footer = styled.div` + display: flex; + justify-content: center; + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid #e2e8f0; `; const ButtonGroup = styled.div` display: flex; - gap: 0.5rem; + gap: 0.75rem; `; const Button = styled.button` - padding: 0.5rem 1rem; - font-size: 0.9rem; - border: 1px solid #ddd; + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + border-radius: 8px; + border: 1px solid #e2e8f0; background-color: white; color: #333; cursor: pointer; transition: all 0.2s ease-in-out; - min-width: 80px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; font-weight: 500; &:hover { background-color: #f8f9fa; - border-color: #ccc; + border-color: #cbd5e1; } `; const BackButton = styled(Button)` - background-color: white; + background-color: #f8fafc; + min-width: 120px; `; const EditButton = styled(Button)` - background-color: white; + background-color: #fff; + color: #3b82f6; + border-color: #3b82f6; + + &:hover { + background-color: #eff6ff; + } `; const DeleteButton = styled(Button)` - background-color: white; - color: #dc3545; + background-color: #fff; + color: #dc2626; + border-color: #dc2626; &:hover { - background-color: #fff5f5; - border-color: #dc3545; + background-color: #fef2f2; } `; const LoadingSpinner = styled.div` text-align: center; - padding: 2rem; - color: #666; - font-size: 1.1rem; + padding: 4rem; + color: #64748b; + font-size: 1.2rem; `; const ErrorMessage = styled.div` text-align: center; - padding: 1rem; - margin: 1rem 0; - background-color: #fff5f5; - color: #e53e3e; - border-radius: 4px; + padding: 1.5rem; + margin: 2rem auto; + max-width: 600px; + background-color: #fef2f2; + color: #dc2626; + border-radius: 8px; font-size: 1rem; - border: 1px solid #feb2b2; + border: 1px solid #fecaca; `; export default SeminarDetail; From 4a0f8dba60e9c5aca62727b921388b36ce3b3474 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 18:55:35 +0900 Subject: [PATCH 06/10] =?UTF-8?q?#235=20feat:=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=A0=9C=EB=AA=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Header/Header.tsx | 8 +++- frontend/src/components/Header/HeaderStyle.ts | 19 ++++++++ .../Header/MobileMenu/MobileMenu.tsx | 39 ++++++++++++++- .../Header/MobileMenu/MobileMenuStyle.ts | 47 +++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 9d9a1082..1051df32 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,5 +1,10 @@ import React, { useState } from 'react'; -import { HeaderContainer, HeaderInner, HeaderNav } from './HeaderStyle'; +import { + HeaderContainer, + HeaderInner, + HeaderNav, + MobileTitle, +} from './HeaderStyle'; import Logo from './Logo/Logo'; import Navigation from './Navigation/Navigation'; import { useHeaderScroll } from './hooks/useHeaderScroll'; @@ -25,6 +30,7 @@ const Header: React.FC = () => { > + 세종대학교 바이오융합공학전공 {isMobile ? ( diff --git a/frontend/src/components/Header/HeaderStyle.ts b/frontend/src/components/Header/HeaderStyle.ts index a45f8234..5c840d30 100644 --- a/frontend/src/components/Header/HeaderStyle.ts +++ b/frontend/src/components/Header/HeaderStyle.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { motion } from 'framer-motion'; + export const HeaderContainer = styled(motion.header)<{ $isDropdownOpen: boolean; }>` @@ -28,6 +29,7 @@ export const HeaderInner = styled.div` display: flex; align-items: center; justify-content: space-between; + position: relative; @media (max-width: 768px) { padding: 0 1rem; @@ -48,6 +50,23 @@ export const HeaderNav = styled.nav` } `; +export const MobileTitle = styled.h1` + display: none; + position: absolute; + width: 100%; + text-align: center; + font-size: 1.1rem; + font-weight: 600; + color: white; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + + @media (max-width: 768px) { + display: block; + } +`; + export const HeaderNavList = styled.ul` display: flex; align-items: center; diff --git a/frontend/src/components/Header/MobileMenu/MobileMenu.tsx b/frontend/src/components/Header/MobileMenu/MobileMenu.tsx index 4ac8c1ea..410f0658 100644 --- a/frontend/src/components/Header/MobileMenu/MobileMenu.tsx +++ b/frontend/src/components/Header/MobileMenu/MobileMenu.tsx @@ -1,5 +1,8 @@ -import React, { useState } from 'react'; +import React, { useState, useContext } from 'react'; import { Menu, ChevronDown } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { AuthContext } from '../../../context/AuthContext'; +import { ReactComponent as UserIcon } from '../../../assets/images/user-icon.svg'; import { navItems } from '../constants'; import { MobileMenuButton, @@ -8,11 +11,17 @@ import { MobileMenuTitle, MobileSubMenu, MobileSubMenuItem, + MobileAuthSection, + MobileLoginButton, + MobileUserProfile, + MobileLogoutButton, } from './MobileMenuStyle'; const MobileMenu = () => { const [isOpen, setIsOpen] = useState(false); const [activeIndices, setActiveIndices] = useState([]); + const navigate = useNavigate(); + const auth = useContext(AuthContext); const toggleSubmenu = (index: number) => { setActiveIndices((prev) => @@ -20,6 +29,18 @@ const MobileMenu = () => { ); }; + const handleSignIn = () => { + navigate('/signin'); + setIsOpen(false); + }; + + const handleSignOut = async () => { + if (auth?.signout) { + await auth.signout(); + setIsOpen(false); + } + }; + return ( <> setIsOpen(!isOpen)}> @@ -50,6 +71,22 @@ const MobileMenu = () => { ))} + + + {auth?.isAuthenticated ? ( + <> + + + {auth.user} + + + 로그아웃 + + + ) : ( + 로그인 + )} + ); diff --git a/frontend/src/components/Header/MobileMenu/MobileMenuStyle.ts b/frontend/src/components/Header/MobileMenu/MobileMenuStyle.ts index 0942faa9..7bc7840d 100644 --- a/frontend/src/components/Header/MobileMenu/MobileMenuStyle.ts +++ b/frontend/src/components/Header/MobileMenu/MobileMenuStyle.ts @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { Link } from 'react-router-dom'; + export const MobileMenuButton = styled.button` padding: 0.5rem; background: none; @@ -21,6 +22,7 @@ export const MobileMenuButton = styled.button` stroke-width: 2px; } `; + export const MobileMenuWrapper = styled.div<{ isOpen: boolean }>` display: none; @@ -86,3 +88,48 @@ export const MobileSubMenuItem = styled(Link)` background-color: ${({ theme }) => theme.colors.primary.crimsonDark}; } `; + +export const MobileAuthSection = styled.div` + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin-top: auto; +`; + +export const MobileLoginButton = styled.button` + width: 100%; + padding: 0.8rem; + background-color: transparent; + border: 1px solid white; + color: white; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } +`; + +export const MobileUserProfile = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.8rem; + color: white; + + svg { + width: 24px; + height: 24px; + } +`; + +export const MobileLogoutButton = styled(MobileLoginButton)` + margin-top: 0.5rem; + color: #ff6b6b; + border-color: #ff6b6b; + + &:hover { + background-color: rgba(255, 107, 107, 0.1); + } +`; From 1dd4a8682f9029ae7e9b48dd65593c752969d0a3 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 19:07:17 +0900 Subject: [PATCH 07/10] =?UTF-8?q?#235=20fix:=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=A1=9C=EA=B3=A0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Header/Header.tsx | 10 +---- frontend/src/components/Header/HeaderStyle.ts | 37 ++++++++++--------- frontend/src/components/Header/Logo/Logo.tsx | 4 ++ .../src/components/Header/Logo/LogoStyle.ts | 27 ++++++++++---- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 1051df32..e1932ead 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,10 +1,5 @@ import React, { useState } from 'react'; -import { - HeaderContainer, - HeaderInner, - HeaderNav, - MobileTitle, -} from './HeaderStyle'; +import { HeaderContainer, HeaderInner, HeaderNav } from './HeaderStyle'; import Logo from './Logo/Logo'; import Navigation from './Navigation/Navigation'; import { useHeaderScroll } from './hooks/useHeaderScroll'; @@ -29,8 +24,7 @@ const Header: React.FC = () => { }} > - - 세종대학교 바이오융합공학전공 + {isMobile ? ( diff --git a/frontend/src/components/Header/HeaderStyle.ts b/frontend/src/components/Header/HeaderStyle.ts index 5c840d30..32f2bdb7 100644 --- a/frontend/src/components/Header/HeaderStyle.ts +++ b/frontend/src/components/Header/HeaderStyle.ts @@ -50,23 +50,6 @@ export const HeaderNav = styled.nav` } `; -export const MobileTitle = styled.h1` - display: none; - position: absolute; - width: 100%; - text-align: center; - font-size: 1.1rem; - font-weight: 600; - color: white; - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - - @media (max-width: 768px) { - display: block; - } -`; - export const HeaderNavList = styled.ul` display: flex; align-items: center; @@ -88,3 +71,23 @@ export const HeaderActions = styled.div` gap: 1rem; color: white; `; + +export const MobileTitle = styled.h1` + display: none; + font-size: 1.1rem; + font-weight: 600; + color: white; + margin: 0; + + @media (max-width: 768px) { + display: block; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 100%; + text-align: center; + white-space: nowrap; + pointer-events: none; // 텍스트 뒤의 요소들과 상호작용 가능하도록 + z-index: 1; // 다른 요소들 위에 표시 + } +`; diff --git a/frontend/src/components/Header/Logo/Logo.tsx b/frontend/src/components/Header/Logo/Logo.tsx index bea5d2bb..a0da3a15 100644 --- a/frontend/src/components/Header/Logo/Logo.tsx +++ b/frontend/src/components/Header/Logo/Logo.tsx @@ -7,6 +7,7 @@ import { LogoTitle, LogoWrapper, Department, + MobileLogoTitle, } from './LogoStyle'; interface LogoProps { @@ -27,6 +28,9 @@ const Logo: React.FC = ({ compact = false }) => { 바이오융합공학전공 )} + {compact && ( + 세종대학교 바이오융합공학전공 + )} diff --git a/frontend/src/components/Header/Logo/LogoStyle.ts b/frontend/src/components/Header/Logo/LogoStyle.ts index fc01c353..d3d70350 100644 --- a/frontend/src/components/Header/Logo/LogoStyle.ts +++ b/frontend/src/components/Header/Logo/LogoStyle.ts @@ -31,6 +31,7 @@ export const LogoWrapper = styled.div` flex-direction: row; align-items: center; gap: 16px; + position: relative; `; export const LogoImage = styled.div` @@ -58,14 +59,6 @@ export const LogoTitle = styled.div` letter-spacing: 0.02em; text-transform: uppercase; opacity: 0.95; - - @media (max-width: 768px) { - font-size: 0.8rem; - } - - @media (max-width: 480px) { - display: none; - } `; export const Department = styled.span` @@ -78,3 +71,21 @@ export const Department = styled.span` font-size: 1.1rem; } `; + +export const MobileLogoTitle = styled.div` + position: fixed; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 1.1rem; + font-weight: 600; + color: white; + width: 100%; + text-align: center; + pointer-events: none; + z-index: 1; + + @media (min-width: 769px) { + display: none; + } +`; From f5290c55615e8181a5735764a6e5e8e1fddcf773 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 19:07:39 +0900 Subject: [PATCH 08/10] =?UTF-8?q?#235=20feat:=20=EC=84=B8=EB=AF=B8?= =?UTF-8?q?=EB=82=98=EC=8B=A4=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Seminar/SeminarDetail.tsx | 75 ++++++++++++++++++- frontend/src/pages/Seminar/SeminarStyle.ts | 1 - .../components/SeminarTable/SeminarTable.tsx | 1 - .../SeminarTable/SeminarTableStyle.ts | 1 - .../src/pages/Seminar/types/seminar.types.ts | 1 - 5 files changed, 74 insertions(+), 5 deletions(-) delete mode 100644 frontend/src/pages/Seminar/SeminarStyle.ts delete mode 100644 frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx delete mode 100644 frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts delete mode 100644 frontend/src/pages/Seminar/types/seminar.types.ts diff --git a/frontend/src/pages/Seminar/SeminarDetail.tsx b/frontend/src/pages/Seminar/SeminarDetail.tsx index 96e7fd3b..48bf870d 100644 --- a/frontend/src/pages/Seminar/SeminarDetail.tsx +++ b/frontend/src/pages/Seminar/SeminarDetail.tsx @@ -4,6 +4,8 @@ import axios from 'axios'; import styled from 'styled-components'; import { AuthContext } from '../../context/AuthContext'; import { apiEndpoints } from '../../config/apiConfig'; +import { Modal, useModal } from '../../components/Modal'; +import { AlertTriangle, CheckCircle } from 'lucide-react'; interface SeminarDetail { id: number; @@ -20,9 +22,11 @@ const SeminarDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const auth = useContext(AuthContext); + const { openModal } = useModal(); const [seminar, setSeminar] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { const fetchSeminarDetail = async () => { @@ -59,6 +63,73 @@ const SeminarDetail: React.FC = () => { .replace('.', ''); }; + const showConfirmModal = () => { + openModal( + <> + + + 세미나 삭제 + + +

정말로 이 세미나를 삭제하시겠습니까?

+

삭제된 세미나는 복구할 수 없습니다.

+
+ + + + {isDeleting ? '삭제 중...' : '삭제'} + + + , + ); + }; + + const showResultModal = (success: boolean) => { + openModal( + <> + + {success ? ( + + ) : ( + + )} + {success ? '삭제 완료' : '삭제 실패'} + + +

+ {success + ? '세미나가 성공적으로 삭제되었습니다.' + : '세미나 삭제 중 오류가 발생했습니다.'} +

+
+ + { + if (success) { + navigate('/news/seminar'); + } + }} + /> + + , + ); + }; + + const handleDelete = async () => { + if (!id || !seminar) return; + + try { + setIsDeleting(true); + await axios.delete(apiEndpoints.seminar.delete(id)); + showResultModal(true); + } catch (error) { + console.error('Failed to delete seminar:', error); + showResultModal(false); + } finally { + setIsDeleting(false); + } + }; + if (loading) return Loading...; if (error) return {error}; if (!seminar) return 세미나를 찾을 수 없습니다.; @@ -89,7 +160,9 @@ const SeminarDetail: React.FC = () => { > 수정 - {}}>삭제 + + {isDeleting ? '삭제 중...' : '삭제'} +
)} diff --git a/frontend/src/pages/Seminar/SeminarStyle.ts b/frontend/src/pages/Seminar/SeminarStyle.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/frontend/src/pages/Seminar/SeminarStyle.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx deleted file mode 100644 index cb0ff5c3..00000000 --- a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTable.tsx +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts b/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/frontend/src/pages/Seminar/components/SeminarTable/SeminarTableStyle.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/frontend/src/pages/Seminar/types/seminar.types.ts b/frontend/src/pages/Seminar/types/seminar.types.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/frontend/src/pages/Seminar/types/seminar.types.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; From faacbcc61e86a00150676b2e9b4acf079bb42166 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 19:20:15 +0900 Subject: [PATCH 09/10] =?UTF-8?q?#235=20feat:=20SeminarList=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=EC=88=98=EC=A0=95=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Seminar/SeminarList.tsx | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/frontend/src/pages/Seminar/SeminarList.tsx b/frontend/src/pages/Seminar/SeminarList.tsx index 645e0242..56132931 100644 --- a/frontend/src/pages/Seminar/SeminarList.tsx +++ b/frontend/src/pages/Seminar/SeminarList.tsx @@ -63,6 +63,24 @@ const SeminarList = () => { } }; + const handleEdit = (e: React.MouseEvent, seminarId: number) => { + e.stopPropagation(); // 상위 요소로의 이벤트 전파 방지 + navigate(`/news/seminar/edit/${seminarId}`); + }; + + const handleDelete = async (e: React.MouseEvent, seminarId: number) => { + e.stopPropagation(); // 상위 요소로의 이벤트 전파 방지 + if (window.confirm('정말로 이 세미나를 삭제하시겠습니까?')) { + try { + await axios.delete(apiEndpoints.seminar.delete(seminarId)); + alert('세미나가 성공적으로 삭제되었습니다.'); + fetchSeminars(); // 목록 새로고침 + } catch (error) { + alert('세미나 삭제에 실패했습니다.'); + } + } + }; + const handlePageChange = (newPage: number) => { setPageInfo((prev) => ({ ...prev, @@ -115,6 +133,7 @@ const SeminarList = () => { > 날짜 + {auth?.isAuthenticated && 관리} @@ -129,6 +148,22 @@ const SeminarList = () => { {seminar.company} {seminar.place} {formatDate(seminar.startDate)} + {auth?.isAuthenticated && ( + + handleEdit(e, seminar.id)} + color="blue" + > + 수정 + + handleDelete(e, seminar.id)} + color="red" + > + 삭제 + + + )} ))} @@ -370,4 +405,37 @@ const EmptyMessage = styled.div` font-size: 1.1rem; `; +const ActionTd = styled(Td)` + padding: 0.5rem; + display: flex; + gap: 0.5rem; + justify-content: center; + align-items: center; + border-bottom: 1px solid ${SEJONG_COLORS.COOL_GRAY}20; +`; + +const ActionButton = styled.button<{ color: 'blue' | 'red' }>` + padding: 0.4rem 0.8rem; + font-size: 0.9rem; + border: 1px solid + ${(props) => + props.color === 'blue' ? '#3B82F6' : SEJONG_COLORS.CRIMSON_RED}; + background-color: white; + color: ${(props) => + props.color === 'blue' ? '#3B82F6' : SEJONG_COLORS.CRIMSON_RED}; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:hover { + background-color: ${(props) => + props.color === 'blue' ? '#3B82F6' : SEJONG_COLORS.CRIMSON_RED}; + color: white; + } + + &:active { + transform: translateY(1px); + } +`; + export default SeminarList; From 633119d20d4b38cd1a8d7659d6846299e83c1238 Mon Sep 17 00:00:00 2001 From: pillow12360 Date: Wed, 1 Jan 2025 19:53:30 +0900 Subject: [PATCH 10/10] =?UTF-8?q?#235=20feat:=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=85=BC=EB=AC=B8=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Header/Navigation/Navigation.tsx | 2 +- frontend/src/pages/Main/Main.tsx | 26 ++- frontend/src/pages/Main/MainStyle.ts | 162 +++++++++--------- 3 files changed, 108 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/Header/Navigation/Navigation.tsx b/frontend/src/components/Header/Navigation/Navigation.tsx index 4bfdbd45..91e0c59a 100644 --- a/frontend/src/components/Header/Navigation/Navigation.tsx +++ b/frontend/src/components/Header/Navigation/Navigation.tsx @@ -70,7 +70,7 @@ const navigationItems: NavigationItem[] = [ { title: '⏱ 서비스', path: '/seminar-rooms/reservation', - menuItems: [{ name: '예약 페이지', path: '/seminar-rooms/reservation' }], + menuItems: [{ name: '세미나실 예약', path: '/seminar-rooms/reservation' }], }, ]; diff --git a/frontend/src/pages/Main/Main.tsx b/frontend/src/pages/Main/Main.tsx index a10af6eb..e9115d7f 100644 --- a/frontend/src/pages/Main/Main.tsx +++ b/frontend/src/pages/Main/Main.tsx @@ -159,6 +159,10 @@ function Main(): JSX.Element { navigate(`/news/noticeboard/${id}`); }; + const handlePaperClick = (id: number) => { + navigate(`/news/thesis/${id}`); + }; + return ( {/* 연구논문 */} @@ -166,10 +170,26 @@ function Main(): JSX.Element { 연구 논문 {papers.map((paper: Paper) => ( - - 논문 이미지 + + window.open(paper.link, '_blank', 'noopener,noreferrer') + } + role="button" + tabIndex={0} + onKeyPress={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + window.open(paper.link, '_blank', 'noopener,noreferrer'); + } + }} + > + {`${paper.title}

{paper.title}

-

{paper.content}

+

{paper.author}

+

+ {paper.journal} ({paper.publicationDate}) +

))}
diff --git a/frontend/src/pages/Main/MainStyle.ts b/frontend/src/pages/Main/MainStyle.ts index bc161ebd..08abf6d7 100644 --- a/frontend/src/pages/Main/MainStyle.ts +++ b/frontend/src/pages/Main/MainStyle.ts @@ -18,21 +18,25 @@ export const PaperContainer = styled.section` display: flex; flex-direction: column; align-items: center; + width: 100%; + max-width: 1600px; + margin: 0 auto; + padding: 0 20px; `; export const TMP = styled.div` display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 40px; + width: 100%; + ${media.tablet} { - display: flex; - flex-wrap: wrap; /* 태블릿에서는 기존 flex 레이아웃 */ - justify-content: space-between; /* 태블릿에서는 4열로 가로 배치 */ + gap: 20px; } ${media.mobile} { - display: grid; /* 모바일에서는 grid 레이아웃 */ - grid-template-columns: repeat(2, 1fr); /* 2열 */ - grid-template-rows: repeat(2, auto); /* 2행 */ - justify-items: center; /* 카드 중앙 정렬 */ + gap: 15px; } `; @@ -53,89 +57,103 @@ export const Title = styled.div` `; export const Paper = styled.article` + cursor: pointer; + transition: + transform 0.2s ease-in-out, + box-shadow 0.2s ease-in-out; display: flex; flex-direction: column; align-items: center; - width: 280px; - margin: 0 40px 0 40px; + width: 320px; padding: 24px; border: solid 1px #d4d2e3; border-radius: 24px; + background-color: white; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + } img { - width: 232px; + width: 272px; height: auto; - margin-bottom: 12px; + margin-bottom: 16px; + border-radius: 12px; ${media.tablet} { - width: 180px; + width: 220px; } ${media.mobile} { - width: 120px; + width: 160px; } } - p:first-of-type { - width: 232px; + p { + width: 272px; margin: 0; margin-bottom: 8px; font-family: 'Noto Sans KR'; color: #5d5a88; - font-size: 18px; - font-weight: 700; word-break: break-all; + } + p:nth-of-type(1) { + font-size: 18px; + font-weight: 700; + line-height: 1.5; display: -webkit-box; - -webkit-line-clamp: 3; /* 표시할 최대 줄 수 */ + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; + height: 54px; + } - ${media.tablet} { - width: 180px; - } + p:nth-of-type(2) { + font-size: 16px; + font-weight: 500; + color: #7a7a7a; + } - ${media.mobile} { - width: 120px; - font-size: 16px; + p:nth-of-type(3) { + font-size: 14px; + font-weight: 400; + color: #9e9e9e; + } + + ${media.tablet} { + width: calc(50% - 30px); + max-width: 280px; + padding: 20px; + + p { + width: 220px; } } - p:last-of-type { - width: 232px; - margin: 0; - font-family: 'Noto Sans KR'; - color: #5d5a88; - font-size: 16px; - font-weight: 400; - word-break: break-all; + ${media.mobile} { + width: calc(100% - 30px); + max-width: 240px; + padding: 16px; - display: -webkit-box; - -webkit-line-clamp: 3; /* 표시할 최대 줄 수 */ - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; + p { + width: 160px; + } - ${media.tablet} { - width: 180px; + p:nth-of-type(1) { + font-size: 16px; + height: 48px; } - ${media.mobile} { - width: 120px; + p:nth-of-type(2) { font-size: 14px; } - } - ${media.tablet} { - flex: 1 0 calc(25% - 20px); /* 태블릿: 25% 너비 (4열) */ - max-width: 220px; - padding: 20px; - } - - ${media.mobile} { - max-width: 160px; /* 모바일에서 카드 크기 축소 */ - padding: 18px; + p:nth-of-type(3) { + font-size: 12px; + } } `; @@ -156,7 +174,6 @@ export const AnnouncementAndSeminar = styled.section` width: 90%; margin-right: 100px; font-family: 'Noto Sans KR'; - display: flex; flex-direction: column; @@ -230,15 +247,15 @@ export const AnnouncementItem = styled.div` span:first-of-type { flex-shrink: 1; - flex-basis: 100%; /* 컨테이너 내부 공간의 100%를 차지 */ - max-width: calc(100% - 80px); /* 오른쪽 날짜 공간을 확보 */ + flex-basis: 100%; + max-width: calc(100% - 80px); display: -webkit-box; - -webkit-line-clamp: 1; // 표시할 최대 줄 수 + -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; - white-space: normal; // 말줄임표 작동을 위해 normal로 설정 + white-space: normal; } span:last-of-type { @@ -251,7 +268,6 @@ export const SeminarContainer = styled.div` display: flex; align-items: flex-end; - // 세미나 정보 button:first-of-type { height: 200px; flex: 1; @@ -261,7 +277,6 @@ export const SeminarContainer = styled.div` margin-right: 24px; padding: 0 30px; background-color: ${token.SEJONG_COLORS.WARM_GRAY1}; - border-radius: 0; border: none; color: white; @@ -327,7 +342,6 @@ export const SeminarContainer = styled.div` ${media.mobile} { height: 148px; margin-right: 8px; - padding: 0; flex: 1; } @@ -338,7 +352,6 @@ export const SeminarContainer = styled.div` } `; -// 세미나실 예약 export const SeminarRoomReservation = styled(Link)` height: 200px; flex: 1; @@ -347,10 +360,13 @@ export const SeminarRoomReservation = styled(Link)` color: white; font-family: 'Noto Sans KR'; cursor: pointer; - - p { - margin: 16px 0 16px 0; - } + display: flex; + align-items: center; + justify-content: center; + padding: 0 24px 0 24px; + font-size: 22px; + background-color: ${token.SEJONG_COLORS.WARM_GRAY1}; + text-decoration: none; span { margin-right: 20px; @@ -361,14 +377,6 @@ export const SeminarRoomReservation = styled(Link)` } } - display: flex; - align-items: center; - justify-content: center; - padding: 0 24px 0 24px; - font-size: 22px; - background-color: ${token.SEJONG_COLORS.WARM_GRAY1}; - text-decoration: none; - ${media.tablet} { height: 180px; font-size: 20px; @@ -388,12 +396,10 @@ export const ShortcutContainer = styled.section` flex: 45%; display: grid; justify-items: center; - grid-template-rows: repeat(3, auto); /* 3개의 행 */ - grid-template-columns: repeat(2, 1fr); /* 2개의 열 */ - gap: 50px 0; /* 요소들 사이의 간격 설정 */ + grid-template-rows: repeat(3, auto); + grid-template-columns: repeat(2, 1fr); + gap: 50px 0; padding: 95px 0 95px 0; - - /* background: linear-gradient(135deg, #d1f1ff 0%, #d1f1ff 50%, #71c9ff0a 100%); */ background-color: #e9dfda; a { @@ -441,7 +447,7 @@ export const Shortcut = styled.div` font-size: 20px; } - ${media.tablet} { + ${media.mobile} { font-size: 18px; } `;