Search

React 평가

React Γ— Spring Boot 평가

β€’
평가 ꡐ과λͺ© : Node.js 와 React ν™œμš©
β€’
총점 : 100점
β€’
평가 주제 : μƒν’ˆ(Product) 관리 μ‹œμŠ€ν…œ
μˆ˜μ—… μ‹œκ°„μ— μ§„ν–‰ν•œ ν”„λ‘œμ νŠΈ(κ²Œμ‹œνŒ, TodoList, 둜그인)λ₯Ό κ·ΈλŒ€λ‘œ λ³΅μ‚¬ν•˜μ—¬ μ œμΆœν•  경우 0점 μ²˜λ¦¬λ©λ‹ˆλ‹€. μƒν’ˆ 관리 μ‹œμŠ€ν…œμ„ 직접 κ΅¬ν˜„ν•˜μ—¬ μ œμΆœν•˜μ‹œμ˜€.

문제 κ°œμš”

React와 Spring Bootλ₯Ό ν™œμš©ν•˜μ—¬ μƒν’ˆ 관리 μ‹œμŠ€ν…œμ„ 직접 κ΅¬ν˜„ν•˜μ—¬ μ œμΆœν•˜μ‹œμ˜€.

μ£Όμš” κ΅¬ν˜„ λ‚΄μš©

β€’
λ°±μ—”λ“œ (Spring Boot): REST API μ„œλ²„ ꡬ좕 및 MySQL λ°μ΄ν„°λ² μ΄μŠ€ 연동
β€’
ν”„λ‘ νŠΈμ—”λ“œ (React): Vite 기반 ν”„λ‘œμ νŠΈ 생성 및 μƒν’ˆ CRUD κΈ°λŠ₯ κ΅¬ν˜„
β€’
μŠ€νƒ€μΌλ§: Tailwind CSS v4λ₯Ό ν™œμš©ν•œ UI λ””μžμΈ
β€’
λΌμš°νŒ…: React Routerλ₯Ό μ΄μš©ν•œ νŽ˜μ΄μ§€ λ„€λΉ„κ²Œμ΄μ…˜
β€’
HTTP 톡신: axiosλ₯Ό μ΄μš©ν•œ 비동기 API 호좜

평가 κΈ°μ€€

β€’
μ½”λ“œμ˜ μ •ν™•μ„± 및 완성도
β€’
μš”κ΅¬μ‚¬ν•­ μΆ©μ‘± μ—¬λΆ€
β€’
μ½”λ“œ 가독성 및 ꡬ쑰
β€’
UI/UX ν’ˆμ§ˆ

제좜 방법

ν•­λͺ©
파일λͺ…
λΉ„κ³ 
λ°±μ—”λ“œ (Spring Boot)
홍길동_product-server.zip
λ˜λŠ” GitHub Repository URL
ν”„λ‘ νŠΈμ—”λ“œ (React)
홍길동_product-client.zip
node_modules 폴더 μ œμ™Έ ν›„ μ••μΆ• λ˜λŠ” GitHub Repository URL

문제1 - ν”„λ‘œμ νŠΈ 생성 및 라이브러리 μ„€μΉ˜ λͺ…λ Ήμ–΄ μž‘μ„±

β€’
λ‚œμ΄λ„ : β˜…β˜†β˜†β˜†β˜† (ν•˜)
β€’
배점 : 20점

μš”κ΅¬μ‚¬ν•­

μ•„λž˜ λͺ…λ Ήμ–΄λ₯Ό μž‘μ„±ν•˜μ‹œμ˜€.
1.
Vite 기반으둜 React ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•˜λŠ” λͺ…λ Ήμ–΄λ₯Ό μž‘μ„±ν•˜μ‹œμ˜€.
β€’
ν”„λ‘œμ νŠΈλͺ… : product-client
β€’
ν…œν”Œλ¦Ώ : react
2.
npm을 μ΄μš©ν•˜μ—¬ μ•„λž˜ 라이브러리λ₯Ό 각각 μ„€μΉ˜ν•˜λŠ” λͺ…λ Ήμ–΄λ₯Ό μž‘μ„±ν•˜μ‹œμ˜€.
β€’
axios
β€’
react-router-dom
3.
npm을 μ΄μš©ν•˜μ—¬ Tailwind CSS v4와 Vite ν”ŒλŸ¬κ·ΈμΈμ„ μ„€μΉ˜ν•˜λŠ” λͺ…λ Ήμ–΄λ₯Ό μž‘μ„±ν•˜μ‹œμ˜€.
β€’
tailwindcss, @tailwindcss/vite
Tailwind v4λΆ€ν„°λŠ” postcss, autoprefixerλ₯Ό λ³„λ„λ‘œ μ„€μΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
4.
vite.config.js에 Tailwind ν”ŒλŸ¬κ·ΈμΈμ„ μΆ”κ°€ν•˜μ‹œμ˜€.
5.
src/index.css의 κΈ°μ‘΄ λ‚΄μš©μ„ λͺ¨λ‘ μ§€μš°κ³  Tailwind v4 import ν•œ μ€„λ‘œ κ΅μ²΄ν•˜μ‹œμ˜€.

힌트

# Vite + React ν”„λ‘œμ νŠΈ 생성 npm create vite@latest [ν”„λ‘œμ νŠΈλͺ…] -- --template react # μƒμ„±λœ ν΄λ”λ‘œ 이동 ν›„ κΈ°λ³Έ νŒ¨ν‚€μ§€ μ„€μΉ˜ cd [ν”„λ‘œμ νŠΈλͺ…] npm install # 라이브러리 μ„€μΉ˜ npm install [라이브러리λͺ…] # Tailwind v4 μ„€μΉ˜ npm install tailwindcss @tailwindcss/vite
Bash
볡사
// vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' // ← μΆ”κ°€ export default defineConfig({ plugins: [ react(), tailwindcss(), // ← μΆ”κ°€ ], })
JavaScript
볡사
/* src/index.css β€” κΈ°μ‘΄ λ‚΄μš© λͺ¨λ‘ μ‚­μ œ ν›„ μ•„λž˜ ν•œ μ€„λ§Œ μž‘μ„± */ @import "tailwindcss";
CSS
볡사

문제2 - Spring Boot λ°±μ—”λ“œ κ΅¬ν˜„

β€’
λ‚œμ΄λ„ : β˜…β˜…β˜…β˜†β˜† (쀑)
β€’
배점 : 25점

μš”κ΅¬μ‚¬ν•­

μ•„λž˜ μƒν’ˆ(Product) 데이터λ₯Ό κ΄€λ¦¬ν•˜λŠ” REST API μ„œλ²„λ₯Ό μ™„μ„±ν•˜μ‹œμ˜€.

μƒν’ˆ(Product) 속성

ν•„λ“œλͺ…
νƒ€μž…
μ„€λͺ…
id
String
κΈ°λ³Έν‚€ (UUID)
name
String
μƒν’ˆλͺ…
price
int
가격
stock
int
재고 μˆ˜λŸ‰
createdAt
LocalDateTime
λ“±λ‘μΌμ‹œ

κ΅¬ν˜„ λͺ©λ‘

1.
Product Domain 클래슀 μž‘μ„± (Lombok @Data, @NoArgsConstructor, @AllArgsConstructor μ‚¬μš©)
β€’
idλŠ” String νƒ€μž…μœΌλ‘œ μ„ μ–Έν•˜κ³ , 등둝 μ‹œ μ„œλ²„μ—μ„œ UUIDλ₯Ό 직접 μƒμ„±ν•˜μ—¬ μ„ΈνŒ…ν•œλ‹€.
2.
ProductMapper μΈν„°νŽ˜μ΄μŠ€ μž‘μ„± (@Mapper μ–΄λ…Έν…Œμ΄μ…˜ μ‚¬μš©)
3.
ProductMapper.xml μž‘μ„± β€” μ•„λž˜ SQL을 λͺ¨λ‘ κ΅¬ν˜„ν•œλ‹€.
SQL ID
κΈ°λŠ₯
selectAll
전체 μƒν’ˆ 쑰회
selectOne
νŠΉμ • μƒν’ˆ 쑰회
insert
μƒν’ˆ 등둝 (λ“±λ‘μΌμ‹œ μžλ™ μž…λ ₯)
update
μƒν’ˆ μˆ˜μ •
delete
μƒν’ˆ μ‚­μ œ
1.
ProductController μž‘μ„± β€” μ•„λž˜ REST API μ—”λ“œν¬μΈνŠΈλ₯Ό λͺ¨λ‘ κ΅¬ν˜„ν•œλ‹€.
Method
URL
κΈ°λŠ₯
GET
/api/products
전체 μƒν’ˆ 쑰회
GET
/api/products/{id}
νŠΉμ • μƒν’ˆ 쑰회
POST
/api/products
μƒν’ˆ 등둝
PUT
/api/products/{id}
μƒν’ˆ μˆ˜μ •
DELETE
/api/products/{id}
μƒν’ˆ μ‚­μ œ
1.
React(포트 5173)μ—μ„œ ν˜ΈμΆœν•  수 μžˆλ„λ‘ CORS 섀정을 μΆ”κ°€ν•˜μ‹œμ˜€.

힌트

// com.aloha.product.domain.Product @Data @NoArgsConstructor @AllArgsConstructor public class Product { private String id; // UUID (ex. "550e8400-e29b-41d4-a716-446655440000") private String name; private int price; private int stock; private LocalDateTime createdAt; }
Java
볡사
// com.aloha.product.mapper.ProductMapper @Mapper public interface ProductMapper { List<Product> selectAll(); Product selectOne(String id); int insert(Product product); int update(Product product); int delete(String id); }
Java
볡사
<!-- resources/mapper/ProductMapper.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "<http://mybatis.org/dtd/mybatis-3-mapper.dtd>"> <mapper namespace="com.aloha.product.mapper.ProductMapper"> <!-- 전체 쑰회 --> <select id="selectAll" resultType="com.aloha.product.domain.Product"> SELECT * FROM product ORDER BY created_at DESC </select> <!-- 단건 쑰회 --> <select id="selectOne" parameterType="String" resultType="com.aloha.product.domain.Product"> SELECT * FROM product WHERE id = #{id} </select> <!-- 등둝 β€” UUIDλŠ” Controllerμ—μ„œ 생성 ν›„ product 객체에 μ„ΈνŒ…ν•˜μ—¬ 전달 --> <insert id="insert" parameterType="com.aloha.product.domain.Product"> INSERT INTO product (id, name, price, stock, created_at) VALUES (#{id}, #{name}, #{price}, #{stock}, NOW()) </insert> <!-- μˆ˜μ • --> <update id="update" parameterType="com.aloha.product.domain.Product"> UPDATE product SET name = #{name}, price = #{price}, stock = #{stock} WHERE id = #{id} </update> <!-- μ‚­μ œ --> <delete id="delete" parameterType="String"> DELETE FROM product WHERE id = #{id} </delete> </mapper>
XML
볡사
// Controller κΈ°λ³Έ ꡬ쑰 @RestController @RequestMapping("/api/products") @CrossOrigin(origins = "<http://localhost>:_____") // React(Vite) 포트 번호 μ±„μš°κΈ° public class ProductController { @Autowired private ProductMapper productMapper; // GET 전체 쑰회 @GetMapping public List<Product> getAll() { return productMapper.________(); } // GET 단건 쑰회 @GetMapping("/{id}") public Product getOne(@PathVariable String id) { return productMapper.________(id); } // POST 등둝 β€” UUIDλ₯Ό 직접 μƒμ„±ν•˜μ—¬ id에 μ„ΈνŒ… @PostMapping public Product create(@RequestBody Product product) { product.setId(UUID.randomUUID().toString()); // UUID 생성 productMapper.________(product); return product; } // PUT μˆ˜μ • @PutMapping("/{id}") public Product update(@PathVariable String id, @RequestBody Product product) { product.setId(id); productMapper.________(product); return productMapper.________(id); } // DELETE μ‚­μ œ @DeleteMapping("/{id}") public void delete(@PathVariable String id) { productMapper.________(id); } }
Java
볡사
# application.properties β€” DB μ—°κ²° 및 MyBatis μ„€μ • spring.datasource.url=jdbc:mysql://localhost:3306/[DBλͺ…]?useSSL=false&serverTimezone=Asia/Seoul spring.datasource.username=root spring.datasource.password=1234 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis mapper xml μœ„μΉ˜ mybatis.mapper-locations=classpath:mapper/*.xml # μΉ΄λ©œμΌ€μ΄μŠ€ μžλ™ λ³€ν™˜ (created_at β†’ createdAt) mybatis.configuration.map-underscore-to-camel-case=true
Plain Text
볡사
ν…Œμ΄λΈ”μ€ μ•„λž˜ SQL둜 직접 μƒμ„±ν•˜μ„Έμš”.
CREATE TABLE product ( id VARCHAR(36) PRIMARY KEY, -- UUID λ¬Έμžμ—΄ (36자) name VARCHAR(100) NOT NULL, price INT NOT NULL, stock INT NOT NULL, created_at DATETIME );
SQL
볡사
UUID.randomUUID().toString()은 "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ν˜•μ‹μ˜ 36자리 λ¬Έμžμ—΄μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€.

문제3 - React μ»΄ν¬λ„ŒνŠΈ ꡬ쑰 및 λΌμš°νŒ… μ„€μ •

β€’
λ‚œμ΄λ„ : β˜…β˜…β˜†β˜†β˜† (μ€‘ν•˜)
β€’
배점 : 20점

μš”κ΅¬μ‚¬ν•­

1.
src/pages/ 폴더 μ•ˆμ— μ•„λž˜ μ»΄ν¬λ„ŒνŠΈ νŒŒμΌμ„ μƒμ„±ν•˜μ‹œμ˜€.
파일λͺ…
μ—­ν• 
ProductList.jsx
μƒν’ˆ λͺ©λ‘ νŽ˜μ΄μ§€
ProductDetail.jsx
μƒν’ˆ 상세 νŽ˜μ΄μ§€
ProductForm.jsx
μƒν’ˆ 등둝 / μˆ˜μ • 폼 νŽ˜μ΄μ§€
1.
src/components/ 폴더 μ•ˆμ— Navbar.jsxλ₯Ό μƒμ„±ν•˜κ³  μ•„λž˜ 메뉴λ₯Ό κ΅¬μ„±ν•˜μ‹œμ˜€.
β€’
"μƒν’ˆ λͺ©λ‘" β†’ /
β€’
"μƒν’ˆ 등둝" β†’ /products/new
2.
App.jsμ—μ„œ react-router-dom을 μ΄μš©ν•˜μ—¬ μ•„λž˜ λΌμš°νŒ…μ„ μ„€μ •ν•˜μ‹œμ˜€.
경둜
λ Œλ”λ§ μ»΄ν¬λ„ŒνŠΈ
/
ProductList
/products/new
ProductForm (등둝 λͺ¨λ“œ)
/products/:id
ProductDetail
/products/:id/edit
ProductForm (μˆ˜μ • λͺ¨λ“œ)

힌트

// App.js β€” λΌμš°νŒ… κΈ°λ³Έ ꡬ쑰 import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Navbar from './components/Navbar'; import ProductList from './pages/ProductList'; // ... λ‚˜λ¨Έμ§€ import μΆ”κ°€ function App() { return ( <BrowserRouter> <Navbar /> <Routes> <Route path="/" element={<ProductList />} /> {/* λ‚˜λ¨Έμ§€ Route μΆ”κ°€ */} </Routes> </BrowserRouter> ); } export default App;
JavaScript
볡사
// Navbar.jsx β€” Link μ»΄ν¬λ„ŒνŠΈ μ‚¬μš© import { Link } from 'react-router-dom'; function Navbar() { return ( <nav> <Link to="/">μƒν’ˆ λͺ©λ‘</Link> <Link to="/products/new">μƒν’ˆ 등둝</Link> </nav> ); }
JavaScript
볡사
<a> νƒœκ·Έ λŒ€μ‹  <Link>λ₯Ό μ‚¬μš©ν•΄μ•Ό νŽ˜μ΄μ§€ μƒˆλ‘œκ³ μΉ¨ 없이 이동할 수 μžˆμŠ΅λ‹ˆλ‹€.

문제4 - axiosλ₯Ό μ΄μš©ν•œ API 연동 및 CRUD κ΅¬ν˜„

β€’
λ‚œμ΄λ„ : β˜…β˜…β˜…β˜…β˜† (상)
β€’
배점 : 25점

μš”κ΅¬μ‚¬ν•­

(1) src/api/productApi.js 파일 μž‘μ„±

axios μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜κ³ , μ•„λž˜ API ν•¨μˆ˜λ“€μ„ μž‘μ„±ν•˜μ‹œμ˜€.
ν•¨μˆ˜λͺ…
HTTP λ©”μ„œλ“œ
μ„€λͺ…
getProducts()
GET
전체 μƒν’ˆ λͺ©λ‘ 쑰회
getProduct(id)
GET
νŠΉμ • μƒν’ˆ 쑰회
createProduct(data)
POST
μƒν’ˆ 등둝
updateProduct(id, data)
PUT
μƒν’ˆ μˆ˜μ •
deleteProduct(id)
DELETE
μƒν’ˆ μ‚­μ œ

(2) ProductList.jsx κ΅¬ν˜„

β€’
μ»΄ν¬λ„ŒνŠΈκ°€ 처음 λ Œλ”λ§λ  λ•Œ 전체 μƒν’ˆ λͺ©λ‘μ„ λΆˆλŸ¬μ™€ 화면에 좜λ ₯ν•œλ‹€.
β€’
각 μƒν’ˆ ν–‰(row)에 "상세보기" λ²„νŠΌκ³Ό "μ‚­μ œ" λ²„νŠΌμ„ ν‘œμ‹œν•œλ‹€.
β€’
"μ‚­μ œ" λ²„νŠΌ 클릭 μ‹œ β†’ confirm() 확인창 ν›„ μ‚­μ œ β†’ λͺ©λ‘ μžλ™ κ°±μ‹ 

(3) ProductDetail.jsx κ΅¬ν˜„

β€’
URL의 :id νŒŒλΌλ―Έν„°λ₯Ό 읽어 ν•΄λ‹Ή μƒν’ˆ 정보λ₯Ό μ‘°νšŒν•˜μ—¬ ν‘œμ‹œν•œλ‹€.
β€’
"μˆ˜μ •" λ²„νŠΌ 클릭 μ‹œ /products/:id/edit νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.
β€’
"λͺ©λ‘μœΌλ‘œ" λ²„νŠΌ 클릭 μ‹œ / νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.

(4) ProductForm.jsx κ΅¬ν˜„

β€’
등둝 λͺ¨λ“œ(/products/new) : 빈 폼으둜 μ‹œμž‘, 제좜 μ‹œ createProduct() 호좜
β€’
μˆ˜μ • λͺ¨λ“œ(/products/:id/edit) : κΈ°μ‘΄ 데이터λ₯Ό λΆˆλŸ¬μ™€ 폼에 μ±„μš΄ ν›„, 제좜 μ‹œ updateProduct() 호좜
β€’
등둝/μˆ˜μ • μ™„λ£Œ ν›„ β†’ λͺ©λ‘ νŽ˜μ΄μ§€(/)둜 이동

힌트

// productApi.js β€” axios μΈμŠ€ν„΄μŠ€ 및 ν•¨μˆ˜ μ˜ˆμ‹œ import axios from 'axios'; const api = axios.create({ baseURL: '<http://localhost:8080/api>' }); export const getProducts = () => api.get('/products'); export const getProduct = (id) => api.get(`/products/${id}`); export const createProduct = (data) => api.______('/products', data); export const updateProduct = (id, data) => api.______(`/products/${id}`, data); export const deleteProduct = (id) => api.______(`/products/${id}`);
JavaScript
볡사
// ProductList.jsx β€” useEffect, useState μ‚¬μš© μ˜ˆμ‹œ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { getProducts, deleteProduct } from '../api/productApi'; function ProductList() { const [products, setProducts] = useState([]); const navigate = useNavigate(); // λͺ©λ‘ 뢈러였기 const fetchProducts = () => { getProducts().then(res => setProducts(res.data)); }; useEffect(() => { fetchProducts(); // μ»΄ν¬λ„ŒνŠΈ 마운트 μ‹œ 1회 μ‹€ν–‰ }, []); // μ‚­μ œ 처리 const handleDelete = (id) => { if (window.confirm('정말 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) { deleteProduct(id).then(() => fetchProducts()); // μ‚­μ œ ν›„ λͺ©λ‘ κ°±μ‹  } }; return ( <div> {products.map(product => ( <div key={product.id}> <span>{product.name}</span> <button onClick={() => navigate(`/products/${product.id}`)}>상세보기</button> <button onClick={() => handleDelete(product.id)}>μ‚­μ œ</button> </div> ))} </div> ); }
JavaScript
볡사
// ProductForm.jsx β€” 등둝/μˆ˜μ • λͺ¨λ“œ λΆ„κΈ° 처리 μ˜ˆμ‹œ import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { getProduct, createProduct, updateProduct } from '../api/productApi'; function ProductForm() { const { id } = useParams(); // μˆ˜μ • λͺ¨λ“œμ΄λ©΄ id 쑴재, 등둝 λͺ¨λ“œμ΄λ©΄ undefined const navigate = useNavigate(); const isEdit = !!id; // trueλ©΄ μˆ˜μ • λͺ¨λ“œ const [form, setForm] = useState({ name: '', price: '', stock: '' }); useEffect(() => { if (isEdit) { // μˆ˜μ • λͺ¨λ“œ: κΈ°μ‘΄ 데이터 뢈러였기 getProduct(id).then(res => setForm(res.data)); } }, [id]); const handleChange = (e) => { setForm({ ...form, [e.target.name]: e.target.value }); }; const handleSubmit = (e) => { e.preventDefault(); if (isEdit) { updateProduct(id, form).then(() => navigate('/')); } else { createProduct(form).then(() => navigate('/')); } }; return ( <form onSubmit={handleSubmit}> <input name="name" value={form.name} onChange={handleChange} placeholder="μƒν’ˆλͺ…" /> <input name="price" value={form.price} onChange={handleChange} placeholder="가격" /> <input name="stock" value={form.stock} onChange={handleChange} placeholder="재고" /> <button type="submit">{isEdit ? 'μˆ˜μ •' : '등둝'}</button> </form> ); }
JavaScript
볡사
useParams()둜 URL νŒŒλΌλ―Έν„°λ₯Ό, useNavigate()둜 νŽ˜μ΄μ§€ 이동을 μ²˜λ¦¬ν•©λ‹ˆλ‹€.

문제5 - Tailwind CSS 적용 및 μœ νš¨μ„± 검사

β€’
λ‚œμ΄λ„ : β˜…β˜…β˜…β˜†β˜† (쀑)
β€’
배점 : 10점

μš”κ΅¬μ‚¬ν•­

(1) Tailwind CSS v4 μ„€μ • μ™„μ„±

vite.config.js에 @tailwindcss/vite ν”ŒλŸ¬κ·ΈμΈμ„ λ“±λ‘ν•˜μ‹œμ˜€.
// vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' // ← μΆ”κ°€ export default defineConfig({ plugins: [ react(), tailwindcss(), // ← μΆ”κ°€ ], })
JavaScript
볡사
src/index.css의 κΈ°μ‘΄ λ‚΄μš©μ„ λͺ¨λ‘ μ§€μš°κ³  μ•„λž˜ ν•œ μ€„λ§Œ μž‘μ„±ν•˜μ‹œμ˜€.
/* src/index.css */ @import "tailwindcss";
CSS
볡사
Tailwind v4λΆ€ν„°λŠ” tailwind.config.js 파일이 ν•„μš” μ—†μŠ΅λ‹ˆλ‹€. @import "tailwindcss" ν•œ μ€„λ‘œ λͺ¨λ“  섀정이 μ™„λ£Œλ©λ‹ˆλ‹€.

(2) UI μŠ€νƒ€μΌ 적용 (Tailwind 클래슀 ν™œμš©)

β€’
μƒν’ˆ λͺ©λ‘μ€ ν…Œμ΄λΈ” λ˜λŠ” μΉ΄λ“œ(grid) ν˜•νƒœλ‘œ ν‘œμ‹œν•œλ‹€.
β€’
λ²„νŠΌμ— 배경색과 hover: 효과λ₯Ό μ μš©ν•œλ‹€.
β€’
폼 μž…λ ₯ ν•„λ“œμ— ν…Œλ‘λ¦¬μ™€ focus: μŠ€νƒ€μΌμ„ μ μš©ν•œλ‹€.

(3) μž…λ ₯ μœ νš¨μ„± 검사 μΆ”κ°€ (ProductForm.jsx)

handleSubmit ν•¨μˆ˜ λ‚΄λΆ€ 제좜 전에 μ•„λž˜ 쑰건을 κ²€μ‚¬ν•˜μ‹œμ˜€.
ν•„λ“œ
쑰건
μƒν’ˆλͺ…(name)
빈 λ¬Έμžμ—΄ λΆˆκ°€
가격(price)
0 μ΄μƒμ˜ 숫자
재고(stock)
0 μ΄μƒμ˜ μ •μˆ˜

힌트

// μœ νš¨μ„± 검사 μ˜ˆμ‹œ const handleSubmit = (e) => { e.preventDefault(); if (!form.name.trim()) { alert('μƒν’ˆλͺ…을 μž…λ ₯ν•˜μ„Έμš”.'); return; } if (form.price === '' || Number(form.price) < 0) { alert('가격은 0 μ΄μƒμ˜ 숫자λ₯Ό μž…λ ₯ν•˜μ„Έμš”.'); return; } if (form.stock === '' || Number(form.stock) < 0) { alert('μž¬κ³ λŠ” 0 μ΄μƒμ˜ 숫자λ₯Ό μž…λ ₯ν•˜μ„Έμš”.'); return; } // 검사 톡과 ν›„ API 호좜 if (isEdit) { ... } else { ... } };
JavaScript
볡사
// Tailwind μŠ€νƒ€μΌ μ˜ˆμ‹œ <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700" > 등둝 </button> <input className="border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" />
JavaScript
볡사

채점 κΈ°μ€€ μš”μ•½

문제
λ‚΄μš©
배점
문제1
ν”„λ‘œμ νŠΈ 생성 및 라이브러리 μ„€μΉ˜ λͺ…λ Ήμ–΄
20점
문제2
Spring Boot REST API (Entity / Repository / Controller / CORS)
25점
문제3
React μ»΄ν¬λ„ŒνŠΈ ꡬ쑰 및 λΌμš°νŒ… μ„€μ •
20점
문제4
axios API 연동 및 CRUD κΈ°λŠ₯ κ΅¬ν˜„
25점
문제5
Tailwind CSS 적용 및 μž…λ ₯ μœ νš¨μ„± 검사
10점
합계
100점

μ°Έκ³  β€” μˆ˜μ—… μ‹œκ°„ μ§„ν–‰ ν”„λ‘œμ νŠΈ (참쑰용, 볡사 제좜 λΆˆκ°€)