diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/category/controller/CategoryController.java b/backend/src/main/java/com/programmers/pcquotation/domain/category/controller/CategoryController.java index 82d32c4..a84f2fc 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/category/controller/CategoryController.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/category/controller/CategoryController.java @@ -23,7 +23,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/categories") +@RequestMapping("/api/admin/categories") public class CategoryController { private final CategoryService categoryService; diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/estimaterequest/entity/EstimateRequest.java b/backend/src/main/java/com/programmers/pcquotation/domain/estimaterequest/entity/EstimateRequest.java index f1d8f98..428768a 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/estimaterequest/entity/EstimateRequest.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/estimaterequest/entity/EstimateRequest.java @@ -3,7 +3,13 @@ import java.time.LocalDateTime; import com.programmers.pcquotation.domain.customer.entity.Customer; -import jakarta.persistence.*; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ImageController.java b/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ImageController.java index 046cdb3..292877e 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ImageController.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ImageController.java @@ -12,7 +12,9 @@ import com.programmers.pcquotation.domain.item.service.ImageService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api/image") @@ -23,6 +25,7 @@ public class ImageController { @GetMapping("/{filename}") public Resource getImage( @PathVariable String filename) { + log.info("Fetching image with filename: {}", filename); // 로그 추가 return imageService.getImageByFilename(filename); } @@ -33,7 +36,7 @@ public ResponseEntity getImageFileName(@PathVariable String filename) // 이미지 Resource 가져오기 Resource resource = imageService.getImageByFilename(filename); return ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) // 또는 적절한 미디어 타입 + .contentType(MediaType.IMAGE_JPEG) // 또는 적절한 미디어 타입 .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"") .body(resource); } catch (Exception e) { diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ItemController.java b/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ItemController.java index 4c7ce37..b5918f1 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ItemController.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/item/controller/ItemController.java @@ -23,7 +23,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/items") +@RequestMapping("/api/admin/items") public class ItemController { diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ImageService.java b/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ImageService.java index 18bcd68..9572d52 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ImageService.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ImageService.java @@ -40,6 +40,7 @@ public ImageService( * @return Product Image Resource */ public Resource getImageByFilename(String filename) { + log.info("Requested filename: {}", filename); if (filename == null || filename.isEmpty() || checkInvalidExt(filename)) { throw new InvalidImageRequestException("Invalid Type Filename"); } diff --git a/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ItemService.java b/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ItemService.java index 03a98b4..079375b 100644 --- a/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ItemService.java +++ b/backend/src/main/java/com/programmers/pcquotation/domain/item/service/ItemService.java @@ -57,10 +57,10 @@ public List getItemList() { return items.stream() .map(item -> new ItemInfoResponse( item.getId(), - item.getCategory().getCategory(), - item.getCategory().getId(), - item.getName(), - item.getImgFilename() + item.getName(), // 부품 이름 (name) + item.getCategory().getId(), // 카테고리 ID + item.getCategory().getCategory(), // 카테고리 이름 (categoryName) + item.getImgFilename() // 이미지 파일명 )) .collect(Collectors.toList()); } @@ -68,21 +68,26 @@ public List getItemList() { //부품 수정 @Transactional public ItemUpdateResponse updateItem(Long id, ItemUpdateRequest request) { - Item item = itemRepository.findById(id) .orElseThrow(() -> new ItemNotFoundException(id)); Category category = categoryRepository.findById(request.categoryId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 카테고리 ID입니다.")); + String imgFilename = request.imgFilename(); + + // imgFilename이 null이거나 비어있다면 기존의 filename을 사용 + if (imgFilename == null || imgFilename.isEmpty()) { + imgFilename = item.getImgFilename(); // 기존 이미지 파일 이름을 가져옴 + } + item.updateItem( request.name(), - request.imgFilename(), + imgFilename, category ); return new ItemUpdateResponse(id, "부품 수정 완료"); - } //부품 삭제 diff --git a/backend/src/main/java/com/programmers/pcquotation/global/security/SecurityConfig.java b/backend/src/main/java/com/programmers/pcquotation/global/security/SecurityConfig.java index 0375c71..4838562 100644 --- a/backend/src/main/java/com/programmers/pcquotation/global/security/SecurityConfig.java +++ b/backend/src/main/java/com/programmers/pcquotation/global/security/SecurityConfig.java @@ -25,9 +25,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorizeRequests -> authorizeRequests - .requestMatchers(HttpMethod.GET, "/seller/api/**") - .hasRole("SELLER") - .requestMatchers(HttpMethod.GET, "/seller") + .requestMatchers(HttpMethod.GET, "/sellers") .hasRole("SELLER") .anyRequest() .permitAll() diff --git a/frontend/src/app/admin/page.js b/frontend/src/app/admin/page.js new file mode 100644 index 0000000..908ec30 --- /dev/null +++ b/frontend/src/app/admin/page.js @@ -0,0 +1,413 @@ +'use client'; +import React, { useState, useEffect } from 'react'; + +const ItemModal = ({ isOpen, onClose, onSubmit, item, categories }) => { + const [name, setName] = useState(''); + const [image, setImage] = useState(null); + const [categoryId, setCategoryId] = useState(''); + + useEffect(() => { + if (item) { + setName(item.name); + setImage(null); // 이미지 초기화 + setCategoryId(item.categoryId); + } + }, [item]); + + const handleSubmit = (e) => { + e.preventDefault(); + console.log('Image file in submit:', image); // 이미지 파일 상태 로그 추가 + if (onSubmit) { + onSubmit({ name, image, categoryId }); + } + onClose(); + }; + + return ( + isOpen && ( +
+
+

{item ? '부품 수정' : '부품 추가'}

+
+ setName(e.target.value)} + placeholder="부품 이름을 입력하세요" + className="border p-2 w-full mb-4" + required + /> + { + const file = e.target.files[0]; + console.log('Selected file:', file); // 선택된 파일 로그 추가 + setImage(file); + }} + className="mb-4" + accept="image/*" // 파일 형식 제한 + /> + {item && item.filename && ( +
+ 현재 이미지 +
+ )} + + + +
+
+
+ ) + ); +}; + +export default function ItemList() { + const [categories, setCategories] = useState([]); + const [items, setItems] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [newCategoryName, setNewCategoryName] = useState(''); + const [selectedItems, setSelectedItems] = useState(new Set()); + const [image, setImage] = useState(null); // 이미지 상태 추가 + + useEffect(() => { + fetchCategories(); + fetchItems(); + }, []); + + const fetchCategories = () => { + fetch('http://localhost:8080/api/admin/categories') + .then((response) => response.json()) + .then((data) => { + if (Array.isArray(data)) { + setCategories(data); + } else { + console.error('잘못된 데이터 형식:', data); + } + }) + .catch((error) => console.error('카테고리 로딩 실패:', error)); + }; + + const fetchItems = () => { + fetch('http://localhost:8080/api/admin/items') + .then((response) => response.json()) + .then((data) => { + if (Array.isArray(data)) { + console.log(data); + setItems(data); + } else { + console.error('잘못된 데이터 형식:', data); + } + }) + .catch((error) => console.error('부품 로딩 실패:', error)); + }; + + const handleCategoryClick = (categoryId) => { + setSelectedCategory(categoryId); + }; + + const handleAddCategory = () => { + if (!newCategoryName.trim()) return alert('카테고리 이름을 입력하세요.'); + fetch('http://localhost:8080/api/admin/categories', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ category: newCategoryName }), + }) + .then((response) => response.json()) + .then(() => { + setNewCategoryName(''); + fetchCategories(); + }) + .catch((error) => console.error('카테고리 추가 실패:', error)); + }; + + const handleUpdateCategory = () => { + if (!selectedCategory) return alert('수정할 카테고리를 선택하세요.'); + const newName = prompt('새로운 카테고리 이름을 입력하세요:'); + if (!newName) return; + + fetch(`http://localhost:8080/api/admin/categories/${selectedCategory}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ category: newName }), + }) + .then((response) => response.json()) + .then(() => fetchCategories()) + .catch((error) => console.error('카테고리 수정 실패:', error)); + }; + + const handleDeleteCategory = () => { + if (!selectedCategory) return alert('삭제할 카테고리를 선택하세요.'); + if (!confirm('정말로 삭제하시겠습니까?')) return; + + fetch(`http://localhost:8080/api/admin/categories/${selectedCategory}`, { + method: 'DELETE', + }) + .then((response) => response.json()) + .then(() => { + setSelectedCategory(null); + fetchCategories(); + }) + .catch((error) => console.error('카테고리 삭제 실패:', error)); + }; + + const handleAddItem = (newItem) => { + // 부품 이름이 비어있는지 확인 + if (!newItem.name) { + return alert('부품 이름을 입력해주세요.'); // 이름 비어 있을 경우 경고 메시지 + } + + // 이미지가 선택되지 않은 경우 경고 메시지 표시 + if (!newItem.image) { + return alert('이미지를 선택하세요.'); + } + + const formData = new FormData(); + formData.append('name', newItem.name); + formData.append('categoryId', newItem.categoryId); + + const uuid = crypto.randomUUID(); // UUID 생성 + const extension = newItem.image.name.split('.').pop(); // 파일 확장자 추출 + const filename = `${uuid}.${extension}`; // UUID와 확장자를 결합한 파일 이름 + + formData.append('image', new Blob([newItem.image], { type: newItem.image.type }), filename); + + fetch('http://localhost:8080/api/admin/items', { + method: 'POST', + body: formData, + }) + .then((response) => { + if (!response.ok) { + throw new Error('부품 추가 실패'); // 응답이 성공적이지 않을 경우 에러 처리 + } + return response.json(); + }) + .then(() => { + fetchItems(); // 아이템 목록 새로고침 + }) + .catch((error) => console.error('부품 추가 실패:', error)); + }; + + const handleUpdateItem = (updatedItem) => { + const formData = new FormData(); + formData.append('name', updatedItem.name); + formData.append('categoryId', updatedItem.categoryId); + + if (updatedItem.image) { + // 새 이미지가 있는 경우 + const uuid = crypto.randomUUID(); // UUID 생성 + const extension = updatedItem.image.name.split('.').pop(); // 파일 확장자 추출 + const filename = `${uuid}.${extension}`; // UUID와 확장자를 결합한 파일 이름 + + formData.append('image', new Blob([updatedItem.image], { type: updatedItem.image.type }), filename); + } else { + // 이미지가 없는 경우 기존 파일 이름 사용 + formData.append('imgFilename', editingItem.filename); // 기존 이미지 파일 이름 사용 + } + + fetch(`http://localhost:8080/api/admin/items/${editingItem.id}`, { + method: 'PUT', + body: formData, + }) + .then((response) => { + if (!response.ok) { + throw new Error('부품 수정 실패'); + } + return response.json(); + }) + .then(() => { + fetchItems(); // 아이템 목록 새로고침 + }) + .catch((error) => console.error('부품 수정 실패:', error)); + }; + + const toggleItemSelection = (itemId) => { + const updatedSelection = new Set(selectedItems); + if (updatedSelection.has(itemId)) { + updatedSelection.delete(itemId); + } else { + updatedSelection.add(itemId); + } + setSelectedItems(updatedSelection); + }; + + const handleDeleteSelectedItems = () => { + if (selectedItems.size === 0) return alert('삭제할 부품을 선택하세요.'); + if (!confirm('선택한 부품을 정말로 삭제하시겠습니까?')) return; + + Promise.all(Array.from(selectedItems).map(itemId => { + return fetch(`http://localhost:8080/api/admin/items/${itemId}`, { + method: 'DELETE', + }); + })) + .then(() => { + setSelectedItems(new Set()); + fetchItems(); + }) + .catch((error) => console.error('부품 삭제 실패:', error)); + }; + + const openAddModal = () => { + setEditingItem(null); + setImage(null); // 이미지 상태 초기화 + setIsModalOpen(true); + }; + + const openEditModal = (item) => { + setEditingItem(item); + setImage(null); // 이미지 상태 초기화 + setIsModalOpen(true); + }; + + return ( +
+

카테고리와 부품 관리자 페이지

+ + {/* 카테고리 추가 */} +
+ setNewCategoryName(e.target.value)} + placeholder="새 카테고리 이름" + className="px-4 py-2 border border-gray-300 rounded-lg dark:bg-gray-800 dark:text-white" + /> + +
+ + {/* 카테고리 목록 */} +
+ {categories.length > 0 ? ( + categories.map((category) => ( + + )) + ) : ( +

카테고리가 없습니다.

+ )} +
+ + {/* 카테고리 수정 & 삭제 */} + {selectedCategory && ( +
+ + +
+ )} + + {/* 부품 목록 */} + {selectedCategory && ( +
+

+ {categories.find(c => c.id === selectedCategory)?.category} 부품 목록 +

+
{/* 중앙 정렬을 위한 div 추가 */} + + +
+
+ +
+
+ {items.filter(item => item.categoryId === selectedCategory).length > 0 ? ( + items + .filter(item => item.categoryId === selectedCategory) + .map((item) => ( +
+ toggleItemSelection(item.id)} + className="mb-2" + /> + {item.name} +

{item.name}

+ +
+ )) + ) : ( +

해당 카테고리에 부품이 없습니다.

+ )} +
+
+ )} + + {/* 모달 */} + setIsModalOpen(false)} + onSubmit={editingItem ? handleUpdateItem : handleAddItem} + item={editingItem} + categories={categories} // 카테고리 목록을 모달에 전달 + /> +
+ ); +} + +