diff --git a/.gitignore b/.gitignore index c82540bb..a9d47990 100644 --- a/.gitignore +++ b/.gitignore @@ -646,3 +646,7 @@ MigrationBackup/ .ionide/ # End of https://www.toptal.com/developers/gitignore/api/intellij,java,react,node,vs + +#custom +db_dev.mv.db +db_dev.trace.db diff --git a/BackEnd/src/main/java/com/team5/nbe341team05/common/jpa/entity/BaseInitData.java b/BackEnd/src/main/java/com/team5/nbe341team05/common/jpa/entity/BaseInitData.java index 67dbec6a..f4fd717a 100644 --- a/BackEnd/src/main/java/com/team5/nbe341team05/common/jpa/entity/BaseInitData.java +++ b/BackEnd/src/main/java/com/team5/nbe341team05/common/jpa/entity/BaseInitData.java @@ -11,15 +11,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockMultipartFile; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; @Configuration @RequiredArgsConstructor @@ -27,6 +25,8 @@ public class BaseInitData { private final CartService cartService; private final MenuService menuService; private final OrderService orderService; + private final ResourceLoader resourceLoader; // ResourceLoader 추가 + @Value("${upload.path}") String savePath; @@ -45,15 +45,18 @@ public ApplicationRunner baseInitDataApplicationRunner() { public void makeSampleMenus() throws IOException { if (menuService.count() > 0) return; - for (int i = 1; i <= 8; i++) { - String fileName = String.format("menu%d.jpg", i ); - Path filePath = Paths.get(savePath + fileName); + for (int i = 1; i <= 35; i++) { + int num = ((i-1) % 8) + 1; + String fileName = String.format("menu%d.jpg", num); + + // Resource를 사용하여 classpath에서 이미지 파일 읽기 + Resource resource = resourceLoader.getResource("classpath:static/images/" + fileName); MultipartFile multipartFile = new MockMultipartFile( fileName, fileName, "image/jpeg", - Files.readAllBytes(filePath) + resource.getInputStream().readAllBytes() ) { }; @@ -65,28 +68,5 @@ public void makeSampleMenus() throws IOException { .build(); menuService.create(menuRequestDto, multipartFile); } - -// String[] menuTypes = {"커피", "주스", "스무디", "티", "에이드"}; -// String[] descriptionsTemplate = { -// "깊고 진한 맛의 ", -// "상큼하고 달콤한 ", -// "시원하고 청량한 ", -// "부드럽고 향긋한 ", -// "달콤쌉싸름한 " -// }; -// -// for (int i = 1; i <= 200; i++) { -// // 메뉴 종류를 랜덤하게 선택 -// String menuType = menuTypes[i % 5]; -// String description = descriptionsTemplate[i % 5]; -// -// menuService.create( -// menuType + " " + i + "호", // 예: "커피 1호", "주스 2호" 등 -// description + menuType + "입니다.", // 예: "깊고 진한 맛의 커피입니다." -// ((i % 3) + 3) * 1000, // 3000~5000원 범위의 가격 -// 100 + (i % 50), // 100~149개 범위의 재고 -// "menu" + i + ".jpg" // menu1.jpg, menu2.jpg 등 -// ); -// } } -} \ No newline at end of file +} diff --git a/BackEnd/src/main/java/com/team5/nbe341team05/common/security/SecurityConfig.java b/BackEnd/src/main/java/com/team5/nbe341team05/common/security/SecurityConfig.java index 3b3e2e72..509db019 100644 --- a/BackEnd/src/main/java/com/team5/nbe341team05/common/security/SecurityConfig.java +++ b/BackEnd/src/main/java/com/team5/nbe341team05/common/security/SecurityConfig.java @@ -17,9 +17,9 @@ import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; +import java.util.List; @Configuration @EnableWebSecurity @@ -49,7 +49,16 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .logoutUrl("/logout") .logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트 ) - .cors(cors -> cors.configurationSource(corsConfigurationSource())); + .cors(corsCustomizer -> corsCustomizer.configurationSource(request -> { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("http://localhost:3000"); + config.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000")); + config.setAllowedMethods(List.of("GET", "POST", "DELETE", "PATCH", "OPTION", "PUT")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + return config; + })) + ; return http.build(); } @@ -78,26 +87,4 @@ public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - - @Bean - public CorsFilter corsFilter() { - return new CorsFilter(corsConfigurationSource()); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.addAllowedOrigin("http://localhost:3000"); // 허용할 주소 설정 - configuration.addAllowedMethod("GET"); // 메서드 허용 - configuration.addAllowedMethod("POST"); // 메서드 허용 - configuration.addAllowedMethod("PUT"); // 메서드 허용 - configuration.addAllowedMethod("DELETE"); // 메서드 허용 - configuration.addAllowedHeader("*"); // 헤더 허용 - configuration.setAllowCredentials(true); // 자격 증명 허용 설정 - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } } diff --git a/BackEnd/src/main/java/com/team5/nbe341team05/domain/menu/service/MenuService.java b/BackEnd/src/main/java/com/team5/nbe341team05/domain/menu/service/MenuService.java index a362e22a..c8bd023d 100644 --- a/BackEnd/src/main/java/com/team5/nbe341team05/domain/menu/service/MenuService.java +++ b/BackEnd/src/main/java/com/team5/nbe341team05/domain/menu/service/MenuService.java @@ -36,17 +36,6 @@ public Page getAllMenus(int page, MenuSortType sortType) { List sorts = new ArrayList<>(); sorts.add(sortType.getOrder()); - // 전체 데이터 수를 먼저 확인 - long totalCount = menuRepository.count(); - int totalPages = (int) Math.ceil((double) totalCount / PAGE_SIZE); - - // 페이지 번호 보정 - if (page < 0) { - page = 0; - } else if (totalCount > 0 && page >= totalPages) { - page = totalPages - 1; - } - Pageable pageable = PageRequest.of(page, PAGE_SIZE, Sort.by(sorts)); return menuRepository.findAll(pageable); } diff --git a/BackEnd/src/main/resources/static/images/event1.jpg b/BackEnd/src/main/resources/static/images/event1.jpg new file mode 100644 index 00000000..54d9f371 Binary files /dev/null and b/BackEnd/src/main/resources/static/images/event1.jpg differ diff --git a/BackEnd/src/main/resources/static/images/event2.jpg b/BackEnd/src/main/resources/static/images/event2.jpg new file mode 100644 index 00000000..e8f2d544 Binary files /dev/null and b/BackEnd/src/main/resources/static/images/event2.jpg differ diff --git a/BackEnd/src/main/resources/static/images/event3.jpg b/BackEnd/src/main/resources/static/images/event3.jpg new file mode 100644 index 00000000..6413900e Binary files /dev/null and b/BackEnd/src/main/resources/static/images/event3.jpg differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 70bac5d4..908fea31 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "ajv-keywords": "^5.1.0", "axios": "^1.7.9", "cra-template": "1.2.0", + "date-fns": "^4.1.0", "lucide-react": "^0.473.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -6353,6 +6354,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -16426,6 +16436,19 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 369dfcbe..36719bfe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "ajv-keywords": "^5.1.0", "axios": "^1.7.9", "cra-template": "1.2.0", + "date-fns": "^4.1.0", "lucide-react": "^0.473.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/frontend/src/App.js b/frontend/src/App.js index 8c85eb3e..c05b2858 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,5 @@ import React from 'react'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'; import OrderPage from './screens/OrderScreen'; import EmailInput from './screens/EmailInput'; import OrderList from './screens/OrderList'; @@ -7,6 +7,9 @@ import OrderDetail from './screens/OrderDetail'; import OrderModify from './screens/OrderModify'; import MainMenuScreen from './screens/MainMenuScreen'; import OrderListScreen from "./screens/OrderListScreen"; +import NoticeScreen from './screens/NoticeScreen'; +import EventsScreen from './screens/EventsScreen'; + import './App.css'; import LoginPage from "./components/login"; import AddMenuPage from "./screens/addMenu"; @@ -15,27 +18,31 @@ import AdminOrderList from './screens/AdminOrderList'; import Login from './components/login'; function App() { - return ( - - - } /> - } /> - } /> - } /> - } /> - {/* /login 경로에 LoginPage 컴포넌트를 렌더링 */} - } /> - {/* 기본 경로 설정 */} - {/* Welcome to the Home Page} /> */} - } /> - }/> - } /> - } /> - } /> - } /> - - - ); + return ( + + + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + {/* /login 경로에 LoginPage 컴포넌트를 렌더링 */} + }/> + {/* 기본 경로 설정 */} + {/* Welcome to the Home Page} /> */} + }/> + }/> + }/> + }/> + }/> + }/> + + + ); } export default App; \ No newline at end of file diff --git a/frontend/src/DL/api.js b/frontend/src/DL/api.js index 776c9428..e90073e4 100644 --- a/frontend/src/DL/api.js +++ b/frontend/src/DL/api.js @@ -53,7 +53,7 @@ export const addMenu = async (menuData,image) => { try { const formData = new FormData(); formData.append("menu", JSON.stringify(menuData)); // JSON으로 변환 후 추가 - formData.append("image", image); + formData.append("image", image); const response = await api.post(`/admin/menus`, formData, { headers: { @@ -101,9 +101,29 @@ export const cancelOrder = (email, id) => api.delete(`/order/${email}/${id}`); // 메뉴 전체 조회 -// export const getAllMenu = () => api.get(`/menus`); -export const getAllMenu = (page = 0) => { - return api.get(`/menus?page=${page}`); +// DL/api.js +export const getAllMenu = (page = 0, sort = 'recent') => { + let sortParam; + switch(sort) { + case 'viewsDesc': + sortParam = 'VIEWS_DESC'; + break; + case 'recent': + sortParam = 'RECENT'; + break; + case 'oldest': + sortParam = 'OLDEST'; + break; + case 'priceDesc': + sortParam = 'PRICE_DESC'; + break; + case 'priceAsc': + sortParam = 'PRICE_ASC'; + break; + default: + sortParam = 'RECENT'; + } + return api.get(`/menus?page=${page}&sortType=${sortParam}`); }; // 특정 메뉴 조회 diff --git a/frontend/src/components/Navbar.js b/frontend/src/components/Navbar.js index 660c43f2..a68f888d 100644 --- a/frontend/src/components/Navbar.js +++ b/frontend/src/components/Navbar.js @@ -18,26 +18,19 @@ const Navbar = () => { 주문 조회 -
  • - { - const cartItems = localStorage.getItem("cartItems"); - console.log("장바구니 데이터 확인:", JSON.parse(cartItems || "[]")); - }} - > - 장바구니 - +
  • + + 장바구니 +
  • setShowDropdown(true)} onMouseLeave={() => setShowDropdown(false)} > - - 관리자 페이지 - + + 관리자 페이지 + {showDropdown && (
    • diff --git a/frontend/src/components/ProductCard.js b/frontend/src/components/ProductCard.js index 20936490..4dd95c32 100644 --- a/frontend/src/components/ProductCard.js +++ b/frontend/src/components/ProductCard.js @@ -19,6 +19,7 @@ const ProductCard = ({ image, title, price, onClick, product }) => { // OrderPage의 addToCart 로직을 직접 사용 const addToCart = (menuId, menuName, quantity, price) => { + // localStorage를 사용하여 장바구니 데이터를 저장할 수 있습니다 const cartItems = JSON.parse(localStorage.getItem('cartItems') || '[]'); const existingItem = cartItems.find(item => item.menuId === menuId); @@ -38,13 +39,31 @@ const ProductCard = ({ image, title, price, onClick, product }) => { return; } + // localStorage에서 현재 장바구니 데이터를 가져옴 + const cartItems = JSON.parse(localStorage.getItem('cartItems') || '[]'); + // OrderPage의 addToCart 메서드 형식에 맞춰서 데이터 전달 - addToCart( - product.id, // menuId - product.productName, // menuName - quantity, // quantity - product.price // price - ); + // 새로운 아이템 생성 + const newItem = { + menuId: product.id, + menuName: product.productName, + quantity: quantity, + price: product.price + }; + + // 이미 존재하는 아이템인지 확인 + const existingItemIndex = cartItems.findIndex(item => item.menuId === product.id); + + if (existingItemIndex !== -1) { + // 기존 아이템이 있으면 수량만 증가 + cartItems[existingItemIndex].quantity += quantity; + } else { + // 새로운 아이템 추가 + cartItems.push(newItem); + } + + // 장바구니 데이터 저장 + localStorage.setItem('cartItems', JSON.stringify(cartItems)); alert('장바구니에 추가되었습니다.'); setQuantity(0); // 수량 초기화 diff --git a/frontend/src/components/ProductDetailPopup.js b/frontend/src/components/ProductDetailPopup.js index 83b1c2f8..c9eb7404 100644 --- a/frontend/src/components/ProductDetailPopup.js +++ b/frontend/src/components/ProductDetailPopup.js @@ -1,10 +1,15 @@ -// ProductDetailPopup.js -import React from 'react'; - +import React, { useState } from 'react'; +import { ImageOff } from 'lucide-react'; const ProductDetailPopup = ({product, onClose}) => { + const [imageError, setImageError] = useState(false); + + const handleImageError = () => { + setImageError(true); + }; + const API_BASE_URL = 'http://localhost:8080'; // 백엔드 API 주소 - + return (
      @@ -16,11 +21,21 @@ const ProductDetailPopup = ({product, onClose}) => {
      - {product.title} + {(!product.image || imageError) ? ( +
      + +
      + ) : ( + {product.title} + )}

      diff --git a/frontend/src/components/ProductList.js b/frontend/src/components/ProductList.js index d6c13296..ab776a4a 100644 --- a/frontend/src/components/ProductList.js +++ b/frontend/src/components/ProductList.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import ProductCard from './ProductCard'; import ProductDetailPopup from './ProductDetailPopup'; -import { getAllMenu } from '../DL/api'; +import { getAllMenu, getMenu } from '../DL/api'; const ProductList = () => { const [selectedProduct, setSelectedProduct] = useState(null); @@ -11,23 +11,34 @@ const ProductList = () => { const [page, setPage] = useState(0); const [hasMore, setHasMore] = useState(true); const observer = useRef(); + const [sortOption, setSortOption] = useState('recent'); const API_BASE_URL = 'http://localhost:8080'; // 백엔드 API 주소 + // 정렬 옵션 변경 핸들러 + const handleSortChange = (e) => { + setSortOption(e.target.value); + setPage(0); + setProducts([]); + setHasMore(true); + setLoading(true); // 로딩 상태 추가 + }; - const fetchProducts = async () => { + const fetchProducts = useCallback(async () => { try { - const response = await getAllMenu(page); + if (!hasMore) return; + setLoading(true); + const response = await getAllMenu(page, sortOption); + console.log('API Response:', response.data); // 응답 확인용 const newProducts = response.data.data.content; - - setProducts(prev => [...prev, ...newProducts]); + setProducts(prev => page === 0 ? newProducts : [...prev, ...newProducts]); setHasMore(!response.data.data.last); - setLoading(false); - console.log("response", response.data.data); } catch (error) { + console.error('API Error:', error); // 에러 확인용 setError('메뉴를 불러오는데 실패했습니다.'); - setLoading(false); + } finally { + setLoading(false); // 성공/실패 상관없이 로딩 상태 해제 } - }; + }, [page, sortOption, hasMore]); const lastProductCallback = useCallback(node => { if (loading) return; @@ -37,7 +48,7 @@ const ProductList = () => { } observer.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && hasMore) { + if (entries[0].isIntersecting && hasMore && !loading) { // loading 체크 추가 setPage(prevPage => prevPage + 1); } }); @@ -47,19 +58,33 @@ const ProductList = () => { } }, [loading, hasMore]); + // cleanup 추가 + useEffect(() => { + return () => { + if (observer.current) { + observer.current.disconnect(); + } + }; + }, []); + const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; - fetchProducts(); - } else if (page > 0) { // 첫 페이지가 아닐 때만 추가 데이터 로드 - fetchProducts(); } - }, [page]); + fetchProducts(); + }, [page, sortOption, fetchProducts]); - const handleProductClick = (product) => { - setSelectedProduct(product); + const handleProductClick = async (product) => { + try { + // 메뉴 조회 API 호출 (조회수 증가) + await getMenu(product.id); + setSelectedProduct(product); + } catch (error) { + console.error('메뉴 조회 실패:', error); + setSelectedProduct(product); // API 실패해도 팝업은 표시 + } }; const handleClosePopup = () => { @@ -70,17 +95,23 @@ const ProductList = () => { return (
      - {/* Sidebar */} -
      -
        -
      • All Products
      • -
      • Coffee Bean package
      • -
      • Capsule
      • -
      -
      - {/* Main Content */}
      + {/* 정렬 드롭다운 */} +
      + +
      +
      {products.map((product, index) => (
      { return ( -
      +
      {/* Sidebar Header */} -
      -

      List

      +
      +

      List

      + {/* Sidebar Navigation */}