
→ 위 그림 참고: 사용자 흐름은 Home → Product List → Detail → Review 식으로 이동하고, 로그인 이후에는 My Page에서 찜목록, 등록상품, 리뷰 등을 관리할 수 있어요.
| 테이블 | 주요 컬럼 | 설명 |
|---|---|---|
| users | id, username, password, nickname, region | 사용자 정보 + 지역 기반 필터 |
| products | id, title, brand, category, price, status, seller_id, image_path | 리셀 상품 정보 |
| reviews | id, product_id, reviewer_id, rating, content, verified | 후기 및 정품 인증 여부 |
| price_logs | id, product_id, price, log_date | 시세 기록 (차트용) |
| wish_list | id, user_id, product_id | 찜한 상품 리스트 |
| regions | id, name | 사용자 지역 분류 (위치 기반 추천용) |
Kakao Map API 연동시
region필드 → 위도/경도 기반으로 확장 가능
| 기능 | Servlet URL | JSP View | 설명 |
|---|---|---|---|
| 메인 화면 | /home | home.jsp | |
| 상품 목록 | /product/list | productList.jsp | 필터 포함 |
| 상품 등록 | /product/register | productForm.jsp | Multipart 업로드 |
| 상품 상세 | /product/detail?id=1 | productDetail.jsp | |
| 시세 그래프 | /price/history?productId=1 | priceChart.jsp or Ajax | Chart.js |
| 후기 작성 | /review/write | reviewForm.jsp | |
| 마이페이지 | /mypage | myPage.jsp | |
| 찜 추가 | /wishlist/add | redirect or Ajax | |
| 로그인 | /login | login.jsp | |
| 회원가입 | /register | register.jsp |
리셀 플랫폼 ERD 다이어그램

🖼️ JSP 템플릿 화면 예시 (예: 상품 상세 페이지 productDetail.jsp)
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head><title>${product.title} - 상세 정보</title></head>
<body>
<h2>${product.title}</h2>
<p><strong>브랜드:</strong> ${product.brand}</p>
<p><strong>카테고리:</strong> ${product.category}</p>
<p><strong>상태:</strong> ${product.status}</p>
<p><strong>가격:</strong> ${product.price}원</p>
<img src="images/${product.imagePath}" alt="상품 이미지" width="300"/>
<hr/>
<h3>리셀 시세 그래프</h3>
<canvas id="priceChart" width="600" height="300"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
fetch('/price/history?productId=${product.id}')
.then(res => res.json())
.then(data => {
new Chart(document.getElementById("priceChart"), {
type: 'line',
data: {
labels: data.dates,
datasets: [{
label: '가격 추이',
data: data.prices,
fill: false,
borderColor: 'blue'
}]
}
});
});
</script>
<hr/>
<form method="post" action="/wishlist/add">
<input type="hidden" name="productId" value="${product.id}"/>
<button>찜하기</button>
</form>
</body>
</html>
🔧 Servlet 코드 샘플 ①: ProductRegisterServlet.java
@WebServlet("/product/register")
@MultipartConfig
public class ProductRegisterServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String title = request.getParameter("title");
String brand = request.getParameter("brand");
String category = request.getParameter("category");
int price = Integer.parseInt(request.getParameter("price"));
Part image = request.getPart("image");
String fileName = Paths.get(image.getSubmittedFileName()).getFileName().toString();
String uploadPath = getServletContext().getRealPath("/images");
image.write(uploadPath + "/" + fileName);
// DB에 저장
ProductDAO dao = new ProductDAO();
dao.insert(new Product(title, brand, category, price, fileName, sellerId));
response.sendRedirect("/product/list");
}
}
✍️ Servlet 코드 샘플 ②: ReviewWriteServlet.java
@WebServlet("/review/write")
public class ReviewWriteServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int productId = Integer.parseInt(request.getParameter("productId"));
int rating = Integer.parseInt(request.getParameter("rating"));
String content = request.getParameter("content");
boolean verified = Boolean.parseBoolean(request.getParameter("verified"));
HttpSession session = request.getSession();
int userId = (int) session.getAttribute("userId");
ReviewDAO dao = new ReviewDAO();
dao.insert(new Review(productId, userId, rating, content, verified));
response.sendRedirect("/product/detail?id=" + productId);
}
}
📈 Chart.js 연동 구조
가격 기록 테이블(price_logs)에서 product_id 기준으로 날짜별 가격을 가져옴
/price/history?productId=3 → JSON 형식 응답
{
"dates": ["2024-04-01", "2024-04-10", "2024-04-15"],
"prices": [120000, 118000, 125000]
}
Java에서 JSON으로 응답할 때:
response.setContentType("application/json");
PrintWriter out = response.getWriter();
out.print("{ \"dates\": [...], \"prices\": [...] }");
📍 Kakao Map 연동 구조
1 사용자 위치 등록:
<select name="region">
<option value="gangnam">강남</option>
<option value="hongdae">홍대</option>
...
</select>
2 상품 등록 시 region 정보 저장
3 상품 목록 필터링 시 현재 사용자 지역과 같은 region만 보여줌
4 Kakao Map API 사용 예:
<div id="map" style="width:100%;height:350px;"></div>
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=YOUR_KEY"></script>
<script>
var mapContainer = document.getElementById('map'),
mapOption = { center: new kakao.maps.LatLng(37.5665, 126.9780), level: 3 };
var map = new kakao.maps.Map(mapContainer, mapOption);
</script>
이후 각 상품 위치에 마커(kakao.maps.Marker) 추가로 동네 상품 시각화 가능!??????
프로젝트의 **백엔드 핵심 로직 3종 세트**를 정리
✔️ DAO 클래스 구조 ✔️ SQL 쿼리 예시 ✔️ 로그인 기능 구현 방식
DAO(Data Access Object)는 DB와의 연결을 담당하는 클래스예요.
ProductDAO.javapublic class ProductDAO {
private Connection conn;
public ProductDAO() {
this.conn = DBConnection.getConnection(); // 커넥션 관리
}
public void insert(Product product) throws SQLException {
String sql = "INSERT INTO products (title, brand, category, price, status, seller_id, image_path) VALUES (?, ?, ?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, product.getTitle());
pstmt.setString(2, product.getBrand());
pstmt.setString(3, product.getCategory());
pstmt.setInt(4, product.getPrice());
pstmt.setString(5, product.getStatus());
pstmt.setInt(6, product.getSellerId());
pstmt.setString(7, product.getImagePath());
pstmt.executeUpdate();
}
public List<Product> findAll() throws SQLException {
String sql = "SELECT * FROM products ORDER BY id DESC";
ResultSet rs = conn.createStatement().executeQuery(sql);
List<Product> list = new ArrayList<>();
while (rs.next()) {
list.add(new Product(
rs.getInt("id"),
rs.getString("title"),
rs.getString("brand"),
rs.getString("category"),
rs.getInt("price"),
rs.getString("status"),
rs.getInt("seller_id"),
rs.getString("image_path")
));
}
return list;
}
}
UserDAO.javapublic class UserDAO {
private Connection conn;
public UserDAO() {
this.conn = DBConnection.getConnection();
}
public User login(String username, String password) throws SQLException {
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return new User(
rs.getInt("id"),
rs.getString("username"),
rs.getString("nickname"),
rs.getString("region")
);
}
return null;
}
}
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
nickname VARCHAR(50),
region VARCHAR(50)
);
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
brand VARCHAR(100),
category VARCHAR(50),
price INT,
status VARCHAR(20),
seller_id INT,
image_path VARCHAR(255),
FOREIGN KEY (seller_id) REFERENCES users(id)
);
CREATE TABLE reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT,
reviewer_id INT,
rating INT,
content TEXT,
verified BOOLEAN,
FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (reviewer_id) REFERENCES users(id)
);
CREATE TABLE price_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT,
price INT,
log_date DATE,
FOREIGN KEY (product_id) REFERENCES products(id)
);
CREATE TABLE wish_list (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
product_id INT,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
login.jsp)<form method="post" action="/login">
<input type="text" name="username" placeholder="아이디" required>
<input type="password" name="password" placeholder="비밀번호" required>
<button type="submit">로그인</button>
</form>
LoginServlet.java)@WebServlet("/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UserDAO dao = new UserDAO();
try {
User user = dao.login(username, password);
if (user != null) {
HttpSession session = request.getSession();
session.setAttribute("userId", user.getId());
session.setAttribute("nickname", user.getNickname());
session.setAttribute("region", user.getRegion());
response.sendRedirect("/home");
} else {
response.sendRedirect("/login.jsp?error=true");
}
} catch (SQLException e) {
throw new ServletException("로그인 오류", e);
}
}
}
<c:if test="${empty sessionScope.userId}">
<a href="/login.jsp">로그인</a>
</c:if>
<c:if test="${not empty sessionScope.userId}">
<p>환영합니다, ${sessionScope.nickname}님!</p>
<a href="/logout">로그아웃</a>
</c:if>
LogoutServlet.java)@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getSession().invalidate();
response.sendRedirect("/home");
}
}
| 항목 | 요약 |
|---|---|
| DAO | DB 연결 → CRUD 메서드 분리 (ex. ProductDAO, UserDAO) |
| SQL | products, users, reviews, wish_list, price_logs |
| 로그인 | UserDAO.login() + 세션 저장 → JSP에서 조건 렌더링 가능 |
실무급 플랫폼으로 만드는 확장 설계
RESTful 구조 + 관리자 기능 + UI 개선 + Ajax 비동기
ALTER TABLE users ADD COLUMN role VARCHAR(10) DEFAULT 'user';
-- 'user' | 'admin' 구분
session.setAttribute("role", user.getRole()); // 'admin' or 'user'
| 기능 | URL | JSP | 설명 |
|---|---|---|---|
| 상품 전체 관리 | /admin/products | adminProducts.jsp | 등록상품 전체 조회 & 삭제 |
| 리뷰 전체 관리 | /admin/reviews | adminReviews.jsp | 부적절한 리뷰 관리 |
<c:if test="${sessionScope.role == 'admin'}">
<a href="/admin/products">관리자 메뉴</a>
</c:if>
| 등급 | 설명 | 역할 |
|---|---|---|
| user | 기본 가입 사용자 | 구매, 후기 작성 |
| seller | 리셀러 | 상품 등록 가능 |
| admin | 관리자 | 전체 관리 기능 |
ALTER TABLE users ADD COLUMN grade VARCHAR(10) DEFAULT 'user';
if(grade.equals("seller")) 조건 확인String imagePath = getServletContext().getRealPath("/images/" + product.getImagePath());
Files.deleteIfExists(Paths.get(imagePath));
String sql = "DELETE FROM products WHERE id=?";
Files.deleteIfExists()로 처리| 기능 | REST URL | Method | 설명 |
|---|---|---|---|
| 상품 조회 | /api/products | GET | 전체 목록 |
| 상품 상세 | /api/products/3 | GET | 특정 상품 |
| 상품 등록 | /api/products | POST | 등록 |
| 상품 수정 | /api/products/3 | PUT | 수정 |
| 상품 삭제 | /api/products/3 | DELETE | 삭제 |
/api/products → JSON 응답 서블릿@WebServlet("/api/products")
public class ProductListApi extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
List<Product> list = new ProductDAO().findAll();
Gson gson = new Gson();
String json = gson.toJson(list);
response.setContentType("application/json");
response.getWriter().write(json);
}
}
fetch("/api/products")
.then(res => res.json())
.then(data => console.log(data));
<c:forEach var="p" items="${products}">
<div class="card">
<img src="images/${p.imagePath}" />
<div class="info">
<h4>${p.title}</h4>
<p>${p.brand}</p>
<p>${p.price}원</p>
</div>
</div>
</c:forEach>
.card {
border: 1px solid #eee;
border-radius: 12px;
padding: 16px;
transition: 0.3s;
}
.card:hover {
box-shadow: 0 0 12px rgba(0,0,0,0.2);
}
<button onclick="addToWish(${product.id})">찜하기</button>
function addToWish(productId) {
fetch("/wishlist/add", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "productId=" + productId
}).then(res => {
if (res.ok) alert("찜 완료!");
else alert("실패!");
});
}
WishlistAddServlet)@WebServlet("/wishlist/add")
public class WishlistAddServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
int userId = (int) req.getSession().getAttribute("userId");
int productId = Integer.parseInt(req.getParameter("productId"));
new WishlistDAO().add(userId, productId);
res.setStatus(HttpServletResponse.SC_OK);
}
}
| 기능 | 설명 |
|---|---|
| 📦 배송 기능 | 배송 상태 관리, 송장 등록 |
| 💬 실시간 댓글 | WebSocket 연동 |
| 📱 반응형 UI | 모바일 최적화 |
| 📈 관리자 통계 대시보드 | 판매량/조회수/후기 등 통계 Chart.js |
| 🔐 비밀번호 해싱 | SHA256 or BCrypt 등 보안 강화 |
예시는 Spring Boot + Thymeleaf 기반으로 위 7가지 기능을 통합·구현한 구조
각 기능별로 Entity → Repository → Service → Controller → View/JS 의 순서로 예시 코드를 제
WebSocket 및 보안 설정까지 모두 포함
아래 예시는 Spring Boot + Thymeleaf 기반으로 위 7가지 기능을 통합·구현한 구조입니다.
여기서 각 기능별로 Entity → Repository → Service → Controller → View/JS 의 순서로 예시 코드를 제시하고, 마지막에 WebSocket 및 보안 설정까지 모두 포함합니다.
/ 1‑1. Entity & Repository /
@Entity
@Table(name="wish_list")
public class Wish {
@Id @GeneratedValue private Long id;
private Long userId;
private Long productId;
// getters/setters
}
public interface WishRepository extends JpaRepository<Wish, Long> {
boolean existsByUserIdAndProductId(Long userId, Long productId);
}
/ 1‑2. Service /
@Service
public class WishService {
@Autowired WishRepository repo;
public void toggleWish(Long userId, Long productId) {
if (repo.existsByUserIdAndProductId(userId, productId)) {
Wish w = repo.findByUserIdAndProductId(userId, productId);
repo.delete(w);
} else {
repo.save(new Wish(userId, productId));
}
}
}
/ 1‑3. REST Controller /
@RestController
@RequestMapping("/api/wish")
public class WishController {
@Autowired WishService wishService;
@PostMapping
public ResponseEntity<?> toggle(@RequestParam Long productId,
@AuthenticationPrincipal UserDetails user) {
wishService.toggleWish(Long.parseLong(user.getUsername()), productId);
return ResponseEntity.ok().build();
}
}
/ 1‑4. Thymeleaf + Ajax /
<button id="wishBtn"
th:text="${wished}? '💖':'🤍'"
onclick="toggleWish([[${product.id}]])"></button>
<script>
function toggleWish(productId) {
fetch('/api/wish?productId='+productId, { method:'POST', credentials:'same-origin' })
.then(r => {
if(r.ok) location.reload();
else alert('실패');
});
}
</script>
@Entity
public class Product { … }
public interface ProductRepository extends JpaRepository<Product, Long> { }
@Service
public class ProductService {
@Autowired ProductRepository repo;
public Product create(ProductDto dto, MultipartFile file) throws IOException {
String fname = UUID.randomUUID() + "-" + file.getOriginalFilename();
File up = new File("uploads/"+fname);
file.transferTo(up);
Product p = dto.toEntity();
p.setImagePath(fname);
return repo.save(p);
}
}
@RestController
@RequestMapping("/api/products")
public class ProductApiController {
@Autowired ProductService service;
@PostMapping(consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Product> create(@ModelAttribute ProductDto dto) throws IOException {
Product p = service.create(dto, dto.getImage());
return ResponseEntity.status(HttpStatus.CREATED).body(p);
}
}
<!-- productForm.html -->
<form th:action="@{/api/products}" method="post" enctype="multipart/form-data">
<input name="title"/><input name="brand"/>…
<input type="file" name="image"/>
<button type="submit">등록</button>
</form>
@Entity
public class Delivery {
@Id @GeneratedValue Long id;
@OneToOne Product product;
private String status; // e.g. READY, IN_TRANSIT, DELIVERED
private String trackingNo;
// getters/setters
}
public interface DeliveryRepository extends JpaRepository<Delivery, Long> { }
@Service
public class DeliveryService {
@Autowired DeliveryRepository repo;
public Delivery updateStatus(Long id, String status) {
Delivery d = repo.findById(id).orElseThrow();
d.setStatus(status);
return repo.save(d);
}
}
@RestController
@RequestMapping("/api/delivery")
public class DeliveryController {
@Autowired DeliveryService srv;
// 송장 등록
@PutMapping("/{id}/tracking")
public Delivery setTracking(@PathVariable Long id, @RequestParam String trackingNo) {
Delivery d = srv.findById(id);
d.setTrackingNo(trackingNo);
return srv.updateStatus(id, d.getStatus());
}
// 상태 변경
@PutMapping("/{id}/status")
public Delivery update(@PathVariable Long id, @RequestParam String status) {
return srv.updateStatus(id, status);
}
}
// pom.xml에 spring-boot-starter-websocket 추가
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public void configureMessageBroker(MessageBrokerRegistry reg) {
reg.enableSimpleBroker("/topic");
reg.setApplicationDestinationPrefixes("/app");
}
public void registerStompEndpoints(StompEndpointRegistry reg) {
reg.addEndpoint("/ws-comments").withSockJS();
}
}
@Controller
public class CommentWsController {
@Autowired CommentService srv;
@MessageMapping("/comment/{productId}")
@SendTo("/topic/comments/{productId}")
public CommentDto send(@DestinationVariable Long productId, CommentDto dto) {
return srv.save(productId, dto); // 저장 후 반환
}
}
<!-- productDetail.html -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs"></script>
<script>
let socket = new SockJS('/ws-comments');
let stomp = Stomp.over(socket);
stomp.connect({}, ()=> {
stomp.subscribe('/topic/comments/'+[[${product.id}]], msg => {
let c = JSON.parse(msg.body);
document.querySelector('#comments').innerHTML +=
`<p><b>${c.user}:</b>${c.text}</p>`;
});
});
function postComment() {
let text = document.querySelector('#commentInput').value;
stomp.send('/app/comment/'+[[${product.id}]], {}, JSON.stringify({ text }));
}
</script>
<div id="comments"></div>
<input id="commentInput"/><button onclick="postComment()">전송</button>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/css/main.css"/>
/* main.css */
.container { max-width: 1200px; margin: auto; padding: 0 16px; }
.card { display: flex; flex-direction: column; }
@media (min-width: 768px) {
.product-list { display: grid; grid-template-columns: repeat(2,1fr); gap: 16px; }
}
@media (min-width: 1024px) {
.product-list { grid-template-columns: repeat(3,1fr); }
}
@RestController
@RequestMapping("/api/admin/stats")
public class AdminStatsController {
@Autowired StatsService stats;
@GetMapping("/sales")
public Map<String, Integer> salesByDay() {
return stats.getSalesCountByDayLastWeek();
}
}
<!-- adminDashboard.html -->
<canvas id="salesChart"></canvas>
<script>
fetch('/api/admin/stats/sales')
.then(r=>r.json())
.then(data=>{
new Chart(document.getElementById('salesChart'), {
type:'bar',
data:{ labels:Object.keys(data), datasets:[{ data:Object.values(data) }] }
});
});
</script>
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 로그인/인가 설정 생략
}
@Service
public class UserService {
@Autowired PasswordEncoder pwEnc;
@Autowired UserRepository userRepo;
public void register(UserDto dto) {
dto.setPassword(pwEnc.encode(dto.getPassword()));
userRepo.save(dto.toEntity());
}
}
위 코드를 통째로 Spring Boot 프로젝트에 추가하면,
1) Ajax 찜 → 2) RESTful 상품 등록 → 3) 배송/송장 → 4) WebSocket 댓글 →
5) 반응형 UI → 6) 관리자 차트 대시보드 → 7) BCrypt 보안 →
까지 한 번에 구현됩니다.
다음 단계:
application.yml설정, 도메인별 예외처리, DTO/Validator 추가, CI/CD 파이프라인 구성 등
더 궁금한 부분이나 실제 코드 통합 예시는 언제든 요청해주세요! 😎
아래는 Spring Boot 프로젝트에 바로 적용 가능한 4가지 통합 예시입니다.
application.yml 설정spring:
application:
name: resale-platform
datasource:
url: jdbc:mysql://localhost:3306/resale_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul
username: your_db_user
password: your_db_pw
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
server:
port: 8080
error:
include-stacktrace: never
logging:
level:
root: INFO
com.yourcompany: DEBUG
jwt:
secret-key: verySecretKey1234567890
expiration-ms: 3600000
kakao:
map:
app-key: YOUR_KAKAO_APP_KEY
설명:
datasource/jpa→ MySQL 연동multipart→ 파일 업로드 용량 제한jwt→ 추후 JWT 인증용kakao.map.app-key→ 위치기반 추천 연동
@ControllerAdvice)public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@Data
@AllArgsConstructor
public class ErrorResponse {
private int status;
private String error<;
private String message;
private LocalDateTime timestamp = LocalDateTime.now();
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse body = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"NOT_FOUND",
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
ErrorResponse body = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"VALIDATION_ERROR",
msg,
LocalDateTime.now()
);
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception ex) {
ErrorResponse body = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"INTERNAL_ERROR",
"서버에서 알 수 없는 오류가 발생했습니다.",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
설명:
ResourceNotFoundException→ 404 처리MethodArgumentNotValidException→ DTO 검증 오류- 그 외 모든 예외 → 500 처리
ProductDto.java)@Data
public class ProductDto {
@NotBlank(message = "제목을 입력하세요")
private String title;
@NotBlank(message = "브랜드를 입력하세요")
private String brand;
@NotBlank(message = "카테고리를 선택하세요")
private String category;
@Min(value = 0, message = "가격은 0원 이상이어야 합니다")
private int price;
@NotNull(message = "이미지를 업로드하세요")
private MultipartFile image;
public Product toEntity(Long sellerId) {
Product p = new Product();
p.setTitle(title);
p.setBrand(brand);
p.setCategory(category);
p.setPrice(price);
p.setSellerId(sellerId);
return p;
}
}
@RestController
@RequestMapping("/api/products")
public class ProductApiController {
@Autowired ProductService service;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Product> create(
@Valid @ModelAttribute ProductDto dto,
BindingResult br,
@AuthenticationPrincipal UserDetails user
) throws IOException {
if (br.hasErrors()) {
throw new MethodArgumentNotValidException(null, br);
}
Product p = service.create(dto, dto.getImage(), Long.parseLong(user.getUsername()));
return ResponseEntity.status(HttpStatus.CREATED).body(p);
}
}
설명:
@Valid+BindingResult→ 검증 실패 시 바로 예외 발생- DTO에
@NotBlank,@Min등 표준 JSR-303 어노테이션 적용
.github/workflows/ci.yml:
name: Java CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2-
- name: Build & Test with Maven
run: mvn clean verify --settings .github/maven-settings.xml
docker-build-push:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && success()
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t yourrepo/resale-platform:${{ github.sha }} .
- name: Log in to Docker Hub
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASS }}
run: |
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
- name: Push to Docker Hub
run: docker push yourrepo/resale-platform:${{ github.sha }}
- name: Deploy to Server via SSH
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull yourrepo/resale-platform:${{ github.sha }}
docker stop resale-platform || true
docker rm resale-platform || true
docker run -d --name resale-platform \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
yourrepo/resale-platform:${{ github.sha }}
설명:
1. build-and-test: 코드 체크아웃 → JDK 세팅 → Maven 빌드/테스트
2. docker-build-push: Main 브랜치에 머지되면 Docker 이미지 빌드 → 레지스트리 푸시 → 원격 서버 배포
이제 애플리케이션 설정, 예외 처리, DTO 검증, CI/CD까지 모두 갖추셨습니다!
더 깊이 들어가고 싶은 부분이나, 실제 서버 보안·모니터링 설정 등이 필요하시면 언제든 알려주세요. 🚀