Search
Duplicate

λ©”μ„œλ“œ κΆŒν•œ 관리

λ©”μ„œλ“œ κΆŒν•œ 관리

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μ • ν΄λž˜μŠ€κ°€ μ•„λ‹Œ, 컨트둀러 μš”μ²­ λ©”μ†Œλ“œμ—μ„œ λ©”μ†Œλ“œ μˆ˜μ€€μ—μ„œμ˜ κΆŒν•œμ„ κ΄€λ¦¬ν•˜λŠ” 방법

μ μš©λ°©λ²•

β€’
μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μ • 클래슀(SecurityConfig.java)
β—¦
@EnableMethodSecurity (spring boot 3.2~)
β—¦
@EnableGlobalMethodSecurity (spring boot ~3.2)
β€’
컨트둀러 λ©”μ†Œλ“œμ— κΆŒν•œ μ œμ–΄
β—¦
@Secured
β—¦
@PreAuthorize
β—¦
@PostAuthorize

μŠ€ν”„λ§ μ‹œνλ¦¬ν‹° μ„€μ • 클래슀(SecurityConfig.java)

β€’
@EnableMethodSecurity (spring boot 3.2~)
β€’
@EnableGlobalMethodSecurity (spring boot ~3.2)

@EnableMethodSecurity

spring boot 3.2 버전 λΆ€ν„°λŠ” @EnableGlobalMethodSecurity λŠ” depreacated
@EnableMethodSecurity ꢌμž₯
λ©”μ†Œλ“œ μˆ˜μ€€μ—μ„œμ˜ λ³΄μ•ˆ μ„€μ • κΈ°λŠ₯을 ν™œμ„±ν™”ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜

μ£Όμš” 속성

속성
μ„€λͺ…
μ‚¬μš© 예제
securedEnabled
@Secured μ–΄λ…Έν…Œμ΄μ…˜ ν™œμ„±ν™” - μ—­ν• (Role) 기반 μ ‘κ·Ό μ œμ–΄λ₯Ό κ°€λŠ₯ν•˜κ²Œ 함
@Secured("ROLE_ADMIN")
prePostEnabled
@PreAuthorize 및 @PostAuthorize μ–΄λ…Έν…Œμ΄μ…˜μ„ ν™œμ„±ν™” - ν‘œν˜„μ‹ 기반 μ œμ–΄λ₯Ό κ°€λŠ₯ν•˜κ²Œ 함
@PreAuthorize("hasRole('ADMIN')") @PostAuthorize("returnObject.username == authentication.name")

securedEnabled = true

μ—­ν• (Role; κΆŒν•œ) 기반으둜 κ°„λ‹¨ν•œ κΆŒν•œ μ œμ–΄
@Slf4j @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig {
Java
볡사
@Secured("ROLE_USER") @GetMapping("/path/to") public String getMethod() { }
Java
볡사
ROLE_USER, ROLE_ADMIN λ“±μ˜ κΆŒν•œμ„ κ°€μ§„ μ‚¬μš©μž μš”μ²­λ§Œ μ²˜λ¦¬κ°€ λ˜λ„λ‘ ν•„ν„°λ§ν•˜κ²Œ λ©λ‹ˆλ‹€.

prePostEnabled = true

@Slf4j @Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { }
Java
볡사
@PreAuthorize("hasRole('ADMIN')") @GetMapping("/path/to") public String getMethod() { }
Java
볡사

μ μš©λ˜λŠ” μ£Όμš” ν•„ν„°

β€’
MethodSecurityInterceptor
β—¦
λ©”μ†Œλ“œ λ³΄μ•ˆμ„ μ‹€μ œλ‘œ μˆ˜ν–‰ν•˜λŠ” 핡심 ν•„ν„°μž…λ‹ˆλ‹€.
β—¦
@Secured와 같은 μ–΄λ…Έν…Œμ΄μ…˜μ˜ κΆŒν•œ μ œμ–΄λ₯Ό μ²˜λ¦¬ν•˜λ©°, λ©”μ†Œλ“œ 호좜 전에 인증된 μ‚¬μš©μžμ˜ κΆŒν•œμ„ ν™•μΈν•©λ‹ˆλ‹€.
β€’
FilterSecurityInterceptor
β—¦
Spring Security의 HTTP μš”μ²­ λ³΄μ•ˆ ν•„ν„°λ‘œ, λ©”μ†Œλ“œ λ³΄μ•ˆλ³΄λ‹€λŠ” URL νŒ¨ν„΄ 기반의 μ ‘κ·Ό μ œμ–΄λ₯Ό μ²˜λ¦¬ν•©λ‹ˆλ‹€.

@EnableGlobalMethodSecurity

spring boot 3.2 버전 λΆ€ν„°λŠ” @EnableGlobalMethodSecurity λŠ” depreacated
@EnableMethodSecurity ꢌμž₯
λ©”μ†Œλ“œ μˆ˜μ€€μ—μ„œμ˜ λ³΄μ•ˆ μ„€μ • κΈ°λŠ₯을 ν™œμ„±ν™”ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜

컨트둀러 λ©”μ†Œλ“œμ— κΆŒν•œ μ œμ–΄

β€’
@Secured
β€’
@PreAuthorize
β€’
@PostAuthorized
μ–΄λ…Έν…Œμ΄μ…˜
적용 μ‹œμ 
μ£Όμš” νŠΉμ§•
@Secured
λ©”μ†Œλ“œ 호좜 μ „
- μ—­ν• (Role) 기반의 κ°„λ‹¨ν•œ λ³΄μ•ˆ. - ROLE_ 접두사 ν•„μš” - SpEL 미지원.
@PreAuthorize
λ©”μ†Œλ“œ 호좜 μ „
- SpEL을 μ‚¬μš©ν•œ ν‘œν˜„μ‹ 기반 λ³΄μ•ˆ. - 동적 κΆŒν•œ 검사 및 λ³΅μž‘ν•œ 쑰건 처리 κ°€λŠ₯.
@PostAuthorize
λ©”μ†Œλ“œ 호좜 ν›„
- λ°˜ν™˜κ°’μ„ 기반으둜 λ³΄μ•ˆ 검사. - SpEL 지원. - λ°˜ν™˜κ°’ 기반의 쑰건 처리 κ°€λŠ₯.

@Secured

λ‹¨μˆœν•œ μ—­ν• (Role) 기반 κΆŒν•œ 검사 - μš”μ²­ μ•ž λ‹¨μ—μ„œ κΆŒν•œμ œμ–΄
@Controller @RequestMapping("/board") public class BoardController { // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)λ₯Ό κ°€μ§„ μ‚¬μš©μž μš”μ²­λ§Œ μ²˜λ¦¬ν•œλ‹€. @Secured("ROLE_USER") @GetMapping("/{id}") public String getBoard(@PathVariable("id") String id) { ... } }
Java
볡사

@PreAuthorize

λ‹¨μˆœ κΆŒν•œ 검사 및 SpEL(μŠ€ν”„λ§ ν‘œν˜„μ‹)을 기반으둜 λ³΅μž‘ν•œ 쑰건의 κΆŒν•œ 검사 - μš”μ²­ μ•ž λ‹¨μ—μ„œ κΆŒν•œμ œμ–΄
@Controller @RequestMapping("/board") public class BoardController { // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)λ₯Ό κ°€μ§„ μ‚¬μš©μž μš”μ²­λ§Œ μ²˜λ¦¬ν•œλ‹€. @PreAuthorize("hasRole('USER')") // μ‚¬μš©μž κΆŒν•œ(ROLE_USER), κ΄€λ¦¬μž κΆŒν•œ(ROLE_ADMIN)λ₯Ό κ°€μ§„ μ‚¬μš©μž μš”μ²­λ§Œ μ²˜λ¦¬ν•œλ‹€. // @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @GetMapping("/{id}") public String getBoard(@PathVariable("id") String id) { ... } }
Java
볡사

@PostAuthorized

λ‹¨μˆœ κΆŒν•œ 검사 및 SpEL(μŠ€ν”„λ§ ν‘œν˜„μ‹)을 기반으둜 λ³΅μž‘ν•œ 쑰건의 κΆŒν•œ 검사 - λ©”μ†Œλ“œ μ‹€ν–‰ ν›„ λ°˜ν™˜ 값에 λŒ€ν•œ κΆŒν•œ 검사
β€’
μš”μ²­ μžμ›(κ²Œμ‹œκΈ€)에 λŒ€ν•΄μ„œ μž‘μ„±μžλ§Œ μ‘°νšŒκ°€ κ°€λŠ₯ν•œ 경우
@RestController @RequestMapping("/board") public class BoardController { @Autowried BoardService boardService; // λ°˜ν™˜λœ κ²Œμ‹œκΈ€ 객체의 userNo 와 인증된 μ‚¬μš©μžμ˜ User.no κ°€ μΌμΉ˜ν•˜λŠ”μ§€ 확인 @PostAuthorize("returnObject.userNo == authentication.principal.user.no") @GetMapping("/{id}") public Board getBoard(@PathVariable("id") String id) { Board board = boardService.select(id); String userNo = board.getUserNo(); return board; } }
Java
볡사
β€’
응닡 κ²°κ³Ό
β—¦
μž‘μ„±μž 본인인 경우, μš”μ²­ν•œ κ²Œμ‹œκΈ€ 정보가 응닡
β—¦
μž‘μ„±μž 본인이 μ•„λ‹Œ 경우, 403 Forbidden μ—λŸ¬ 응닡
β€’
returnObject
β—¦
μš”μ²­ 컨트둀러 λ©”μ†Œλ“œμ—μ„œ λ°˜ν™˜ν•˜λŠ” 객체λ₯Ό μ°Έμ‘°ν•˜λŠ” ν‚€μ›Œλ“œ
β—¦
μ—¬κΈ°μ„œλŠ”, return board; Board νƒ€μž…μ˜ κ²Œμ‹œκΈ€ 정보 객체λ₯Ό 가리킨닀.

SpEL( Spring Expression Language ) κΆŒν•œ μ œμ–΄ ν•¨μˆ˜

ν•¨μˆ˜
μ„€λͺ…
μ‚¬μš© μ˜ˆμ‹œ
hasRole('role')
μ‚¬μš©μž κΆŒν•œμ΄ μ§€μ •λœ μ—­ν• (role)인지 확인. ROLE_ 접두사 μƒλž΅κ°€λŠ₯ ROLE_ 접두사가 μžλ™μœΌλ‘œ λΆ™μŒ
@PreAuthorize("hasRole('ADMIN')")
hasAuthority('authority')
μ‚¬μš©μž κΆŒν•œμ΄ μ§€μ •λœ κΆŒν•œ(authority)인지 확인. ROLE_ 접두사 포함
@PreAuthorize("hasAuthority('ROLE_USER')")
isAuthenticated()
μ‚¬μš©μžκ°€ 인증된 μƒνƒœμΈμ§€ 확인. (둜그인 μ—¬λΆ€)
@PreAuthorize("isAuthenticated()")
isAnonymous()
μ‚¬μš©μžκ°€ 읡λͺ…(비인증) μƒνƒœμΈμ§€ 확인.
@PreAuthorize("isAnonymous()")
permitAll()
λͺ¨λ“  μ‚¬μš©μžκ°€ μ ‘κ·Όν•  수 μžˆλ„λ‘ ν—ˆμš©.
@PreAuthorize("permitAll()")
denyAll()
λͺ¨λ“  μ‚¬μš©μžμ˜ 접근을 κ±°λΆ€.
@PreAuthorize("denyAll()")
hasPermission(target, permission)
νŠΉμ • λŒ€μƒμ— λŒ€ν•΄ νŠΉμ • κΆŒν•œ(permission)을 κ°€μ§€κ³  μžˆλŠ”μ§€ 확인.
@PreAuthorize("hasPermission(#post, 'EDIT')")
principal
ν˜„μž¬ 인증된 μ‚¬μš©μžμ˜ principal을 μ°Έμ‘°.
@PreAuthorize("principal.username == 'admin'")

κΆŒν•œ μ œμ–΄ μ˜ˆμ‹œ

β€’
μ†Œμœ μž 검증 둜직
β€’
κ²Œμ‹œκΈ€ 등둝
β€’
κ²Œμ‹œκΈ€ 쑰회
β€’
κ²Œμ‹œκΈ€ μˆ˜μ •
β€’
κ²Œμ‹œκΈ€ μ‚­μ œ

μ†Œμœ μž 검증 둜직

public interface BoardService { ... // μ†Œμœ μž 확인 public boolean isOwner(String id, Long userNo) throws Exception; ... }
Java
볡사
@Slf4j @Service("BoardService") // ⭐ 빈 이름을 BoardService 둜 μ§€μ • public class BoardServiceImpl implements BoardService { ... /** * @param id : κ²Œμ‹œκΈ€ id, userNo : νšŒμ› no (PK) * κ²Œμ‹œκΈ€ id둜 μž‘μ„±μž userNo λ₯Ό μ‘°νšŒν•˜μ—¬, * 인증된 μ‚¬μš©μž no 와 μΌμΉ˜ν•˜λŠ”μ§€ 확인 */ @Override public boolean isOwner(String id, Long userNo) throws Exception { log.info("isOwner - id : " + id); log.info("isOwner - userNo : " + userNo); Board board = select(id); Long boardUserNo = board.getUserNo(); if( userNo != null && userNo == boardUserNo ) { return true; } return false; } }
Java
볡사

κ²Œμ‹œκΈ€ 등둝

인증된 μ‚¬μš©μž, 즉 νšŒμ›λ§Œ κ²Œμ‹œκΈ€μ„ 등둝할 수 μžˆλ‹€.
@Controller @RequestMapping("/board") public class BoardController { // κ²Œμ‹œκΈ€ 등둝 ν™”λ©΄ @Secured("ROLE_USER") // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)인 경우 //@PreAuthorize("hasRole('USER')") // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)인 경우 //@PreAuthorize("isAuthenticated()") // 인증된 μ‚¬μš©μž 인 경우 //@PreAuthorize("isAuthenticated() and hasRole('ADMIN')") // 인증 + κ΄€λ¦¬μžμΈ 경우 @GetMapping("/insert") public String insert() { ... } // κ²Œμ‹œκΈ€ 등둝 처리 @Secured("ROLE_USER") // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)인 경우 //@PreAuthorize("hasRole('USER')") // μ‚¬μš©μž κΆŒν•œ(ROLE_USER)인 경우 //@PreAuthorize("isAuthenticated()") // 인증된 μ‚¬μš©μž 인 경우 //@PreAuthorize("isAuthenticated() and hasRole('ADMIN')") // 인증 + κ΄€λ¦¬μžμΈ 경우 @PostMapping("") public String insertPost(Board board) { ... } }
Java
볡사

κ²Œμ‹œκΈ€ 쑰회

λˆ„κ΅¬λ‚˜ κ²Œμ‹œκΈ€μ„ μ‘°νšŒν•  수 μžˆλ‹€.
@Controller @RequestMapping("/board") public class BoardController { // λ³„λ„λ‘œ κΆŒν•œμ œμ–΄ μ•ˆ 함 @GetMapping("/{id}") public String select(@PathVariable("id") String id) { ... return "/board/select"; } }
Java
볡사

κ²Œμ‹œκΈ€ μˆ˜μ •

μž‘μ„±μž 본인 λ˜λŠ” κ΄€λ¦¬μžλ§Œ κ²Œμ‹œκΈ€ μˆ˜μ •μ΄ κ°€λŠ₯ν•˜λ‹€.
@Controller @RequestMapping("/board") public class BoardController { @Autowried BoardService boardService; // μˆ˜μ • ν™”λ©΄ // πŸ‘©β€πŸ’Ό μž‘μ„±μž 본인 πŸ‘©β€πŸ”§ κ΄€λ¦¬μž @PreAuthorize(" hasRole('ADMIN') or (#p0 != null and @BoardService.isOwner(#p0, authentication.principal.user.no) )") @GetMapping("/update/{id}") public String update(@PathVariable("id") String id) { Board board = boardService.select(id); ... } // μˆ˜μ • 처리 // πŸ‘©β€πŸ’Ό μž‘μ„±μž 본인 πŸ‘©β€πŸ”§ κ΄€λ¦¬μž @PreAuthorize(" hasRole('ADMIN') or (#p0.id != null and @BoardService.isOwner(#p0.id, authentication.principal.user.no) )") @ResponseBody @PutMapping("") public String update(@RequestBody Board board) { Board board = boardService.update(board); ... return "SUCCESS"; } }
Java
볡사
@PreAuthorize μ–΄λ…Έν…Œμ΄μ…˜ μ•ˆμ—μ„œ, SpEL ν‘œν˜„μ‹μ„ μ‚¬μš©ν•˜λ©΄, #p0, #p1… 와 같은 ν˜•μ‹μœΌλ‘œ νŒŒλΌλ―Έν„°λ₯Ό 인덱슀둜 μ§€μ •ν•˜μ—¬, κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.
public String update(@PathVariable("id") String id) { ...
Java
볡사
μ—¬κΈ°μ„œ 첫번째 (인덱슀 0 ) νŒŒλΌλ―Έν„°λ₯Ό κ°€μ Έμ˜€λ €λ©΄, SpEL ν‘œν˜„μ‹μ—μ„œλŠ” #p0 으둜 μ§€μ •ν•΄μ„œ κ°€μ Έμ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€,
νŒŒλΌλ―Έν„° - @PathVariable("id") String id
SpEL - #p0
id #p0
@BoardService.isOwner(#p0.id, authentication.principal.user.no)
Java
볡사
SpEL ν‘œν˜„μ‹ μ•ˆμ—μ„œ, "@λΉˆμ΄λ¦„.λ©”μ†Œλ“œ" ν˜•νƒœλ‘œ νŠΉμ • 빈의 λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€.
μ—¬κΈ°μ—μ„œλŠ” λ©”μ†Œλ“œμ— νŒŒλΌλ―Έν„° id(κ²Œμ‹œκΈ€ id), 인증된 μ‚¬μš©μž no λ₯Ό λ©”μ†Œλ“œλ‘œ μ „λ‹¬ν•˜μ—¬ μ†Œμœ μžμΈμ§€ κ²€μ¦ν•˜κ³  μ—¬λΆ€λ₯Ό true, false 둜 λ°˜ν™˜λ°›μ•„ μ†Œμœ μžλ₯Ό λ©”μ†Œλ“œ 호좜 전에 κΆŒν•œ μ œμ–΄ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ²Œμ‹œκΈ€ μ‚­μ œ

μž‘μ„±μž 본인 λ˜λŠ” κ΄€λ¦¬μžλ§Œ κ²Œμ‹œκΈ€ μ‚­μ œκ°€ κ°€λŠ₯ν•˜λ‹€.
@Controller @RequestMapping("/board") public class BoardController { @Autowried BoardService boardService; // μ‚­μ œ 처리 // πŸ‘©β€πŸ’Ό μž‘μ„±μž 본인 πŸ‘©β€πŸ”§ κ΄€λ¦¬μž @PreAuthorize("( hasRole('ADMIN')) or (#p0 != null and @BoardService.isOwner(#p0, authentication.principal.user.no))") @ResponseBody @DeleteMapping("/{id}") public String update(@PathVariable("id") String id) { Board board = boardService.delete(id); ... return "SUCCESS"; } }
Java
볡사
인증된 μ‚¬μš©μž κΆŒν•œμ΄ 관리(ROLE_ADMIN) 인지 κ²€μ¦ν•©λ‹ˆλ‹€.
νŒŒλΌλ―Έν„°λ‘œ κ²Œμ‹œκΈ€ id 와 인증된 μ‚¬μš©μž no 받아와 κ²Œμ‹œκΈ€ μ†Œμœ μžλ₯Ό ν™•μΈν•©λ‹ˆλ‹€.

κ²Œμ‹œνŒ κΆŒν•œ μ œμ–΄

β€’
ν…Œμ΄λΈ” 생성
β—¦
board.sql

board.sql

-- board : κ²Œμ‹œκΈ€ DROP TABLE IF EXISTS `board`; CREATE Table `board` ( `no` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'PK', `id` VARCHAR(64) UNIQUE COMMENT 'UK', `title` VARCHAR(100) NOT NULL COMMENT '제λͺ©', -- `writer` VARCHAR(100) NOT NULL COMMENT 'μž‘μ„±μž', `user_no` BIGINT NOT NULL COMMENT 'νšŒμ›λ²ˆν˜Έ(PK)', `content` TEXT NULL COMMENT 'λ‚΄μš©', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'λ“±λ‘μΌμž', `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'μˆ˜μ •μΌμž', FOREIGN KEY (user_no) REFERENCES `user`(no) ON UPDATE CASCADE ON DELETE CASCADE ) COMMENT 'κ²Œμ‹œκΈ€';
SQL
볡사

View

β€’
board/list.html
β€’
board/create.html
β€’
board/detail.html
β€’
board/update.html
β€’
board/list.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>κ²Œμ‹œνŒ ν”„λ‘œμ νŠΈ</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <main> <div class="container py-5"> <div class="row justify-content-center"> <div class="col-12"> <h1 class="text-center">κ²Œμ‹œκΈ€ λͺ©λ‘</h1> <div class="d-flex justify-content-end"> <a href="/board/create" class="btn btn-primary">κΈ€μ“°κΈ°</a> </div> <table class="table table-bordered table-striped table-hover table-responsive mt-3 text-center"> <colgroup> <col class="col-1"> <!-- 번호 --> <col class="col-3"> <!-- 제λͺ© --> <col class="col-2"> <!-- μž‘μ„±μž --> <col class="col-2"> <!-- λ“±λ‘μΌμž --> <col class="col-2"> <!-- μˆ˜μ •μΌμž --> </colgroup> <tr class="table-dark"> <th width="150">번호</th> <th width="300">제λͺ©</th> <th>μž‘μ„±μž</th> <th>λ“±λ‘μΌμž</th> <th>μˆ˜μ •μΌμž</th> </tr> <th:block th:if="${ list == null || list.isEmpty() }"> <tr> <td colspan="5">쑰회된 데이터가 μ—†μŠ΅λ‹ˆλ‹€.</td> </tr> </th:block> <th:block th:each="board : ${list}"> <tr class="align-middle"> <td th:text="${board.no}">번호</td> <td class="text-start"> <a th:href="|/board/detail?no=${board.no}|" th:text="${board.title}">제λͺ©</a> </td> <td th:text="${board.writer}">μž‘μ„±μž</td> <td> <span th:text="${ #dates.format( board.createdAt, 'yyyy-MM-dd HH:mm:ss' ) }"> λ“±λ‘μΌμž </span> </td> <td> <span th:text="${ #dates.format( board.updatedAt, 'yyyy-MM-dd HH:mm:ss' ) }"> μˆ˜μ •μΌμž </span> </td> </tr> </th:block> </table> </div> </div> </div> </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
HTML
볡사
β€’
board/create.html
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>κ²Œμ‹œνŒ ν”„λ‘œμ νŠΈ</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <main> <div class="container py-5"> <div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-6"> <h1 class="text-center">κ²Œμ‹œκΈ€ 등둝</h1> <!-- action 제거 or 의미만 μœ μ§€ --> <form id="boardForm"> <div class="mb-3"> <label class="form-label">제λͺ©</label> <input type="text" class="form-control" name="title" id="title" required> </div> <div class="mb-3"> <label class="form-label">μž‘μ„±μž</label> <input type="text" class="form-control" name="writer" id="writer" required> </div> <div class="mb-3"> <label class="form-label">λ‚΄μš©</label> <textarea class="form-control" name="content" id="content" rows="5"></textarea> </div> <div class="d-grid"> <button type="submit" class="btn btn-primary">등둝</button> </div> </form> </div> </div> </div> </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> <script> document.getElementById("boardForm").addEventListener("submit", async function (e) { e.preventDefault(); // κΈ°λ³Έ submit 막기 const data = { title: document.getElementById("title").value, writer: document.getElementById("writer").value, content: document.getElementById("content").value }; try { const response = await fetch("/board/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error("등둝 μ‹€νŒ¨"); } const result = await response.json(); alert("κ²Œμ‹œκΈ€μ΄ λ“±λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); // λͺ©λ‘μœΌλ‘œ 이동 location.href = "/board/list"; } catch (error) { alert(error.message); } }); </script> </body> </html>
HTML
볡사
β€’
board/detail.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>κ²Œμ‹œνŒ ν”„λ‘œμ νŠΈ</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <main> <div class="container py-5"> <div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-6"> <h1 class="text-center">κ²Œμ‹œκΈ€ 쑰회</h1> <form action="/board/create" method="post" th:object="${board}"> <div class="mb-3"> <label for="" class="form-label">제λͺ©</label> <input type="text" class="form-control" th:field="*{title}" aria-describedby="helpTitle" placeholder="제λͺ©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”" readonly /> </div> <div class="mb-3"> <label for="" class="form-label">μž‘μ„±μž</label> <input type="text" class="form-control" th:field="*{writer}" aria-describedby="helpWriter" placeholder="μž‘μ„±μžλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”" readonly /> </div> <div class="mb-3"> <label for="" class="form-label">λ‚΄μš©</label> <textarea class="form-control" th:field="*{content}" rows="5" readonly></textarea> </div> <div class="d-grid gap-2 d-md-flex justify-content-center"> <button type="button" class="btn btn-outline-primary w-100" onclick="moveList()">λͺ©λ‘</button> <button type="button" class="btn btn-primary w-100" onclick="moveUpdate()">μˆ˜μ •</button> </div> </form> </div> </div> </div> </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> <script> // πŸ‘©β€πŸ’» λͺ¨λΈ 객체λ₯Ό μžλ°”μŠ€ν¬λ¦½νŠΈλ‘œ κ°€μ Έμ˜€λŠ” 방법 let no = '[[${board.no}]]' // λͺ©λ‘ ν™”λ©΄ 이동 function moveList() { location.href = '/board/list' } // μˆ˜μ • ν™”λ©΄ 이동 function moveUpdate() { location.href = '/board/update?no=' + no } </script> </body> </html>
HTML
볡사
β€’
board/update.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>κ²Œμ‹œνŒ ν”„λ‘œμ νŠΈ</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <main> <div class="container py-5"> <div class="row justify-content-center"> <div class="col-12 col-md-10 col-lg-6"> <h1 class="text-center">κ²Œμ‹œκΈ€ μˆ˜μ •</h1> <form id="form" action="/board/update" method="post" th:object="${board}"> <input type="hidden" th:field="*{no}"> <div class="mb-3"> <label for="" class="form-label">제λͺ©</label> <input type="text" class="form-control" th:field="*{title}" aria-describedby="helpTitle" placeholder="제λͺ©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”" required /> </div> <div class="mb-3"> <label for="" class="form-label">μž‘μ„±μž</label> <input type="text" class="form-control" th:field="*{writer}" aria-describedby="helpWriter" placeholder="μž‘μ„±μžλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”" required /> </div> <div class="mb-3"> <label for="" class="form-label">λ‚΄μš©</label> <textarea class="form-control" th:field="*{content}" rows="5"></textarea> </div> <div class="d-grid gap-2 d-md-flex justify-content-center mt-2"> <button type="submit" class="btn btn-primary w-100">μˆ˜μ •</button> </div> <div class="d-grid gap-2 d-md-flex justify-content-center mt-2"> <button type="button" class="btn btn-outline-primary w-100" onclick="moveList()">λͺ©λ‘</button> <button type="button" class="btn btn-outline-danger w-100" onclick="actionDelete()">μ‚­μ œ</button> </div> </form> </div> </div> </div> </main> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script> <script> const form = document.getElementById("form"); form.addEventListener("submit", async (e) => { e.preventDefault(); // κΈ°λ³Έ submit 막기 const data = { no: document.getElementById("no").value, title: document.getElementById("title").value, writer: document.getElementById("writer").value, content: document.getElementById("content").value }; try { const response = await fetch("/board/update", { method: "PUT", // λ˜λŠ” POST headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); if (!response.ok) throw new Error("μˆ˜μ • μ‹€νŒ¨"); alert("κ²Œμ‹œκΈ€μ΄ μˆ˜μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); location.href = "/board/list"; } catch (err) { alert(err.message); } }); async function actionDelete() { if (!confirm("μ •λ§λ‘œ μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?")) return; const no = document.getElementById("no").value; try { const response = await fetch(`/board/delete/${no}`, { method: "DELETE" }); if (!response.ok) throw new Error("μ‚­μ œ μ‹€νŒ¨"); alert("μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."); location.href = "/board/list"; } catch (err) { alert(err.message); } } function moveList() { location.href = "/board/list"; } </script> </body> </html>
HTML
볡사