๐Ÿ›’ ์ƒํ’ˆ ์ถ”๊ฐ€๊ธฐ๋Šฅ 2 & Navbar ๋งŒ๋“ค๊ธฐ

EthAnalogยท2025๋…„ 8์›” 20์ผ

Spring Boot

๋ชฉ๋ก ๋ณด๊ธฐ
12/16
post-thumbnail

โœ… ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ธ๊ฐ€?

  • /write ํผ์—์„œ title, price ์ž…๋ ฅ โ†’ /add๋กœ POST
  • ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆ ํ›„ DB ์ €์žฅ (ItemRepository.save)
  • ์ƒ๋‹จ Navbar๋ฅผ ๋ชจ๋“  ํŽ˜์ด์ง€์— ๊ณตํ†ต ํฌํ•จ

๐Ÿ‘‰ ์™œ ์ด๊ฑธ ๋ฐฐ์›Œ์•ผ ํ•˜์ง€?

  • ์‹ค๋ฌด์˜ 90%๋Š” ํผ ์ž…๋ ฅ โ†’ ๊ฒ€์ฆ โ†’ ์ €์žฅ ํ๋ฆ„
  • DTO/์—”ํ‹ฐํ‹ฐ ๋ฐ”์ธ๋”ฉ(@ModelAttribute)๊ณผ ๊ณตํ†ต UI ์žฌ์‚ฌ์šฉ์€ ์œ ์ง€๋ณด์ˆ˜ ํ•ต์‹ฌ
  • CRUD, ๋กœ๊ทธ์ธ, ์ฃผ๋ฌธ ๋“ฑ ๊ธฐ๋Šฅ ํ™•์žฅ์— ์ง๊ฒฐ๋˜๋Š” ํŒจํ„ด

๐Ÿ“š ๊ฐœ๋… ์ •๋ฆฌ

ํ‚ค์›Œ๋“œ์„ค๋ช…
@ModelAttribute์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ž๋ฐ” ๊ฐ์ฒด์— ์ž๋™ ๋ฐ”์ธ๋”ฉ
@PostMappingPOST ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ ๋งคํ•‘
ItemRepository.save()JPA๋กœ ํ–‰ ์ถ”๊ฐ€/์ˆ˜์ •
th:fragment์žฌ์‚ฌ์šฉํ•  HTML ๋ธ”๋ก ์ •์˜
th:replace์ •์˜๋œ fragment๋ฅผ ์น˜ํ™˜ํ•ด ํฌํ•จ
templates vs staticํ…œํ”Œ๋ฆฟ(๋™์ )์€ templates์—์„œ return "๋ทฐ๋ช…" / ์ •์ ์€ static์—์„œ URL ์ง์ ‘ ์ ‘๊ทผ

โš™๏ธ ๊ตฌํ˜„ ํ๋ฆ„ ๋ฐ ์ฝ”๋“œ

1) ์—”ํ‹ฐํ‹ฐ & ๋ฆฌํฌ์ง€ํ† ๋ฆฌ

๐Ÿ“„ src/main/java/com/apple/shop/item/Item.java

package com.apple.shop.item;

import jakarta.persistence.*;

@Entity
public class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private Integer price;

    // getter/setter
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public Integer getPrice() { return price; }
    public void setTitle(String title) { this.title = title; }
    public void setPrice(Integer price) { this.price = price; }
}
  • @Entity : JPA๊ฐ€ ํ…Œ์ด๋ธ”๊ณผ ๋งคํ•‘
  • @Id @GeneratedValue : ๊ธฐ๋ณธํ‚ค ์ž๋™์ƒ์„ฑ
  • title, price๋Š” ์ปฌ๋Ÿผ์œผ๋กœ ๋งคํ•‘๋จ(๊ธฐ๋ณธ ์ „๋žต)

๐Ÿ“„ src/main/java/com/apple/shop/item/ItemRepository.java

package com.apple.shop.item;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, Long> {
}
  • JpaRepository๋งŒ ์ƒ์†ํ•ด๋„ save, findAll ๋“ฑ ๊ธฐ๋ณธ CRUD ์ œ๊ณต

2) ์ปจํŠธ๋กค๋Ÿฌ: ์ˆ˜๋™ ๋ฐ”์ธ๋”ฉ โ†’ ์ž๋™ ๋ฐ”์ธ๋”ฉ(@ModelAttribute)

๐Ÿ“„ src/main/java/com/apple/shop/BasicController.java

package com.apple.shop;

import com.apple.shop.item.Item;
import com.apple.shop.item.ItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class BasicController {

    @Autowired
    private ItemRepository itemRepository;

    // ์ž‘์„ฑ ํผ
    @GetMapping("/write")
    public String write() {
        return "write"; // templates/write.html
    }

    // (A) ์ˆ˜๋™ ๋ฐ”์ธ๋”ฉ ์˜ˆ์‹œ
    @PostMapping("/add-manual")
    public String addManual(@RequestParam String title,
                            @RequestParam Integer price) {
        if (title == null || title.isBlank() || price == null || price < 0) {
            return "redirect:/write";
        }
        Item item = new Item();
        item.setTitle(title);
        item.setPrice(price);
        itemRepository.save(item);
        return "redirect:/list";
    }

    // (B) @ModelAttribute ์ž๋™ ๋ฐ”์ธ๋”ฉ
    @PostMapping("/add")
    public String add(@ModelAttribute Item item) {
        if (item.getTitle() == null || item.getTitle().isBlank()
                || item.getPrice() == null || item.getPrice() < 0) {
            return "redirect:/write";
        }
        itemRepository.save(item);
        return "redirect:/list";
    }

    // ๋ชฉ๋ก ํŽ˜์ด์ง€(์ƒ˜ํ”Œ)
    @GetMapping("/list")
    public String list() {
        return "list"; // templates/list.html
    }
}
  • return "write": templates/ ๊ธฐ์ค€์œผ๋กœ write.html ๋ Œ๋”
  • (A) @RequestParam์œผ๋กœ ์ง์ ‘ ๊ฐ’ ๊บผ๋‚ด์„œ Item ์ฑ„์šฐ๊ธฐ
  • (B) @ModelAttribute Item item์œผ๋กœ ์ž๋™ ๋ฐ”์ธ๋”ฉ (ํผ name โ†โ†’ ํ•„๋“œ๋ช… ๋งค์นญ)

3) Navbar fragment ์ •์˜ & ์žฌ์‚ฌ์šฉ

๐Ÿ“„ src/main/resources/templates/nav.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Nav</title>
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;800&display=swap" rel="stylesheet">
  <style>
    .nav { display: flex; padding: 10px; align-items: center; font-family: 'Montserrat', sans-serif; }
    .nav a { margin-right: 10px; text-decoration: none; font-weight: 400; letter-spacing: -0.5px; }
    .nav .logo { font-weight: 800; }
    input, button { padding: 8px 13px; margin-top: 5px; border: 1px solid grey; border-radius: 4px; vertical-align: middle; }
    button { background: black; color: white; border: none; }
    input { display: block; }
  </style>
</head>
<body>
<div class="nav" th:fragment="navbar">
  <a class="logo" th:href="@{/}">SpringMall</a>
  <a th:href="@{/list}">List</a>
  <a th:href="@{/write}">Write</a>
</div>
</body>
</html>
  • th:fragment="navbar": ์žฌ์‚ฌ์šฉ ๋ธ”๋ก ์ •์˜
  • ๊ธ€๊ผด/CSS ํฌํ•จ โ†’ ๊ฐ ํŽ˜์ด์ง€์— ์น˜ํ™˜๋˜์–ด ์‚ฌ์šฉ

๐Ÿ“„ src/main/resources/templates/write.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>์ƒํ’ˆ ๋“ฑ๋ก</title>
</head>
<body>
  <div th:replace="~{nav.html :: navbar}"></div>

  <h3>์ƒํ’ˆ ๋“ฑ๋ก</h3>
  <form th:action="@{/add}" method="post">
    <input name="title" placeholder="์ƒํ’ˆ๋ช…">
    <input name="price" placeholder="๊ฐ€๊ฒฉ" type="number" min="0">
    <button type="submit">๋“ฑ๋ก</button>
  </form>
</body>
</html>
  • th:replace="~{nav.html :: navbar}": div ์ „์ฒด๋ฅผ Navbar๋กœ ์น˜ํ™˜
  • th:action="@{/add}": ์ปจํ…์ŠคํŠธ ๊ฒฝ๋กœ ๊ณ ๋ คํ•œ ์•ˆ์ „ํ•œ URL ๋ฐ”์ธ๋”ฉ

์ฐธ๊ณ 

  • th:insert๋Š” ์•ˆ์— ์‚ฝ์ž…, th:replace๋Š” ์น˜ํ™˜(๋ณดํ†ต replace ์‚ฌ์šฉ)
  • fragment๋Š” ์—ฌ๋Ÿฌ ํŒŒ์ผ์—์„œ ๋ฐ˜๋ณต ์‚ฌ์šฉ OK
  • nav.html :: navbar('๋ฐ์ดํ„ฐ1')์ฒ˜๋Ÿผ ์ธ์ž ์ „๋‹ฌ๋„ ๊ฐ€๋Šฅ(ํ•„์š” ์‹œ ๊ฒ€์ƒ‰)

๐Ÿ“Œ ์‚ฌ์šฉ๋œ ๊ฐœ๋… ์š”์•ฝ

๋ฌธ๋ฒ•/๋ฉ”์„œ๋“œ์„ค๋ช…
@ModelAttributeํŒŒ๋ผ๋ฏธํ„ฐ โ†’ ๊ฐ์ฒด ์ž๋™ ๋งคํ•‘
@PostMapping("/add")ํผ POST ์ฒ˜๋ฆฌ
Repository.save()JPA ์ €์žฅ(Insert/Update)
th:fragment๊ณตํ†ต UI ๋ธ”๋ก ์ •์˜
th:replace๋‹ค๋ฅธ ํ…œํ”Œ๋ฆฟ์—์„œ fragment ์น˜ํ™˜
templates/*.htmlreturn "๋ทฐ๋ช…"์œผ๋กœ ๋ Œ๋”๋ง ๋Œ€์ƒ

๐Ÿ’ก ์ด๋Ÿฐ ๊ณณ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”

  • ์ƒํ’ˆ/๊ฒŒ์‹œ๊ธ€/์ฃผ๋ฌธ ๋“ฑ๋ก ํผ
  • ํ—ค๋”/ํ‘ธํ„ฐ/์‚ฌ์ด๋“œ๋ฐ” ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ
  • ๋‹ค๊ตญ์–ด/๋ธŒ๋žœ๋”ฉ ๋ณ€๊ฒฝ ์‹œ ํ•œ ๊ณณ ์ˆ˜์ • โ†’ ์ „ ํŽ˜์ด์ง€ ๋ฐ˜์˜

โœ๏ธ ๊ฐœ์ธ ์ •๋ฆฌ ๋ฐ ํšŒ๊ณ 

  • @ModelAttribute๋กœ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ œ๊ฑฐ โ†’ ๊ฐ€๋…์„ฑโ†‘
  • Navbar๋ฅผ fragment๋กœ ๋นผ๋‹ˆ ๋ณต๋ถ™ ์ง€์˜ฅ ํƒˆ์ถœ, ๋ณ€๊ฒฝ ๋น„์šฉโ†“

๐Ÿ”‘ ์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ 3์ค„ ์š”์•ฝ

  1. @ModelAttribute๋กœ ํผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ ์ž๋™ ๋ฐ”์ธ๋”ฉ
  2. itemRepository.save(item)๋กœ DB ์ €์žฅ ํ›„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
  3. th:fragment + th:replace๋กœ Navbar ์žฌ์‚ฌ์šฉ

0๊ฐœ์˜ ๋Œ“๊ธ€