diff --git a/src/App.js b/src/App.js index a70d38d..12898e3 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,8 @@ import Partnership from "./pages/partnership/Parnership"; import PartnershipWrite from "./pages/partnership/PartnershipWrite"; import Data from "./pages/data/Data"; import DataWrite from "./pages/data/DataWrite"; +import LinkHub from "./pages/linkhub/linkHub"; +import LinkHubWrite from "./pages/linkhub/LinkHubWrite"; function App() { return ( @@ -48,6 +50,8 @@ const Content = () => { } /> } /> } /> + } /> + } /> ); diff --git a/src/components/menu/menu.js b/src/components/menu/menu.js index 8541e78..a9ac796 100644 --- a/src/components/menu/menu.js +++ b/src/components/menu/menu.js @@ -16,7 +16,7 @@ const Menu = () => {
  • Q&A
  • 100인 안건 상정제
  • 세칙 및 회칙
  • -
  • 제휴백과
  • +
  • Linkhub
  • ); diff --git a/src/pages/linkhub/LinkHubWrite.js b/src/pages/linkhub/LinkHubWrite.js new file mode 100644 index 0000000..f39b8e6 --- /dev/null +++ b/src/pages/linkhub/LinkHubWrite.js @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; + +import "./linkHub.css"; + +const apiClient = axios.create({ + baseURL: process.env.REACT_APP_API_URL || 'https://api.ajouchong.com' +}); + +const LinkHubWrite = () => { + const [title, setTitle] = useState(''); + const [link, setLink] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!title.trim() || !link.trim()) { + alert("제목과 링크를 입력해주세요."); + return; + } + + try { + new URL(link); + } catch { + alert("올바른 URL 형식을 입력해주세요."); + return; + } + + setLoading(true); + setError(null); + + try { + const requestData = { + title: title, + link: link + }; + + const response = await apiClient.post(`/api/admin/link/upload`, requestData, { + withCredentials: true, + }); + + if (response.data.code === 1) { + alert("링크가 성공적으로 등록되었습니다!"); + navigate("/linkHub"); + } else { + alert("링크 등록에 실패했습니다."); + } + } catch (error) { + setError("링크 등록 중 오류가 발생했습니다."); + console.error("링크 등록 실패:", error); + alert("링크 등록 중 오류가 발생했습니다."); + } finally { + setLoading(false); + } + }; + + return ( +
    +

    링크 등록

    + {error &&

    {error}

    } +
    +
    + + setTitle(e.target.value)} + placeholder="링크 제목을 입력하세요" + required + /> +
    + +
    + + setLink(e.target.value)} + placeholder="https://example.com" + required + /> +
    + +
    + + +
    +
    +
    + ); +}; + +export default LinkHubWrite; diff --git a/src/pages/linkhub/linkHub.css b/src/pages/linkhub/linkHub.css new file mode 100644 index 0000000..5ced586 --- /dev/null +++ b/src/pages/linkhub/linkHub.css @@ -0,0 +1,314 @@ +.admin-container { + width: 90%; + margin: 20px auto; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.admin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.admin-title { + font-weight: bold; + color: #333; +} + +.write-btn { + background: #4CAF50; + color: white; + border: none; + padding: 8px 15px; + cursor: pointer; + border-radius: 5px; + font-size: 14px; + transition: 0.3s; +} + +.write-btn:hover { + background: #0056b3; +} + +.admin-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.admin-table th { + background: #f5f5f5; + font-weight: bold; + padding: 12px; + text-align: center; + border: 1px solid #ddd; +} + +.admin-table td { + padding: 10px; + text-align: center; + border: 1px solid #ddd; +} + +.admin-table tbody tr:nth-child(even) { + background: #f9f9f9; +} + +.admin-table tbody tr:hover { + background: #f0f8ff; + cursor: pointer; +} + +.delete-btn { + background: #dc3545; + color: white; + border: none; + padding: 6px 12px; + cursor: pointer; + border-radius: 5px; + transition: 0.3s; +} + +.delete-btn:hover { + background: #b22222; +} + +.pagination { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 15px; +} + +.pagination button { + padding: 6px 12px; + border: none; + cursor: pointer; + border-radius: 4px; + background: #4CAF50; + color: white; +} + +.pagination button:disabled { + background: #ccc; + cursor: not-allowed; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 20px; + border-radius: 10px; + width: 500px; + max-width: 90%; + text-align: left; + position: relative; + max-height: 80vh; + overflow-y: auto; +} + +.modal-content h2 { + margin-top: 0; + color: #333; + border-bottom: 2px solid #4CAF50; + padding-bottom: 10px; +} + +.modal-content p { + margin: 10px 0; + line-height: 1.5; +} + +.close-btn { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; +} + +.close-btn:hover { + color: #333; +} + +.link-url { + color: #0066cc; + text-decoration: none; + word-break: break-all; + max-width: 200px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.link-url:hover { + text-decoration: underline; +} + +.modal-link { + color: #0066cc; + text-decoration: none; + word-break: break-all; + display: block; + max-width: 100%; + white-space: normal; + margin: 5px 0; + padding: 5px; + background: #f9f9f9; + border-radius: 3px; +} + +.modal-link:hover { + text-decoration: underline; + background: #f0f0f0; +} + +.error-message { + color: #dc3545; + background: #f8d7da; + border: 1px solid #f5c6cb; + padding: 10px; + border-radius: 5px; + margin-bottom: 15px; + text-align: center; +} + +/* ===== LinkHub Write Page Styles ===== */ +.link-write-container { + width: 90%; + max-width: 600px; + margin: 20px auto; + padding: 30px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background: white; +} + +.link-write-container h2 { + text-align: center; + margin-bottom: 30px; + color: #333; + border-bottom: 2px solid #4CAF50; + padding-bottom: 10px; +} + +.link-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; +} + +.form-group label { + font-weight: bold; + margin-bottom: 8px; + color: #333; +} + +.form-group input { + padding: 12px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 14px; + transition: border-color 0.3s; +} + +.form-group input:focus { + outline: none; + border-color: #4CAF50; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); +} + +.form-group input[type="url"] { + font-family: monospace; +} + +.form-actions { + display: flex; + gap: 15px; + justify-content: center; + margin-top: 20px; +} + +.submit-btn { + background: #4CAF50; + color: white; + border: none; + padding: 12px 30px; + border-radius: 5px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; + min-width: 120px; +} + +.submit-btn:hover:not(:disabled) { + background: #45a049; +} + +.submit-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.cancel-btn { + background: #6c757d; + color: white; + border: none; + padding: 12px 30px; + border-radius: 5px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; + min-width: 120px; +} + +.cancel-btn:hover:not(:disabled) { + background: #5a6268; +} + +.cancel-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .link-write-container { + width: 95%; + padding: 20px; + margin: 10px auto; + } + + .form-actions { + flex-direction: column; + } + + .submit-btn, + .cancel-btn { + width: 100%; + } +} diff --git a/src/pages/linkhub/linkHub.js b/src/pages/linkhub/linkHub.js new file mode 100644 index 0000000..eee319e --- /dev/null +++ b/src/pages/linkhub/linkHub.js @@ -0,0 +1,209 @@ +import React, { useEffect, useState, useCallback } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + +import "./linkHub.css"; + +const apiClient = axios.create({ + baseURL: process.env.REACT_APP_API_URL || 'https://api.ajouchong.com' +}); + +const LinkHub = () => { + const [links, setLinks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const linksPerPage = 10; + + const [showModal, setShowModal] = useState(false); + const [selectedLink, setSelectedLink] = useState(null); + + const navigate = useNavigate(); + + const fetchLinks = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await apiClient.get(`/api/admin/link`, { + withCredentials: true, + }); + + console.log("Link response:", response.data); + + if (response.data.code === 1) { + setLinks(response.data.data || []); + } else { + console.error('Error fetching links:', response.data.message); + setError('링크 목록을 불러오는데 실패했습니다.'); + } + } catch (error) { + console.error("링크 불러오기 실패:", error); + setError('링크 목록을 불러오는 중 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchLinks(); + }, [fetchLinks]); + + const deleteLink = async (id) => { + if (!window.confirm(`${id}번 링크를 삭제하시겠습니까?`)) return; + + setLoading(true); + try { + const response = await apiClient.delete(`/api/admin/link/${id}/delete`, { + withCredentials: true, + }); + + if (response.data.code === 1) { + alert(`${id}번 링크 삭제 성공!`); + fetchLinks(); + } else { + console.error("Error deleting link:", response.data.message); + alert("링크 삭제에 실패했습니다."); + } + } catch (error) { + console.error("삭제 실패:", error); + alert("링크 삭제 중 오류가 발생했습니다."); + } finally { + setLoading(false); + } + }; + + const openModal = (link) => { + setSelectedLink(link); + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + setSelectedLink(null); + }; + + const handleOverlayClick = (e) => { + if (e.target.classList.contains("modal-overlay")) { + closeModal(); + } + }; + + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === "Escape") { + closeModal(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, []); + + const indexOfLastLink = currentPage * linksPerPage; + const indexOfFirstLink = indexOfLastLink - linksPerPage; + const currentLinks = links.slice(indexOfFirstLink, indexOfLastLink); + const totalPages = Math.ceil(links.length / linksPerPage); + + const handlePageChange = (newPage) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage); + } + }; + + return ( +
    +
    +

    {links.length}개의 링크가 있습니다.

    + +
    + + {error &&

    {error}

    } + + + + + + + + + + + + {loading ? ( + + + + ) : currentLinks.length > 0 ? ( + currentLinks.map((link) => ( + openModal(link)}> + + + + + + )) + ) : ( + + + + )} + +
    ID링크 제목URL관리
    로딩 중...
    {link.id}{link.title} + e.stopPropagation()} + className="link-url" + > + {link.link} + + + +
    등록된 링크가 없습니다.
    + +
    + + {currentPage} / {totalPages} + +
    + + {showModal && selectedLink && ( +
    +
    + +

    {selectedLink.title}

    +

    URL:

    +

    + + {selectedLink.link} + +

    +

    생성일:

    +

    {new Date(selectedLink.createdAt).toLocaleString('ko-KR')}

    +
    +
    + )} +
    + ); +}; + +export default LinkHub;