D:
Spring Boot
workspace
알집으로 압축풀면 파일명이 너무 길어서 잘안된다 !!
반디집 사용하도록 ~!
압축을 Spring Boot 폴더에 풀기 !!
D:
Spring Boot
workspace
편집기
JAVA
JDK(jdk17, jdk11) notepad, eclipse
Oracle
JDK(jdk17, jdk11) sqldeveloper
MySQL workbench
Spring Framework STS3
Spring Boot STS4
Spring Boot
Spring Boot ?
- 스프링 프레임워크의 서브 프로젝트로 만들어졌다.
스프링 부트는 스프링 프레임워크를 사용 가능한 상태로 만들어주는 도구 정도로 이해할 수 있다.
- 스프링 부트는 다른 프레임워크처럼 커맨드 도구를 제공하고 톰캣이나 제티 같은 내장 서버를 통해
복잡한 설정과 실행을 간소화했다.
스프링 부트 퀵스타트
스프링 부트로 프로젝트를 생성하면 스프링을 비롯한 어떤 라이브러리도 개발자가 신경 쓸 필요가 없다. 스프링 부트가 모든 라이브러리를 자동으로 다운로드하고 관리해준다. 스프링 컨테이너를 위한 XML 환경설정 파일 역시 작성하지 않는데, 이는 스프링 부트가 기본적으로 모든 빈(Bean) 설정을 XML이 아닌 어노테이션으로 처리하기 때문이다.
생성되는 프로젝트를 웹 프로젝트로 패키징하여 실행하려면 WAR로 설정해야 하지만
스프링 부트는 웹 애플리케이션도 JAR 파일로 패키징하여 실행할 수 있다.
- main 메소드에서 시작한다. (클래스로 만들어진다.)
클래스명: 프로젝트명+Application.java
프로젝트명은 main메소드를 주는 클래스이기 때문에 대문자로 시작하는 거를 추천한다.
메인메소드를 가지고 있는 클래스는 프로젝트명+Application.java 이렇게 생성이된다.
그래서 프로젝트명은 웬만하면 대문자로 사용하기를 추천한다.
Project: Chapter01HelloMaven
꽤나 걸린다...
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
Chapter01HelloMavenApplication.java
pom.xml
여기서 https에서 s를 빼야되는 빨간줄이 사라지는 사람이 있고 그냥되는 사람이 있다.
난 뺐다 !
Chapter01HelloMavenApplication.java
@SpringBootApplication -- 시작점이라는 의미의 어노테이션이 들어와있다.
//스프링 부트로 만든 애플리케이션 시작 클래스임을 의미
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication //스프링 부트로 만든 애플리케이션 시작 클래스임을 의미
public class Chapter01HelloMavenApplication {
public static void main(String[] args) {
SpringApplication.run(Chapter01HelloMavenApplication.class, args);
System.out.println("Hello Spring Boot !!");
}
}
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
Chapter01HelloMavenApplication.java
Chapter01HelloGradle
build.gradle
만약에 선택할 때 못했으면 mvn repository가서 직접 쳐서 가져와야한다.
Chapter01HelloGradleApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication//스프링 부트로 만든 애플리케이션 시작 클래스임을 의미
public class Chapter01HelloGradleApplication {
public static void main(String[] args) {
SpringApplication.run(Chapter01HelloGradleApplication.class, args);
}
}
이런식으로 알아서 톰캣서버를 가져오기 때문에 인텔리제이로 해도된다는 것이다 !!
main 밑에 아무것도 없는 거 보니 jsp 파일 쓰는거를 막아버렸다.
templates는 응답
static은 요청
리액트 프로젝트는 main으로 끼어들어간다 보면 된다.
리액트 스프링 프레임워크처럼 따로따로 해도되고
위에 말한것처럼 스프링부트 안에 리액트 프로젝트를 만들어볼 것이다 !!
합해보자 !!
- 스프링 부트로 만든 애플리케이션은 일반 자바 애플리케이션으로 실행할 수도 있고
웹 애플리케이션으로 실행할 수도 있다.
- 기본적으로 작성된 메인 클래스를 실행하면 웹 애플리케이션으로 실행된다.
내장 Tomcat이 구동되고 브라우저에서 전송한 요청을 처리할 수 있다.
하지만 코드를 수정하여 일반 자바 애플리케이션으로 실행하면 내장 Tomcat은 구동되지 않는다.
WebApplicationType 으로 설정할 수 있는 3가지 타입
① WebApplicationType.NONE – 웹으로 동작하지 않도록 설정
② WebApplicationType.SERVLET – 기존의 스프링 MVC를 기반으로 웹 애플리케이션을 구동하는 설정
③ WebApplicationType.REACTIVE – 스프링 5.0에서 추가된 비동기 처리와 논블로킹 입출력을 지원하는 웹플럭스(WebFlux)를 적용할 때 사용
외부 프로퍼티 사용
src/main/resources 의 application.properties 파일은 전체 프로젝트의 프로퍼티 정보를 관리하는 설정 파일이다.
자바 소스보다 application.properties 설정이 우선순위가 높다.
자바 소스에서 WebApplicationType.NONE 으로 설정했어도 프로퍼티 설정의 우선순위가 높기 때문에 웹 애플리케이션이 실행된다.
application.properties
spring framework에서
=> servlet-context.xml
=> root-context.xml
이렇게 두 가지를 했던 거를 여기선 하나가 다 해준다.
application.properties에 설정한 프로퍼티 정보들은 실제 해당 Properties 객체의 Setter 메소드가 호출되어 의존성이 주입된다는 것이다.
Ctrl 키를 누른 상태에서 server.port에 마우스를 대면 하이퍼링크로 변한다. 링크를 클릭하면 ServerProperties 클래스의 setPort() 메소드가 선택된다.
사용자가 정의한 클래스들이 자동으로 빈으로 등록되기 때문에 스프링 부트에서는 패키지 이름을 주의해서 작성해야 한다.
만약 루트 패키지인 "com.example.demo" 가 아닌 다른 패키지에 클래스를 작성하면 스프링 컨테이너는 해당 클래스를 빈으로 등록하지 않는다.
다른 패키지의 클래스까지 스캔 대상에 포함 시키려면 메인 클래스에 @ComponentScan을 추가하여 패키지를 직접 지정하면 된다.
Chapter01HelloGradleApplication.java
이제 톰캣 안 뜬다 !!
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication//스프링 부트로 만든 애플리케이션 시작 클래스임을 의미
public class Chapter01HelloGradleApplication {
public static void main(String[] args) {
//SpringApplication.run(Chapter01HelloGradleApplication.class, args);
SpringApplication springApplication = new SpringApplication(Chapter01HelloGradleApplication.class);
springApplication.setWebApplicationType(WebApplicationType.NONE);
springApplication.run(args);
System.out.println("Hello Spring Boot !");
}
}
application.properties
웹을 사용안한다고 했어도 우선권은 application.properties가 가지고 있기 때문에 톰캣서버가 잡히는 것을 알 수 있다.
포트번호를 바꿔보자
spring.application.name=Chapter01HelloGradle
#WebApplicatinType
spring.main.web-application-type=servlet
#Server Setting
server.port=9000
잘 바뀐것을 확인할 수 있다 !!
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
com/example/demo
Chapter01HelloGradleApplication.java (메인메소드)
com.example.demo.controller
HelloController.java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
public HelloController() {
System.out.println("HelloController 생성자");
}
}
먼저 잘 뜨는지부터 확인하자 !!
원래 스프링프레임워크에서는
컨트롤러로 쓰겠다 하면은 빈설정을 직접해줬어야했다.
여기 스프링에서는 알아서 끌고오는 것을 볼 수 있다 !
Component scan 이런걸 할 필요가 없다.
HelloController.java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
public HelloController() {
System.out.println("HelloController 생성자");
}
@RequestMapping(value="/")
@ResponseBody//브라우저에 바로 문자열을 뿌리기
public String index() {
return "Hello Spring Boot !!";
}
}
저 문장을 바로 브라우저로 뿌려라할 것이다. -- @ResponseBody(브라우저에 바로 뿌려주는 역할)
jsp나 html 파일 거치지말고 !!
브라우저에가서
http://localhost:9000 치면 잘나와야한다.
@RestController는 JSP 같은 뷰를 별도로 만들지 않는 대신에
컨트롤러 메소드가 리턴하는 데이터 자체를 클라이언트로 보낸다.
@RestController를 쓰게되면
@ResponseBody가 필요없어진다 !!
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
//@Controller
@RestController
public class HelloController {
public HelloController() {
System.out.println("HelloController 생성자");
}
@RequestMapping(value="/")
//@ResponseBody//브라우저에 바로 문자열을 뿌리기
public String index() {
return "Hello Spring Boot !!";
}
}
@RequestMapping(value = "/hello")
public String hello(@RequestParam(value = "name") String name) {
return "안녕하세요 " + name + "님!!";
}
http://localhost:9000/hello?변수=값
http://localhost:9000/hello?name=hong
이렇게 입력을 하게되는 방식이 GetMapping이다.
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
com/example/demo
Chapter01HelloGradleApplication.java (메인메소드)
com.example.demo.controller
HelloController.java
board.controller
BoardController.java
package board.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BoardController {
public BoardController() {
System.out.println("BoardController 생성자");
}
}
생성자에 있는 내용이 출력이 되지 안되는 이유는 빈으로 생성이 안되었다는 것이다.
그래서 화면선상에서 찍히고있지 않는 것이다 !!
com/example/demo 자신은 빈생성해준다.
com.example.demo.controller 자식까지도 해준다.
board.controller 얜 아예 연관관계가 없다.
그러므로 강제 빈 생성을 해줘야한다 !! (빈 설정 !!)
Chapter01HelloGradleApplication.java
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"com.example.demo.controller", "board.controller"})
추가하기
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"com.example.demo.controller", "board.controller"})
@SpringBootApplication//스프링 부트로 만든 애플리케이션 시작 클래스임을 의미
public class Chapter01HelloGradleApplication {
public static void main(String[] args) {
//SpringApplication.run(Chapter01HelloGradleApplication.class, args);
SpringApplication springApplication = new SpringApplication(Chapter01HelloGradleApplication.class);
springApplication.setWebApplicationType(WebApplicationType.NONE);
springApplication.run(args);
System.out.println("Hello Spring Boot !");
}
}
이제 잘 뜨는걸 확인할 수 있다 !!
만약 루트 패키지인 "com.example.demo" 가 아닌 다른 패키지에 클래스를 작성하면
스프링 컨테이너는 해당 클래스를 빈으로 등록하지 않는다.
다른 패키지의 클래스까지 스캔 대상에 포함 시키려면 메인 클래스에 @ComponentScan을 추가하여
패키지를 직접 지정하면 된다.
BoardController.java
@GetMapping(value = "board/hello")
public String hello(@RequestParam(value = "name") String name) {
return "Hello " + name + "!!!";
}
http://localhost:9000/board/hello?name=hong
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
com/example/demo
Chapter01HelloGradleApplication.java (메인메소드)
com.example.demo.controller
HelloController.java
board.controller
BoardController.java
board.bean
BoardDTO.java
package board.bean;
import java.util.Date;
import lombok.Data;
@Data
public class BoardDTO {
private int seq;
private String name;
private String subject;
private String content;
private Date logtime;
}
롬복 설정을 한 번은 해줘야하므로 sts끄고 cmd 창으로 가자 !!
여기서 터미널 열기 !!
install 하기 !!
이제 롬복 설정이 잘 됐다 !!
@RestController는 JSP 같은 뷰를 별도로 만들지 않는 대신에
컨트롤러 메소드가 리턴하는 데이터 자체를 클라이언트로 보낸다.
클라이언트에 전달되는 데이터는 문자열, DTO, 컬렉션 형태의 자바 객체인데,
자바 객체가 전달되는 경우에는 자동으로 JSON으로 변환하여 처리하게 된다.
BoardController.java
@GetMapping(value = "board/getBoard")
public BoardDTO getBoard() {
BoardDTO boardDTO = new BoardDTO();
boardDTO.setSeq(10);
boardDTO.setName("허균");
boardDTO.setSubject("홍길동전");
boardDTO.setContent("의로운 의적 !!");
boardDTO.setLogtime(new Date());
return boardDTO;
}
클라이언트에 전달되는 데이터는 문자열, DTO, 컬렉션 형태의 자바 객체인데,
자바 객체가 전달되는 경우에는 자동으로 JSON으로 변환하여 처리하게 된다.
BoardController.java
@GetMapping(value = "board/getBoardList")
public List<BoardDTO> getBoardList() {
List<BoardDTO> list = new ArrayList<>();
BoardDTO boardDTO = new BoardDTO();
boardDTO.setSeq(10);
boardDTO.setName("허균");
boardDTO.setSubject("홍길동전");
boardDTO.setContent("의로운 의적 !!");
boardDTO.setLogtime(new Date());
list.add(boardDTO);
boardDTO = new BoardDTO();
boardDTO.setSeq(11);
boardDTO.setName("김수정");
boardDTO.setSubject("아기공룡 둘리");
boardDTO.setContent("빙하타고 내려와 고길동 집에서 구박받으면서 산다 !!!");
boardDTO.setLogtime(new Date());
list.add(boardDTO);
return list;
}
System.out.println(boardDTO);
이렇게 출력했을 때 우리가 toString을 안잡아줬어도 롬복이 알아서 해주므로 주소가 아니라
이런식으로 데이터값으로 나온다 !!
@GetMapping(value = "board/getBoard")
public BoardDTO getBoard() {
BoardDTO boardDTO = new BoardDTO();
boardDTO.setSeq(10);
boardDTO.setName("허균");
boardDTO.setSubject("홍길동전");
boardDTO.setContent("의로운 의적 !!");
boardDTO.setLogtime(new Date());
System.out.println(boardDTO);
return boardDTO;
}
cd frontend
npm start
D:
Spring Boot
workspace
Chapter01HelloMaven
src/main/java
com/example/demo
Chapter01HelloGradleApplication.java (메인메소드)
com.example.demo.controller
HelloController.java
board.controller
BoardController.java
Board2Controller.java
board.bean
BoardDTO.java
src
main
frontend (리액트)
src
App.js
components
BoardInput.jsx
BoardList.jsx
http://localhost:9000/board2/boardInput
http://localhost:9000/board2/boardList
=> 입력시 seq는 useRef로 처리한다.
name, subject, content 입력받는다.
package board.bean;
import java.util.Date;
import lombok.Data;
@Data
public class BoardDTO {
private int seq;
private String name;
private String subject;
private String content;
private Date logtime;
}
Board2Controller.java
package board.controller;
import java.util.ArrayList;
import java.util.Date; // Date import 추가
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import board.bean.BoardDTO;
@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
@RestController
public class Board2Controller {
private List<BoardDTO> list = new ArrayList<>();
private AtomicInteger seqCounter = new AtomicInteger(1);
@PostMapping("/boardInput")
public void createBoard(@RequestBody BoardDTO boardDTO) {
boardDTO.setSeq(seqCounter.getAndIncrement());
boardDTO.setLogtime(new Date());
System.out.println("번호: " + boardDTO.getSeq());
System.out.println("이름: " + boardDTO.getName());
System.out.println("제목: " + boardDTO.getSubject());
System.out.println("내용: " + boardDTO.getContent());
System.out.println("날짜: " + boardDTO.getLogtime());
list.add(boardDTO);
}
@GetMapping("boardList")
public List<BoardDTO> getBoardList() {
return list;
}
}
import React from 'react';
import { BrowserRouter as Router, Routes, Route, BrowserRouter, Link } from 'react-router-dom';
import BoardInput from './components/BoardInput';
import BoardList from './components/BoardList';
import './css/style.css';
const App = () => {
return (
<BrowserRouter>
<>
<nav className='menunav'>
<ul>
<li><Link to='board/input' >글쓰기</Link></li>
<li><Link to='board/list' >글목록</Link></li>
</ul>
</nav>
<Routes>
<Route path="/board/input" element={<BoardInput />} />
<Route path="/board/list" element={<BoardList />} />
</Routes>
</>
</BrowserRouter>
);
};
export default App;
import React, { useRef, useState } from 'react';
import axios from 'axios';
import styles from '../css/BoardInputForm.module.css';
import { useNavigate } from 'react-router-dom';
const BoardInput = () => {
const seq = useRef(1);
const [name, setName] = useState('');
const [subject, setSubject] = useState('');
const [content, setContent] = useState('');
const [nameDiv, setNameDiv] = useState('');
const [subjectDiv, setSubjectDiv] = useState('');
const [contentDiv, setContentDiv] = useState('');
const navigate = useNavigate();
const onSubmit = (e) => {
e.preventDefault();
setNameDiv('');
setSubjectDiv('');
setContentDiv('');
if (!name) {
setNameDiv('이름을 입력해주세요');
} else if (!subject) {
setSubjectDiv('제목을 입력해주세요');
} else if (!content) {
setContentDiv('내용을 입력해주세요');
} else {
const id = seq.current;
seq.current++;
const boardData = {
seq: id,
name: name,
subject: subject,
content: content
};
axios
.post('http://localhost:9000/boardInput', boardData, { // boardData를 요청 본문으로 전송
withCredentials: true
})
.then(res => {
alert('게시글 작성에 성공하였습니다.');
navigate('/board/list');
})
.catch(err => {
console.error(err);
alert('게시글 작성에 실패하였습니다.');
});
}
};
const onReset = () => {
setName('');
setSubject('');
setContent('');
setNameDiv('');
setSubjectDiv('');
setContentDiv('');
};
return (
<div className={styles['board-write-container']}>
<h2 className={styles['board-list-title']}>글쓰기</h2>
<form className={styles['board-write-box']} onSubmit={onSubmit}>
<div>
<label className={styles['board-write-title']}>이름</label>
<input
type="text"
className={styles['board-write-input']}
name='name'
value={name}
onChange={e => setName(e.target.value)}
/>
<div id='nameDiv' className={styles['div']}>{nameDiv}</div>
</div>
<div>
<label className={styles['board-write-title']}>제목</label>
<input
type="text"
className={styles['board-write-input']}
name='subject'
value={subject}
onChange={e => setSubject(e.target.value)}
/>
<div id='subjectDiv' className={styles['div']}>{subjectDiv}</div>
</div>
<div>
<label className={styles['board-write-title']}>내용</label>
<textarea
className={styles['board-write-input']}
name='content'
value={content}
onChange={e => setContent(e.target.value)}
rows={5}
/>
<div id='contentDiv' className={styles['div']}>{contentDiv}</div>
</div>
<div className={styles['button-container']}>
<button className={styles['board-write-button']} type="submit">글작성</button>
<button className={styles['board-reset-button']} type="button" onClick={onReset}>초기화</button>
</div>
</form>
</div>
);
};
export default BoardInput;
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import styles from '../css/BoardListForm.module.css';
const BoardList = () => {
const [list, setList] = useState([]);
useEffect(() => {
const fetchBoardList = async () => {
try {
const res = await axios.get('http://localhost:9000/boardList');
console.log('응답 데이터:', res.data);
setList(res.data);
} catch (error) {
console.error('에러:', error);
}
};
fetchBoardList();
}, []);
return (
<div className={styles['board-list-container']}>
<h2 className={styles['board-list-title']}>게시글 목록</h2>
<div className={styles['board-list']}>
<div className={`${styles['board-list-item']} ${styles['board-list-header']}`}>
<h3>번호</h3>
<h3>이름</h3>
<h3>제목</h3>
<h3>내용</h3>
<h3>날짜</h3>
</div>
{list.length > 0 ? (
list.map((data, index) => (
<div key={index} className={styles['board-list-item']}>
<p>{data.seq}</p>
<p>{data.name}</p>
<p>{data.subject}</p>
<p>{data.content}</p>
<p>{data.logtime}</p>
</div>
))
) : (
<p>게시글이 없습니다.</p>
)}
</div>
</div>
);
};
export default BoardList;
.menunav {width: 100%; background: #FFDFF6; padding: 25px 0;}
.menunav ul {display: flex; justify-content: center;}
.menunav ul li {list-style: none; margin-right: 30px; }
.menunav ul li a {transition: 0.3s; text-decoration: none;}
.menunav ul li a:hover {color: tomato;}
.main {width: 1000px; margin: 50px auto; text-align: center;}
.main h1 {font-size: 70px; margin-bottom: 15px;}
.main h2 {font-size: 50px;}
.products {width: 1100px; margin: 50px auto;}
.products h2 {font-size: 40px; margin-bottom: 20px; font-weight: 600;}
.products div {display: flex; justify-content: space-between;}
.products article {border: 1px solid #dcdcdc; padding: 20px;}
.products article h3 {font-size: 25px; margin-bottom: 15px; text-align: center; color: tomato;}
.products article img {width: 200px;}
.item {width: 1000px; margin: 50px auto; text-align: center;}
.item h2 {font-size: 50px; margin-bottom: 20px; color: tomato;}
.item h3 {font-size: 30px; margin-bottom: 15px;}
.item img {width: 300px;}
.item p:last-of-type {margin-top: 20px; font-size: 20px; margin-bottom: 15px;}
.item button {width: 200px; height: 40px; background: #000; color: #fff; border: none;}
.board-write-container {
display: flex;
justify-content: center;
align-items: center;
height: 50vh; /* 필요에 따라 높이 조정 */
flex-direction: column;
}
.board-write-box {
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
width: 400px; /* 너비를 적절히 조정 */
box-shadow: 0px 4px 10px rgba(82, 80, 80, 0.1);
}
.board-write-title {
margin-bottom: 10px;
font-weight: bold;
}
.board-write-input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #ddd;
margin-bottom: 10px;
}
.board-write-button {
width: 50%;
padding: 10px;
border-radius: 4px;
border: none;
background-color: #FFDFF6;
color: black;
font-weight: bold;
cursor: pointer;
}
.board-write-button:hover {
background-color: #f5b7e3;
}
.div {
color: red;
font-size: 10pt;
margin-bottom: 10px;
}
.button-container {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.board-reset-button {
width: 48%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
background-color: #EDE1FF;
color: black;
font-weight: bold;
cursor: pointer;
}
.board-reset-button:hover {
background-color: #e0e0e0;
}
.board-list-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
width: 100%;
}
.board-list-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.board-list {
width: 100%;
max-width: 800px;
border-collapse: collapse;
text-align: left;
margin: 10px 0;
}
.board-list-header {
background-color: #ffdcf4; /* 헤더 배경색 적용 */
font-weight: bold;
}
.board-list-item {
display: grid;
grid-template-columns: 1fr 2fr 3fr 2fr 1fr;
padding: 15px;
border-bottom: 1px solid #ddd;
}
.board-list-item:nth-child(even) {
background-color: #fafafa;
}
.board-list-item h3, .board-list-item p {
margin: 0;
padding: 5px;
}
.board-list-item p a {
text-decoration: none;
color: inherit;
transition: color 0.3s;
}
.board-list-item p a:hover {
color: tomato;
;
}
.post-title {
font-size: 16px;
font-weight: bold;
}
.post-content {
font-size: 14px;
color: #555;
}
.post-date {
font-size: 12px;
color: gray;
}
'Spring Boot' 카테고리의 다른 글
DAY 89 - JPA (2024.11.12) (1) | 2024.11.12 |
---|---|
DAY 88 - JPA (2024.11.11) (0) | 2024.11.12 |
DAY 86 - Spring Boot DB연결 + Thymeleaf (2024.11.07) (1) | 2024.11.08 |
DAY 85 - Thymeleaf (2024.11.06) (0) | 2024.11.06 |
DAY 84 - Thymeleaf (2024.11.05) (0) | 2024.11.06 |