diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/css/BestItems.jsx b/css/BestItems.jsx new file mode 100644 index 00000000..c932ba08 --- /dev/null +++ b/css/BestItems.jsx @@ -0,0 +1,101 @@ +import React, { useState, useEffect } from 'react'; +import { getProductList } from './ProductService'; +import { useResponsive } from '../hooks/useResponsive'; + +const BestItems = () => { + const [bestProducts, setBestProducts] = useState([]); + const [loading, setLoading] = useState(false); + + // 커스텀 훅 사용 - 반응형 pageSize 자동 관리 + const pageSize = useResponsive('best'); + + // 베스트 상품 로드 (pageSize 변경시 자동 재로드) + useEffect(() => { + const loadBestProducts = async () => { + setLoading(true); + + try { + const data = await getProductList(1, pageSize, '', 'favorite'); + setBestProducts(data.list || []); + } catch (err) { + console.error('베스트 상품 조회 실패:', err); + setBestProducts([]); + } finally { + setLoading(false); + } + }; + + loadBestProducts(); + }, [pageSize]); + + // 가격 포맷 함수 + const formatPrice = (price) => { + if (!price) return '0원'; + return new Intl.NumberFormat('ko-KR').format(price) + '원'; + }; + + // 날짜 포맷 함수 + const formatDate = (dateString) => { + if (!dateString) return ''; + const date = new Date(dateString); + const now = new Date(); + const diff = now - date; + const hours = Math.floor(diff / (1000 * 60 * 60)); + + if (hours < 24) { + return hours < 1 ? '방금 전' : `${hours}시간 전`; + } + const days = Math.floor(hours / 24); + return `${days}일 전`; + }; + + // 상품 카드 생성 + const createProductCard = (product) => { + const imageUrl = + product.images && product.images.length > 0 + ? product.images[0] + : 'https://via.placeholder.com/200x200?text=No+Image'; + + return ( +
+
+ {product.name +
+
+

{product.name || '제목 없음'}

+

{formatPrice(product.price)}

+
+ ♥ {product.favoriteCount || 0} + {formatDate(product.createdAt)} +
+
+
+ ); + }; + + // 베스트 상품 렌더링 + const renderBestProducts = () => { + if (loading) { + return
베스트 상품을 불러오고 있습니다...
; + } + + return ( +
+ {bestProducts.map((product) => createProductCard(product))} +
+ ); + }; + + return ( +
+
+

베스트 상품

+
+ {renderBestProducts()} +
+
+
+ ); +}; + +export default BestItems; \ No newline at end of file diff --git a/css/global.css b/css/global.css new file mode 100644 index 00000000..3d270999 --- /dev/null +++ b/css/global.css @@ -0,0 +1,459 @@ +/* Global CSS - 모든 페이지 공통 스타일 */ + +/* 기본 설정 */ +body { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.5; + color: #333; + background: #fff; +} + +/* 상단 메뉴바 - 모든 페이지 공통 */ +.header-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 70px; + background: #FFFFFF; + border-bottom: 1px solid #DFDFDF; + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} + +.nav-container { + width: 100%; + max-width: 1120px; + display: flex; + align-items: center; + padding: 0 200px; +} + +/* 로고 */ +.brand-logo { + width: 200px; + height: 67px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + margin-right: 40px; +} + +.brand-logo img { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* 네비게이션 메뉴들 */ +.nav-menu-group { + display: flex; + gap: 60px; + align-items: center; + margin-left: 20px; /* 로고 다음 간격 (추가) */ +} + +.board-link, .market-link { + color: #374151; + text-decoration: none; + font-size: 16px; + font-weight: 500; + transition: color 0.2s; +} + +.board-link:hover, .market-link:hover { + color: #3B82F6; +} + +/* 로그인 버튼을 오른쪽 끝으로 */ +.nav-right { + margin-left: auto; +} + +.login-button { + background: #3B82F6; + color: white; + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + text-decoration: none; + transition: background-color 0.2s; +} + +.login-button:hover { + background: #2563EB; +} + +/* 페이지 레이아웃 공통 */ +.page-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.main-content { + flex: 1; + margin-top: 70px; + padding-top: 60px; +} + +/* 중앙 정렬 컨테이너 */ +.center-container { + display: flex; + justify-content: center; + align-items: flex-start; +} + +/* 폼 공통 스타일 */ +.form-container { + width: 640px; + background: transparent; + display: flex; + flex-direction: column; + margin: 0 auto; +} + +.form-logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 28px; +} + +.form-logo img { + width: 396px; + height: 132px; + object-fit: contain; +} + +.input-group { + margin: 14px 0; +} + +.input-label { + font-size: 13px; + font-weight: 700; + color: #374151; + margin-bottom: 6px; + display: block; +} + +.text-input { + width: 100%; + height: 48px; + padding: 0 16px; + font-size: 16px; + border: 1px solid #E5E7EB; + border-radius: 12px; + background: #F3F4F6; + box-sizing: border-box; +} + +.text-input:focus { + outline: none; + border-color: #3692FF; + background: #fff; + box-shadow: 0 0 0 3px rgba(54,146,255,.12); +} + +.text-input::placeholder { + color: #9CA3AF; +} + +/* 비밀번호 필드 공통 */ +.password-container { + position: relative; +} + +.password-toggle-button { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + border: none; + background: #E5E7EB; + color: #6B7280; + border-radius: 8px; + cursor: pointer; +} + +/* 버튼 공통 스타일 */ +.submit-button { + width: 100%; + height: 48px; + border: none; + border-radius: 9999px; + font-size: 16px; + font-weight: 700; + color: #fff; + background: #9CA3AF; + cursor: not-allowed; + margin-top: 10px; +} + +.submit-button.active { + background: #3692FF; + cursor: pointer; +} + +/* 소셜 로그인 바 공통 */ +.social-login-bar { + display: flex; + align-items: center; + justify-content: space-between; + background: #EAF3FF; + border-radius: 12px; + padding: 12px 16px; + margin-top: 16px; +} + +.social-login-text { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.social-icons-container { + display: flex; + gap: 12px; +} + +.social-icon-link { + width: 44px; + height: 44px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 16px; + text-decoration: none; + border: 1px solid #D1D5DB; + background: #fff; +} + +.social-icon-link.google { + color: #4285F4; +} + +.social-icon-link.kakao { + background: #FEE500; + color: #3C1E1E; + border: none; +} + +/* 하단 링크 공통 */ +.bottom-navigation-link { + text-align: center; + margin-top: 12px; + font-size: 13px; + color: #6B7280; +} + +.bottom-navigation-link a { + color: #3692FF; + text-decoration: none; + font-weight: 700; +} + +/* 빈 페이지 공통 스타일 */ +.empty-page-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 50px 20px; +} + +.empty-page-content { + text-align: center; + max-width: 600px; +} + +.empty-page-content h1 { + font-size: 48px; + font-weight: 700; + color: #1F2937; + margin-bottom: 20px; +} + +.empty-page-content p { + font-size: 18px; + color: #6B7280; + margin-bottom: 30px; +} + +.back-to-home-button { + background: #3B82F6; + color: white; + padding: 16px 32px; + border: none; + border-radius: 12px; + font-size: 18px; + font-weight: 600; + cursor: pointer; + text-decoration: none; + display: inline-block; + transition: transform 0.2s, box-shadow 0.2s; + box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3); +} + +.back-to-home-button:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(59, 130, 246, 0.4); +} + +/* 모달 스타일 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; +} + +.modal-container { + background: white; + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + max-width: 400px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +.modal-content { + padding: 32px 24px 24px; + text-align: center; +} + +.modal-message { + font-size: 16px; + color: #374151; + line-height: 1.5; + margin-bottom: 24px; + word-break: keep-all; +} + +.modal-close-button { + background: #3B82F6; + color: white; + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; + min-width: 80px; +} + +.modal-close-button:hover { + background: #2563EB; +} + +/* 폼 입력 에러 스타일 */ +.text-input.error { + border-color: #DC2626 !important; + background: #FEF2F2; +} + +.error-message { + color: #DC2626; + font-size: 12px; + font-weight: 400; + margin-top: 4px; + margin-bottom: 8px; +} + +/* 클릭 가능한 요소들 */ +.brand-logo, +.bottom-navigation-link a, +.social-icon-link, +.submit-button.active, +.back-to-home-button { + cursor: pointer; +} + +/* 반응형 디자인 */ + +/* PC: 1200px 이상 */ +@media (min-width: 1200px) { + .nav-container { + padding: 0 200px; + } +} + +/* Tablet: 744px ~ 1199px */ +@media (min-width: 744px) and (max-width: 1199px) { + .nav-container { + padding: 0 24px; + } + + .brand-logo { + margin-right: 20px; + } + + .nav-menu-group { + gap: 16px; + } +} + +/* Mobile: 375px ~ 743px */ +@media (min-width: 375px) and (max-width: 743px) { + .nav-container { + padding: 0 16px; + } + + .brand-logo { + width: 120px; + height: 40px; + margin-right: 16px; + } + + .nav-menu-group { + gap: 12px; + margin-left: 12px; /* 모바일에서도 약간 간격 */ + } + + .board-link, .market-link { + font-size: 14px; + } + + .login-button { + font-size: 14px; + padding: 8px 16px; + } + + /* 모바일에서 폼 컨테이너 반응형 */ + .form-container { + width: calc(100% - 32px); + max-width: 400px; + padding: 0 16px; + } + + .form-logo img { + width: 100%; + max-width: 300px; + height: auto; + } +} + +/* ===== 피드백 반영: Active 상태 공통 클래스 ===== */ +.board-link.active, +.market-link.active { + color: #3B82F6; +} \ No newline at end of file diff --git a/css/pages/index.css b/css/pages/index.css new file mode 100644 index 00000000..a978c730 --- /dev/null +++ b/css/pages/index.css @@ -0,0 +1,271 @@ +/* ===== NAV: 전역 플렉스 흐름을 유지하고 index만 패딩/정렬 살짝 조정 ===== */ +.nav-container { + display: flex !important; + align-items: center !important; + justify-content: flex-start !important; /* 왼쪽부터 흐르도록 */ + padding: 0 100px !important; /* index는 100px (원본 유지) */ + gap: 0 !important; +} +.brand-logo, +.nav-menu-group, +.nav-right { + position: static !important; + left: auto !important; + right: auto !important; + transform: none !important; +} +.nav-menu-group { + display: flex !important; + align-items: center !important; + gap: 24px !important; + margin-left: 20px !important; /* 로고 ↔ 메뉴 간격 */ +} +.nav-right { + margin-left: auto !important; /* 로그인 버튼을 오른쪽 끝으로 */ +} +.board-link, +.market-link { + color: #374151 !important; + text-decoration: none !important; + font-size: 16px !important; + font-weight: 500 !important; + transition: color 0.2s !important; +} +.board-link:hover, +.market-link:hover { + color: #3b82f6 !important; +} + +/* ===== 메인 섹션들 ===== */ +.hero-section { + background: #bfdbfe; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + margin-top: 70px; /* 헤더 높이 보정 */ +} +.hero-content { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.hero-image-link { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.2s, opacity 0.2s; +} +.hero-image-link:hover { + transform: scale(1.02); + opacity: 0.9; +} +.hero-image-link img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.content-section { + padding: 138px 0; + max-width: 1200px; + margin: 0 auto; + background: #ffffff; +} +.section-inner { + display: flex; + align-items: center; + justify-content: center; + padding: 0 32px; +} +.section-image { + width: 100%; + max-width: 988px; + height: 444px; + display: flex; + align-items: center; + justify-content: center; +} + +.trust-section { + background: #bfdbfe; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} +.trust-section .section-image { + width: 100%; + max-width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.trust-section .section-image img { + width: 100%; + height: 100%; + object-fit: contain; + max-width: none; + max-height: none; +} + +/* 푸터 */ +.footer { + background: #111827; + padding: 32px 0; +} +.footer-container { + max-width: 1920px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 200px; +} +.footer-text, +.footer-links a { + color: #9ca3af; + font-size: 16px; + text-decoration: none; + transition: color 0.2s; +} +.footer-links { + display: flex; + gap: 30px; +} +.footer-links a:hover { + color: #d1d5db; +} +.footer-social-icons { + display: flex; + gap: 12px; +} +.footer-social-button { + display: flex; + width: 40px; + height: 40px; + border-radius: 50%; + background: #374151; + align-items: center; + justify-content: center; + color: #9ca3af; + text-decoration: none; + font-size: 20px; + transition: background-color 0.2s, color 0.2s; +} +.footer-social-button:hover { + background: #4b5563; + color: #d1d5db; +} + +/* 이미지 공통 */ +img { + width: 100%; + height: 100%; + object-fit: contain; +} + +/* ===== 반응형 ===== */ +@media (min-width: 1200px) { + .hero-section, + .trust-section { + background: #bfdbfe; + width: 100%; + } + .footer-container { + padding: 0 200px; + } + .content-section { + padding: 138px 32px; + width: 100%; + } + .section-image img { + width: 988px; + height: 444px; + } +} + +@media (min-width: 744px) and (max-width: 1199px) { + .nav-container { + padding: 0 24px !important; + } + .footer-container { + padding: 0 24px; + } + .content-section { + padding: 100px 24px; + } + .section-image img { + width: 988px; + height: 444px; + max-width: 100%; + } +} + +@media (min-width: 375px) and (max-width: 743px) { + .nav-container { + padding: 0 16px !important; + } + .brand-logo { + width: 120px !important; + height: 40px !important; + } + .nav-menu-group { + gap: 12px !important; + margin-left: 12px !important; + } + .board-link, + .market-link { + font-size: 14px !important; + } + .login-button { + font-size: 14px !important; + padding: 8px 16px !important; + } + + .footer-container { + padding: 0 16px; + flex-direction: column; + gap: 20px; + text-align: center; + } + .footer-links { + order: -1; + } + + .content-section { + padding: 60px 32px; + } + .hero-section { + height: 300px; + } + .trust-section { + height: 300px; + } + .section-image { + width: 100%; + height: auto; + padding: 0; + } + .section-image img { + width: 100%; + height: auto; + object-fit: contain; + } + .footer-text { + order: 1; + } + .footer-social-icons { + order: 2; + } +} \ No newline at end of file diff --git a/css/pages/market.css b/css/pages/market.css new file mode 100644 index 00000000..eb1d22c2 --- /dev/null +++ b/css/pages/market.css @@ -0,0 +1,477 @@ +/* 상단 메뉴바 스타일 + 로고 → 메뉴(자유게시판/중고마켓) → 로그인 순서로 배치 */ + +/* 기본 네비게이션 컨테이너 */ +.nav-container { + display: flex !important; + align-items: center !important; + justify-content: flex-start !important; /* 왼쪽부터 시작 */ + padding: 0 100px !important; /* HTML 인라인 스타일에서 가져온 값 */ + gap: 0 !important; +} + +/* 위치 고정 안함 (자연스럽게 흐름에 따라 배치) */ +.brand-logo, +.nav-menu-group, +.nav-right { + position: static !important; + left: auto !important; + right: auto !important; + transform: none !important; + z-index: auto !important; +} + +/* 메뉴(자유게시판/중고마켓) 부분 - HTML 인라인 스타일 우선순위 적용 */ +html body .header-nav .nav-container .nav-menu-group { + display: flex !important; + align-items: center !important; + gap: 24px !important; /* 24px로 변경 */ + margin-left: 20px !important; /* 로고와 간격 */ +} + +/* 더 강력한 셀렉터로 간격 강제 적용 */ +html body nav.header-nav div.nav-container div.nav-menu-group { + gap: 24px !important; /* 24px로 변경 */ +} + +/* 로그인 버튼을 맨 오른쪽으로 - HTML 인라인 스타일 우선순위 적용 */ +html body .header-nav .nav-right { + margin-left: auto !important; +} + +/* 메뉴 링크 스타일 - HTML 인라인 스타일 우선순위 적용 */ +html body .header-nav .board-link, +html body .header-nav .market-link { + color: #374151 !important; + text-decoration: none !important; + font-size: 16px !important; + font-weight: 500 !important; + transition: color 0.2s !important; +} + +/* 중고마켓 active 상태만 검은색 유지 - HTML 인라인 스타일에서 가져옴 */ +html body .header-nav .market-link.active { + color: #374151 !important; +} + +/* 호버 시에도 중고마켓은 검은색 유지 */ +html body .header-nav .market-link:hover { + color: #374151 !important; +} + +/* 자유게시판만 호버 시 파란색 */ +html body .header-nav .board-link:hover { + color: #3b82f6 !important; +} + +/* 본문 영역 */ +.main-content { + margin-top: 70px; + padding: 40px 0; + min-height: calc(100vh - 70px); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.section-title { + font-size: 24px; + font-weight: 700; + margin-bottom: 24px; + color: #1f2937; + text-align: left; +} + +/* 베스트 상품 영역 */ +.best-products-section { + margin-bottom: 80px; +} + +.best-products-grid { + display: grid; + gap: 24px; + margin-bottom: 40px; +} + +/* 전체 상품 영역 */ +.all-products-section .products-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 32px; + gap: 20px; +} + +.products-controls { + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; +} + +/* 검색창 */ +.search-container { + display: flex; + align-items: center; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 4px; + min-width: 280px; +} + +.search-input { + border: none; + background: transparent; + padding: 8px 12px; + outline: none; + flex: 1; + font-size: 14px; +} + +.search-button { + background: #3b82f6; + color: white; + border: none; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} +.search-button:hover { + background: #2563eb; +} + +/* 정렬 선택 드롭다운 */ +.sort-select { + padding: 10px 14px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: white; + outline: none; + font-size: 14px; + cursor: pointer; + min-width: 120px; +} + +.products-grid { + display: grid; + gap: 24px; + margin-bottom: 40px; +} + +/* 상품 카드 디자인 */ +.product-card { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; + height: fit-content; +} +.product-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* 상품 이미지 */ +.product-image { + width: 100%; + height: 200px; + overflow: hidden; + background: #f9fafb; +} +.product-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.2s; +} +.product-card:hover .product-image img { + transform: scale(1.05); +} + +/* 상품 정보 */ +.product-info { + padding: 16px; +} + +.product-name { + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #1f2937; +} + +.product-price { + font-size: 18px; + font-weight: 700; + color: #1f2937; + margin-bottom: 12px; +} + +.product-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #6b7280; +} +.product-favorite { + color: #ef4444; + font-weight: 500; +} +.product-date { + color: #9ca3af; +} + +/* 페이지 번호 버튼들 */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin: 40px 0; +} + +.pagination-btn { + padding: 10px 16px; + border: 1px solid #e5e7eb; + background: white; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; + font-size: 14px; + color: #374151; +} +.pagination-btn:hover:not(:disabled) { + background: #f3f4f6; + border-color: #d1d5db; +} +.pagination-btn.active { + background: #3b82f6; + color: white; + border-color: #3b82f6; +} +.pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f9fafb; +} + +/* 로딩 중, 에러, 상품 없음 메시지 */ +.loading, +.error, +.no-products { + text-align: center; + padding: 60px 20px; + font-size: 16px; + color: #6b7280; + background: #f9fafb; + border-radius: 12px; + margin: 20px 0; +} +.error { + color: #ef4444; + background: #fef2f2; + border: 1px solid #fecaca; +} +.no-products { + color: #9ca3af; +} + +/* 하단 푸터 */ +.footer { + background: #111827; + padding: 32px 0; + margin-top: 80px; +} +.footer-container { + max-width: 1920px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 200px; +} +.footer-text, +.footer-links a { + color: #9ca3af; + font-size: 16px; + text-decoration: none; + transition: color 0.2s; +} +.footer-links { + display: flex; + gap: 30px; +} +.footer-links a:hover { + color: #d1d5db; +} +.footer-social-icons { + display: flex; + gap: 12px; +} +.footer-social-button { + display: flex; + width: 40px; + height: 40px; + border-radius: 50%; + background: #374151; + align-items: center; + justify-content: center; + color: #9ca3af; + text-decoration: none; + font-size: 20px; + transition: background-color 0.2s, color 0.2s; +} +.footer-social-button:hover { + background: #4b5563; + color: #d1d5db; +} + +/* 반응형 디자인 */ +/* PC 화면 (1200px 이상) */ +@media (min-width: 1200px) { + .best-products-grid { + grid-template-columns: repeat(4, 1fr); + } + .products-grid { + grid-template-columns: repeat(5, 1fr); + } +} + +/* 태블릿 화면 (744px ~ 1199px) */ +@media (min-width: 744px) and (max-width: 1199px) { + .nav-container { + padding: 0 24px !important; + } + + .container { + padding: 0 24px; + } + .best-products-grid { + grid-template-columns: repeat(2, 1fr); + } + .products-grid { + grid-template-columns: repeat(3, 1fr); + } + + .products-header { + flex-direction: column; + align-items: stretch; + gap: 16px; + } + .products-controls { + justify-content: space-between; + } + .search-container { + flex: 1; + min-width: 200px; + } + + .footer-container { + padding: 0 24px; + } +} + +/* 모바일 화면 (743px 이하) */ +@media (max-width: 743px) { + .nav-container { + padding: 0 16px !important; + } + .brand-logo { + width: 120px !important; + height: 40px !important; + margin-right: 16px !important; + } + + html body .header-nav .board-link, + html body .header-nav .market-link { + font-size: 14px !important; + } + + .login-button { + font-size: 14px !important; + padding: 8px 16px !important; + } + + .container { + padding: 0 16px; + } + .section-title { + font-size: 20px; + } + + .products-header { + flex-direction: column; + align-items: stretch; + gap: 16px; + } + .products-controls { + flex-direction: column; + gap: 12px; + } + .search-container { + min-width: auto; + } + .search-input { + font-size: 16px; + } + .sort-select { + width: 100%; + } + + .best-products-grid { + grid-template-columns: 1fr; + } + .products-grid { + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } + + .product-image { + height: 160px; + } + .product-info { + padding: 12px; + } + .product-name { + font-size: 14px; + } + .product-price { + font-size: 16px; + } + + .pagination { + flex-wrap: wrap; + gap: 4px; + } + .pagination-btn { + padding: 8px 12px; + font-size: 13px; + } + + .footer-container { + padding: 0 16px; + flex-direction: column; + gap: 20px; + text-align: center; + } + .footer-links { + order: -1; + } + .footer-text { + order: 1; + } + .footer-social-icons { + order: 2; + } +} \ No newline at end of file diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 00000000..fe981763 --- /dev/null +++ b/css/reset.css @@ -0,0 +1,74 @@ +/* Reset CSS */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, div, span, h1, h2, h3, h4, h5, h6, p, a, img, ul, ol, +li, form, label, table, tr, th, td, article, aside, footer, header, nav, section { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset */ +article, aside, footer, header, nav, section { + display: block; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +html { + font-size: 16px; +} + +button { + background: none; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +input, textarea, select { + font: inherit; + border: none; + outline: none; + background: none; +} + +img { + max-width: 100%; + height: auto; + display: block; +} \ No newline at end of file diff --git a/images/facebook.png b/images/facebook.png new file mode 100644 index 00000000..b2b9469b Binary files /dev/null and b/images/facebook.png differ diff --git a/images/insta.png b/images/insta.png new file mode 100644 index 00000000..df99929f Binary files /dev/null and b/images/insta.png differ diff --git a/images/panda1.png b/images/panda1.png new file mode 100644 index 00000000..7740eaf0 Binary files /dev/null and b/images/panda1.png differ diff --git a/images/panda2.png b/images/panda2.png new file mode 100644 index 00000000..090eac01 Binary files /dev/null and b/images/panda2.png differ diff --git a/images/panda3.png b/images/panda3.png new file mode 100644 index 00000000..5d8ef91f Binary files /dev/null and b/images/panda3.png differ diff --git a/images/panda4.png b/images/panda4.png new file mode 100644 index 00000000..2d6b56d5 Binary files /dev/null and b/images/panda4.png differ diff --git a/images/panda5.png b/images/panda5.png new file mode 100644 index 00000000..84f5f8cd Binary files /dev/null and b/images/panda5.png differ diff --git a/images/pandalogo.png b/images/pandalogo.png new file mode 100644 index 00000000..9b53b257 Binary files /dev/null and b/images/pandalogo.png differ diff --git a/images/tw.png b/images/tw.png new file mode 100644 index 00000000..45bfdabf Binary files /dev/null and b/images/tw.png differ diff --git a/images/youtube.png b/images/youtube.png new file mode 100644 index 00000000..4c8c1cd9 Binary files /dev/null and b/images/youtube.png differ diff --git a/js/.env b/js/.env new file mode 100644 index 00000000..359d43fe --- /dev/null +++ b/js/.env @@ -0,0 +1,2 @@ +MONGODB_URI=mongodb+srv://marin:marin123@cluster0.obnttz8.mongodb.net/market?retryWrites=true&w=majority +PORT=5000 \ No newline at end of file diff --git a/js/App.js b/js/App.js new file mode 100644 index 00000000..d83cde76 --- /dev/null +++ b/js/App.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import Navigation from './components/Navigation'; +import LandingPage from './pages/LandingPage'; +import MarketPage from './pages/MarketPage'; +import RegistrationPage from './pages/RegistrationPage'; +import Footer from './components/Footer'; + +function App() { + return ( + +
+ {/* 상단 네비게이션 바 */} + + + {/* 메인 콘텐츠 */} +
+ + {/* 랜딩 페이지 - "/" */} + } /> + + {/* 중고마켓 페이지 - "/items" */} + } /> + + {/* 상품 등록 페이지 - "/registration" */} + } /> + + {/* 상품 상세 페이지 (빈 페이지) */} + } /> + +
+ + {/* 하단 푸터 */} +
+
+ ); +} + +// 빈 페이지 컴포넌트 +const EmptyPage = ({ title }) => { + return ( +
+
+

{title}

+

현재 준비 중인 페이지입니다.

+ 홈으로 돌아가기 +
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/js/ArticleService.js b/js/ArticleService.js new file mode 100644 index 00000000..806965b8 --- /dev/null +++ b/js/ArticleService.js @@ -0,0 +1,150 @@ +// API 서버 주소 +const BASE_URL = 'https://panda-market-api-crud.vercel.app'; + +// 게시글 목록 가져오기 +// page: 몇 페이지를 볼지 (기본값: 1페이지) +// pageSize: 한 페이지에 몇 개씩 보여줄지 (기본값: 10개) +// keyword: 검색어 (비어있으면 전체 조회) +export function getArticleList(page = 1, pageSize = 10, keyword = '') { + // URL에 붙일 파라미터 준비 + const params = new URLSearchParams(); + if (page) params.append('page', page); + if (pageSize) params.append('pageSize', pageSize); + if (keyword) params.append('keyword', keyword); + + // 최종 요청 URL 만들기 + const url = `${BASE_URL}/articles?${params.toString()}`; + + // 서버에 요청 보내기 + return fetch(url) + .then(response => { + // 서버 응답이 정상이 아니면 에러 발생 + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + // JSON 형식으로 변환 + return response.json(); + }) + .then(data => { + console.log('게시글 목록 조회 성공:', data); + return data; + }) + .catch(error => { + console.error('게시글 목록 조회 실패:', error.message); + throw error; + }); +} + +// 특정 게시글 하나만 가져오기 +// id: 조회할 게시글 번호 +export function getArticle(id) { + const url = `${BASE_URL}/articles/${id}`; + + return fetch(url) + .then(response => { + // 서버 응답이 정상이 아니면 에러 발생 + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + // JSON 형식으로 변환 + return response.json(); + }) + .then(data => { + console.log('게시글 조회 성공:', data); + return data; + }) + .catch(error => { + console.error('게시글 조회 실패:', error.message); + throw error; + }); +} + +// 새 게시글 작성하기 +// title: 게시글 제목 +// content: 게시글 내용 +// image: 이미지 URL (선택사항) +export function createArticle(title, content, image) { + const url = `${BASE_URL}/articles`; + + // 서버로 보낼 데이터 + const requestBody = { + title, + content, + image + }; + + return fetch(url, { + method: 'POST', // 생성은 POST 메서드 사용 + headers: { + 'Content-Type': 'application/json', // JSON 형식으로 보냄 + }, + body: JSON.stringify(requestBody) // 객체를 JSON 문자열로 변환 + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + return response.json(); + }) + .then(data => { + console.log('게시글 생성 성공:', data); + return data; + }) + .catch(error => { + console.error('게시글 생성 실패:', error.message); + throw error; + }); +} + +// 기존 게시글 수정하기 +// id: 수정할 게시글 번호 +// updateData: 수정할 내용 (예: {title: "새 제목", content: "새 내용"}) +export function patchArticle(id, updateData) { + const url = `${BASE_URL}/articles/${id}`; + + return fetch(url, { + method: 'PATCH', // 수정은 PATCH 메서드 사용 + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updateData) // 수정할 내용을 JSON으로 변환 + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + return response.json(); + }) + .then(data => { + console.log('게시글 수정 성공:', data); + return data; + }) + .catch(error => { + console.error('게시글 수정 실패:', error.message); + throw error; + }); +} + +// 게시글 삭제하기 +// id: 삭제할 게시글 번호 +export function deleteArticle(id) { + const url = `${BASE_URL}/articles/${id}`; + + return fetch(url, { + method: 'DELETE' // 삭제는 DELETE 메서드 사용 + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + return response.json(); + }) + .then(data => { + console.log('게시글 삭제 성공:', data); + return data; + }) + .catch(error => { + console.error('게시글 삭제 실패:', error.message); + throw error; + }); +} \ No newline at end of file diff --git a/js/MarketPage.jsx b/js/MarketPage.jsx new file mode 100644 index 00000000..732a765b --- /dev/null +++ b/js/MarketPage.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import BestItems from '../components/BestItems'; +import SalesItemList from '../components/SalesItemList'; + +const MarketPage = () => { + const navigate = useNavigate(); + + // 상품 등록 버튼 클릭 핸들러 + const handleRegisterClick = () => { + navigate('/registration'); + }; + + return ( +
+ {/* 상품 등록 버튼 */} +
+
+ +
+
+ + {/* 베스트 상품 섹션 */} + + + {/* 전체 상품 섹션 */} + +
+ ); +}; + +export default MarketPage; \ No newline at end of file diff --git a/js/ProductService.js b/js/ProductService.js new file mode 100644 index 00000000..c935c747 --- /dev/null +++ b/js/ProductService.js @@ -0,0 +1,126 @@ +const BASE_URL = 'https://panda-market-api-crud.vercel.app'; + +// async/await으로 상품 목록 조회 (orderBy 파라미터 추가) +export async function getProductList(page = 1, pageSize = 10, keyword = '', orderBy = 'recent') { + try { + const params = new URLSearchParams(); + if (page) params.append('page', page); + if (pageSize) params.append('pageSize', pageSize); + if (keyword) params.append('keyword', keyword); + if (orderBy) params.append('orderBy', orderBy); + + const url = `${BASE_URL}/products?${params.toString()}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + console.log('상품 목록 조회 성공:', data); + return data; + } catch (error) { + console.error('상품 목록 조회 실패:', error.message); + throw error; + } +} + +// async/await으로 특정 상품 조회 +export async function getProduct(id) { + try { + const url = `${BASE_URL}/products/${id}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + console.log('상품 조회 성공:', data); + return data; + } catch (error) { + console.error('상품 조회 실패:', error.message); + throw error; + } +} + +// async/await으로 상품 생성 +export async function createProduct(name, description, price, tags, images) { + try { + const url = `${BASE_URL}/products`; + const requestBody = { + name, + description, + price, + tags, + images + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + console.log('상품 생성 성공:', data); + return data; + } catch (error) { + console.error('상품 생성 실패:', error.message); + throw error; + } +} + +// async/await으로 상품 수정 +export async function patchProduct(id, updateData) { + try { + const url = `${BASE_URL}/products/${id}`; + + const response = await fetch(url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updateData) + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + console.log('상품 수정 성공:', data); + return data; + } catch (error) { + console.error('상품 수정 실패:', error.message); + throw error; + } +} + +// async/await으로 상품 삭제 +export async function deleteProduct(id) { + try { + const url = `${BASE_URL}/products/${id}`; + + const response = await fetch(url, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`); + } + + const data = await response.json(); + console.log('상품 삭제 성공:', data); + return data; + } catch (error) { + console.error('상품 삭제 실패:', error.message); + throw error; + } +} \ No newline at end of file diff --git a/js/RegistrationPage.jsx b/js/RegistrationPage.jsx new file mode 100644 index 00000000..d9cabdad --- /dev/null +++ b/js/RegistrationPage.jsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { useValidation } from '../hooks/useValidation'; +import { createProduct } from '../services/ProductService'; + +const RegistrationPage = () => { + const { errors, touched, validate, validateAll, setFieldTouched, isFormValid } = useValidation(); + + const [formData, setFormData] = useState({ + name: '', + description: '', + price: '', + tags: [] + }); + + const [tagInput, setTagInput] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Input 값 변경 핸들러 + const handleInputChange = (field, value) => { + setFormData({ ...formData, [field]: value }); + validate(field, value); + }; + + // Input focus out 핸들러 + const handleBlur = (field) => { + setFieldTouched(field); + validate(field, formData[field]); + }; + + // 태그 입력 핸들러 + const handleTagInputChange = (e) => { + const value = e.target.value; + setTagInput(value); + validate('tag', value); + }; + + // 엔터키로 태그 추가 + const handleTagKeyPress = (e) => { + if (e.key === 'Enter' && tagInput.trim()) { + e.preventDefault(); + + // 태그 유효성 검사 + if (validate('tag', tagInput)) { + setFormData({ + ...formData, + tags: [...formData.tags, tagInput.trim()] + }); + setTagInput(''); + } + } + }; + + // 태그 삭제 + const removeTag = (indexToRemove) => { + setFormData({ + ...formData, + tags: formData.tags.filter((_, index) => index !== indexToRemove) + }); + }; + + // 폼 제출 + const handleSubmit = async (e) => { + e.preventDefault(); + + // 전체 유효성 검사 + if (!validateAll(formData)) { + return; + } + + setIsSubmitting(true); + + try { + const response = await createProduct( + formData.name, + formData.description, + Number(formData.price), + formData.tags, + [] // images는 빈 배열로 + ); + + // 등록 성공 시 상품 상세 페이지로 이동 (빈 페이지) + window.location.href = `/items/${response.id}`; + } catch (error) { + console.error('상품 등록 실패:', error); + alert('상품 등록에 실패했습니다. 다시 시도해주세요.'); + } finally { + setIsSubmitting(false); + } + }; + + // 등록 버튼 활성화 여부 + const canSubmit = isFormValid(formData) && !isSubmitting; + + return ( +
+
+

상품 등록하기

+ +
+ {/* 상품명 */} +
+ + handleInputChange('name', e.target.value)} + onBlur={() => handleBlur('name')} + /> + {touched.name && errors.name && ( +

{errors.name}

+ )} +
+ + {/* 상품 소개 */} +
+ +