A. ๋ถ๋ก
A. ๋ถ๋ก ๊ด๋ จ
01. ์ธํ ๋ฆฌ์ ์ด ์ฌ์ฉํ๊ธฐ
STS ๋์ ์ธํ ๋ฆฌ์ ์ด ์ปค๋ฎค๋ํฐ ์๋์ ์ ์ฌ์ฉํ๋ ค๋ฉด ๋ค์์ ์๋ด์ ๋ฐ๋ผ ์ธํ ๋ฆฌ์ ์ด๋ฅผ ์ค์นํ๊ณ ์ฌ์ฉํ์.
Spring Initializr
์ธํ ๋ฆฌ์ ์ด๋ฅผ ์ค์นํ๊ธฐ ์ ์ ์คํ๋ง๋ถํธ ๊ฐ๋ฐ์ ๋์์ฃผ๋ Spring Initializr๋ฅผ ์ฌ์ฉํด ๋ณด์. ๊ณง ์ฐ๋ฆฌ๊ฐ ์ค์นํ ์ธํ ๋ฆฌ์ ์ด ๋ฌด๋ฃ๋ฒ์ ์ธ CE(Comunity Edition)๋ ์คํ๋ง ๋๊ตฌ ์ง์์ด ์๋์ง๋ง Spring Initializr๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ง๋ถํธ ๊ฐ๋ฐ์ ์ฝ๊ฒ ์์ํ ์ ์๋ค. Spring Initializr๋ฅผ ํตํด ์คํ๋ง๋ถํธ ํ๋ก์ ํธ๋ฅผ ์ค์ ํ์ฌ ๋ค์ด๋ก๋ํ ์ ์๋ค.
๋ค์ URL์ ์ ์ํ์.
์ ํ๋ฉด์์ ๋นจ๊ฐ ์ ๋ฐ์ค์ ๋์ผํ๊ฒ ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํ๋ค.
- Project: Gradle Project
- Language: Java
- Sprint Boot: 2.6.6 (SNAPSHOT ๋๋ M2, M3 ๊ฐ ๋ถ์ง ์์ ์ต์ ๋ฒ์ ์ ์ ํํ๋ค.)
- Project Meta Data
- Group:
com.mysite
- Artifact:
sbb
- Name:
sbb
- Description: My project for Sprint Boot
- Package name :
com.mysite.sbb
- Packaging: Jar
- Java: 11
- Group:
์์ ๊ฐ์ด ์ค์ ํ๊ณ ["ADD DEPENDENCIES"]
๋ฒํผ์ ๋๋ฅด์. ๊ทธ๋ฌ๋ฉด ๋ค์๊ณผ ๊ฐ์ ํ์
์ฐฝ์ด ๋ํ๋๋ค.
๊ทธ๋ฌ๋ฉด sbb.zip
ํ์ผ์ด ๋ค์ด๋ก๋ ๋๋ค. sbb.zip
ํ์ผ์ "ํ๋ก์ ํธ ํ ๋๋ ํฐ๋ฆฌ"์ ์์ถํด์ ํ์.
ํ๋ก์ ํธ ํ ๋๋ ํฐ๋ฆฌ: ์๋์ฐ๋
C:/Users/<์ฌ์ฉ์๋ช >/projects
๋๋ ํฐ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ ๋งฅ OS๋ผ๋ฉด/Users/<์ฌ์ฉ์๋ช >/projects
๋ฅผ ์ฌ์ฉํ์.
๊ทธ๋ฌ๋ฉด ํ๋ก์ ํธ ํ ๋๋ ํฐ๋ฆฌ ๋ฐ์ sbb
๋๋ ํฐ๋ฆฌ๊ฐ ์์ฑ๋ ๊ฒ์ด๋ค. ์ด์ ์ธํ
๋ฆฌ์ ์ด๋ฅผ ์ค์นํ๊ณ sbb
๋๋ ํฐ๋ฆฌ๋ฅผ ์ธํ
๋ฆฌ์ ์ด์์ ["Open"]
ํ์ฌ ์คํ๋ง๋ถํธ ํ๋ก์ ํธ๋ฅผ ์์ํ ์ ์๋ค.
์ธํ ๋ฆฌ์ ์ด ์ค์น
๋ค์์ URL์์ ์ธํ ๋ฆฌ์ ์ด๋ฅผ ๋ค์ด๋ก๋ ํ์.
์ URL์ ์ ์ํ๋ฉด Ultimate์ Community ๋ ๊ฐ์ง ๋ฒ์ ์ด ์๋๋ฐ ๋ฌด๋ฃ์ธ Community ๋ฒ์ ์ ๋ค์ด๋ก๋ํ์ฌ ์ค์นํ์.
์ค์น ํ ์ธํ ๋ฆฌ์ ์ด๋ฅผ ์คํํ์.
.["Open"]
๋ฒํผ์ ๋๋ฅด๊ณ ์์์ ์์ถํด์ ํ sbb
๋๋ ํฐ๋ฆฌ๋ฅผ ์ ํํ๋ค.
ํ๋ก์ ํธ ์์ํ์๋ Gradle ์์ (๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ค์ด๋ก๋ ๋ฑ)์ผ๋ก ์ธํ ์๊ฐ์ด 1~2๋ถ ์ ๋ ์์๋๋ค.
SDK ์ค๋ฅ
.com/mysite/sbb/
SbbApplication.java
ํ์ผ์ ์ด์์ ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค๋ฉด SDK๊ฐ ์ง์ ๋์ง ์์ ๊ฒฝ์ฐ์ด๋ฏ๋ก ์๋ํฐ ์ฐฝ ์๋จ์ ํ์๋๋ "SDK" ์ค์ ์ ํตํด ์ค์น๋ ์๋ฐ SDK๋ฅผ ์ง์ ํ์.
๋กฌ๋ณต ํ๋ฌ๊ทธ์ธ ์ค์น
๋ค์์ฒ๋ผ [Preferences -> Plugins]
์์ ๋กฌ๋ณต(Lombok)์ ๊ฒ์ํ์ฌ ์ค์นํ์.
Auto Reload ์ค์
์ธํ ๋ฆฌ์ ์ด์์ ์๋ฐ ํ์ผ์ ์์ ํ๊ฑฐ๋ ํ ํ๋ฆฟ์ ์์ ํ ๊ฒฝ์ฐ ์์์ ์์ด ์๋์ผ๋ก ๋ณ๊ฒฝ ์ฌํญ์ ์ ์ฉํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ค์ ํด์ผ ํ๋ค.
Java
1-05์ฅ์ Spring Boot Devtools๋ฅผ ์ค์นํ ํ์ ์ ์ฉํ์.
ํ์๋ฆฌํ(thymeleaf)
ํ
ํ๋ฆฟ ํ์ผ์ ๋ณ๊ฒฝํ ํ ์๋ ์ ์ฉ๋๊ฒ ํ๋ ค๋ฉด application.properties
ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ ๋ด์ฉ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
sbb/src/main/resources/
application.properties
// (... ์๋ต ...)
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=file:src/main/resources/templates/
ํ
ํ๋ฆฟ์ ์ฌ์ฉํ๋ 2-07์ฅ ๋ถํฐ ํ์. jar
๋ฐฐํฌ์์๋ spring.thymeleaf.prefix
ํญ๋ชฉ์ ์ฃผ์์ฒ๋ฆฌํด์ผ ํ๋ค. (์ค๋ฅ ๋ณด๊ณ ๊ฐ ์์)
Unused ๊ฒฝ๊ณ ๋ฉ์์ง ๋๊ธฐ
์ธํ ๋ฆฌ์ ์ด ์ปค๋ฎค๋ํฐ ๋ฒ์ ์ ์คํ๋ง์ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์ ์คํ๋ง์ ์ปจํธ๋กค๋ฌ์ URL ๋งคํ ๋ฉ์๋๋ค์์ Unused ๊ฒฝ๊ณ ๋ฉ์์ง๊ฐ ๋ํ๋๋ค. ํ์ง๋ง ๋ฌด์ํ ์ ์์ ๋งํผ ๋ง์ ๊ฒฝ๊ณ ๋ฉ์์ง๊ฐ ๋์ค๊ธฐ ๋๋ฌธ์ ์ด ํญ๋ชฉ์ ์ค์ ์์ ๋๋๊ฒ ์ข๋ค.
Gradle
๊ทธ๋ ์ด๋ค๋ก ๋ก์ปฌ ์๋ฒ๋ฅผ ์คํํ๋ ๋ฐฉ๋ฒ๊ณผ ๋ฐฐํฌ ํ์ผ(jar
)์ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด์.
๋ก์ปฌ ์๋ฒ ์คํํ๊ธฐ
๋ฐฐํฌํ์ผ ์์ฑํ๊ธฐ
๊ทธ๋ฆฌ๊ณ ์ฐ์ธก ๋ง์ฐ์ค ๋ฒํผ์ ๋๋ฌ Run sbb [bootJar]
๋ฅผ ์ ํํ๋ค. ๊ทธ๋ฌ๋ฉด build/libs/
๋๋ ํฐ๋ฆฌ์ sbb-0.0.1-SNAPSHOT.jar
์ ๊ฐ์ ๋ฐฐํฌ ํ์ผ์ด ์์ฑ๋๋ค.
02. AWS ๋ผ์ดํธ์ธ์ผ ์ฌ์ฉ ์ทจ์
AWS ์๋น์ค๋ฅผ ๋ ์ด์ ์ด์ํ์ง ์๋๋ค๋ฉด ์ธ์คํด์ค์ ๊ณ ์ IP ๊ทธ๋ฆฌ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ญ์ ํ์ฌ ์๋ํ์ง ์์ ์๊ธ ๋ฐ์์ ๋ง์.
์ธ์คํด์ค์ ๊ณ ์ IP ์ญ์
AWS ๋ผ์ดํธ์ธ์ผ ์ธ์คํด์ค๋ 3๋ฌ๊ฐ ๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์๊ณ ์ดํ์ ๋น์ฉ์ด ๋ฐ์ํ๋ค. ์ด๋ฅผ ์์น ์๋๋ค๋ฉด ์ธ์คํด์ค์ ๊ณ ์ IP๋ฅผ ์ญ์ ํด์ผ ํ๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ญ์
03. ์คํ๋ง๋ถํธ 2.x ๋ฒ์ ์๋ด
์ด ์ฑ ์ ์คํ๋ง๋ถํธ 3.x ๋ฒ์ ์ ๊ธฐ์ค์ผ๋ก ํ๋ค. ๋ง์ฝ ์คํ๋ง๋ถํธ 2.x ๋ฒ์ ์ผ๋ก ์ด ์ฑ ์ ๋ด์ฉ์ ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ๋ค์์ ์๋ด์ ๋ฐ๋ผ ์คํ๋ง๋ถํธ 2.x ๋ฒ์ ์ ์ฌ์ฉํ ์ ์๋ค.
jakarta
ํจํค์ง ๋ณ๊ฒฝ
์ด ์ฑ
์ ์ฌ์ฉํ import jakarta.*
ํจํค์ง๋ฅผ ๋ชจ๋ import javax.*
ํจํค์ง๋ฅผ ์ฌ์ฉํ๋๋ก ๋ณ๊ฒฝํด์ผ ํ๋ค. ์ฆ, jakarta
๋ก ๋์ด ์๋ import
๋ฌธ์ ์ ๋ถ javax
๋ก ๊ต์ฒดํ์ฌ ์ฌ์ฉํด์ผ ํ๋ค.
SecurityConfig.java
์คํ๋ง๋ถํธ 2.x ๋ฒ์ ์ ์คํ๋ง ์ํ๋ฆฌํฐ ์ค์ ์ ๋ค์์ SecurityCofnig.java
ํ์ผ๋ก ๋์ฒดํด์ผ ํ๋ค. antMatchers
, ignoringAntMatchers
๋ฑ์ URL ํจํด ๋งค์นํ๋ ๋ถ๋ถ๋ค์ด ๋ณ๊ฒฝ๋์๋ค.
package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.mysite.sbb.user.UserSecurityService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserSecurityService userSecurityService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll()
.and()
.csrf().ignoringAntMatchers("/h2-console/**")
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
}
}
์คํ๋ง๋ถํธ 2.x ๊นํ๋ธ ์ฃผ์
์ด ์ฑ ์ ์คํ๋ง๋ถํธ 2.x ๋ฒ์ ๋ถํฐ ์์ฑ๋ ์ฑ ์ด๋ค. ์คํ๋ง๋ถํธ 2.x ๋ฒ์ ์ผ๋ก ์์ฑ๋ ์์ค์ฝ๋๋ ๋ค์์ ๊นํ๋ธ ์ฃผ์์์ ํ์ธํ ์ ์๋ค.
04. ๋๊ธ (์ญ์ ์์ )
::: warn ์ฃผ์
์๋ ๋ด์ฉ์ "์ ํ ํฌ ์คํ๋ง๋ถํธ" ์์ ๋ฒ์ ์ ๋ด์ฉ์ด๋ฏ๋ก ํ์ฌ๊น์ง ์งํํ ์์ค์ฝ๋์ ์ ์ฉํ ๋๋ ์ฃผ์ํด์ผ ํจ ์ง๋ฌธ ๋๋ ๋ต๋ณ์ ๋ํ์ฌ ์งค๋งํ๊ฒ ๋ตํด์ ์ฌ๋ฆฌ๋ ๊ธ์ ๋๊ธ์ด๋ผ๊ณ ํ๋ค. ์ด๋ฒ์๋ ์ง๋ฌธ๊ณผ ๋ต๋ณ์ ๋๊ธ(Comment
) ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋ณด์.
:::
๋๊ธ ๋๋ฉ์ธ
๋๊ธ ์ญ์ ์ง๋ฌธ๊ณผ ๋ต๋ณ์ฒ๋ผ ํ๋์ ๋๋ฉ์ธ์ผ๋ก ์ทจ๊ธํ์.
๋๊ธ ๋ชจ๋ธ
Comment
๋ชจ๋ธ
๋๊ธ ์์ฑ์ ์ํด์ ๊ฐ์ฅ ๋จผ์ ์ค๋นํด์ผ ํ ๊ฒ์ ๋๊ธ ๋ชจ๋ธ์ด๋ค.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
Comment.java
package com.mysite.sbb.comment;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import com.mysite.sbb.answer.Answer;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne
private SiteUser author;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
private LocalDateTime modifyDate;
@ManyToOne
private Question question;
@ManyToOne
private Answer answer;
}
Comment
๋ชจ๋ธ์ ์์ฑ์ ๋ค์๊ณผ ๊ฐ๋ค.
ํ๋ | ์ค๋ช |
---|---|
id | ๋๊ธ์ ๊ณ ์ ๋ฒํธ |
author | ๋๊ธ ์์ฑ์ |
content | ๋๊ธ ๋ด์ฉ |
createDate | ๋๊ธ ์์ฑ์ผ์ |
modifyDate | ๋๊ธ ์์ ์ผ์ |
question | ์ด ๋๊ธ์ด ๋ฌ๋ฆฐ ์ง๋ฌธ |
answer | ์ด ๋๊ธ์ด ๋ฌ๋ฆฐ ๋ต๋ณ |
๋ง์ฝ ์ง๋ฌธ์ ๋๊ธ์ด ์์ฑ๋ ๊ฒฝ์ฐ์๋ question
์ ๊ฐ์ด ์ ์ฅ๋๊ณ ๋ต๋ณ์ ๋๊ธ์ด ์์ฑ๋ ๊ฒฝ์ฐ์๋ answer
์ ๊ฐ์ด ์ ์ฅ๋ ๊ฒ์ด๋ค. ํ ์ฌ๋์ด ์ฌ๋ฌ๊ฐ์ ๋๊ธ์ ๋ฌ์ ์๊ณ ์ง๋ฌธ ๋๋ ๋ต๋ณ ํ๊ฐ์ ์ฌ๋ฌ๊ฐ์ ๋๊ธ์ ๋ฌ์ ์๊ธฐ ๋๋ฌธ์ author
, question
, answer
์์ฑ์๋ @ManyToOne
์ ๋ํ
์ด์
์ ์ค์ ํด์ผ ํ๋ค.
๊ทธ๋ฆฌ๊ณ ๋๊ธ์ ์์ ํ๊ฑฐ๋ ์ญ์ ํ ํ์ ์ง๋ฌธ ์์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๊ธฐ ์ํด์๋ ๋๊ธ์ ํตํด ์ง๋ฌธ์ id
๋ฅผ ์์๋ด๋ getQuestionId
๋ฉ์๋๊ฐ ํ์ํ๋ค. ์ดํ ์งํํ ๋๊ธ ์์ , ์ญ์ ์์ ํ์ํ ๊ธฐ๋ฅ์ด์ง๋ง ํธ์๋ฅผ ์ํด ์ฌ๊ธฐ์ ๋จผ์ ๋ง๋ค๊ณ ๊ฐ๋๋ก ํ์.
๋ค์๊ณผ ๊ฐ์ด Comment
๋ชจ๋ธ์ getQuestionId
๋ฉ์๋๋ฅผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
Comment.java
package com.mysite.sbb.comment;
// (... ์๋ต ...)
@Entity
@Getter
@Setter
public class Comment {
// (... ์๋ต ...)
public Integer getQuestionId() {
Integer result = null;
if (this.question != null) {
result = this.question.getId();
} else if (this.answer != null) {
result = this.answer.getQuestion().getId();
}
return result;
}
}
getQuestionId
๋ฉ์๋๋ ๋๊ธ์ ํตํด ์ง๋ฌธ์ id ๊ฐ์ ๋ฆฌํดํ๋ ๋ฉ์๋๋ก question
์์ฑ์ด null
์ด ์๋ ๊ฒฝ์ฐ๋ ์ง๋ฌธ์ ๋ฌ๋ฆฐ ๋๊ธ์ด๋ฏ๋ก this.question.getId()
๊ฐ์ ๋ฆฌํดํ๊ณ ๋ต๋ณ์ ๋ฌ๋ฆฐ ๋๊ธ์ธ ๊ฒฝ์ฐ this.answer.getQuestion().getId()
๊ฐ์ ๋ฆฌํดํ๋ค.
Question
๋ชจ๋ธ
๊ทธ๋ฆฌ๊ณ ์ง๋ฌธ์์ ๋๊ธ์ ์ฐธ์กฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ์ง๋ฌธ ๋ชจ๋ธ์ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question
Question.java
package com.mysite.sbb.question;
// (... ์๋ต ...)
import com.mysite.sbb.comment.Comment;
// (... ์๋ต ...)
@Getter
@Setter
@Entity
public class Question {
// (... ์๋ต ...)
@OneToMany(mappedBy = "question")
private List<Comment> commentList;
}
์ง๋ฌธ์ ์์ฑ๋ ๋๊ธ ๋ฆฌ์คํธ๋ฅผ ์ฐธ์กฐํ๊ธฐ ์ํด commentList
์์ฑ์ @OneToMany
์ ๋ํ
์ด์
์ผ๋ก ์์ฑํ๋ค. Comment
๋ชจ๋ธ์์ Question
์ ์ฐ๊ฒฐํ๊ธฐ ์ํ ์์ฑ๋ช
์ด question
์ด๋ฏ๋ก mappedBy
์ ๊ฐ์ผ๋ก "question"์ด ์ ๋ฌ๋์ด์ผ ํ๋ค.
Answer
๋ชจ๋ธ
๋ง์ฐฌ๊ฐ์ง๋ก ๋ต๋ณ์์ ๋๊ธ์ ์ฐธ์กฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ๋ต๋ณ ๋ชจ๋ธ์ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
Answer.java
package com.mysite.sbb.answer;
// (... ์๋ต ...)
import com.mysite.sbb.comment.Comment;
// (... ์๋ต ...)
@Entity
@Getter
@Setter
public class Answer {
// (... ์๋ต ...)
@OneToMany(mappedBy = "answer")
private List<Comment> commentList;
}
๋ต๋ณ์ ์์ฑ๋ ๋๊ธ ๋ฆฌ์คํธ๋ฅผ ์ฐธ์กฐํ๊ธฐ ์ํด commentList
์์ฑ์ @OneToMany
์ ๋ํ
์ด์
์ผ๋ก ์์ฑํ๋ค.
์ง๋ฌธ ๋๊ธ
์ง๋ฌธ์ ๋๊ธ์ ๋ฑ๋กํ ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋ณด์. ์ด์ ์คํ๋ง๋ถํธ์ ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ํจํด์ ์ต์ํด์ก์ ๊ฒ์ด๋ค. ์ง๋ฌธ ๋๊ธ์ ์ง๋ฌธ ์์ฑ๊ณผ ๊ฑฐ์ ์ฐจ์ด๊ฐ ์์ผ๋ฏ๋ก ์ฝ๋์์ฑ์ ๋น ๋ฅด๊ฒ(ํ๋ฒ์) ์งํํด ๋ณด์.
์ง๋ฌธ ๋๊ธ ๋งํฌ
์ง๋ฌธ ์์ธ ํ ํ๋ฆฟ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์.
ํ์ผ๋ช :
C:\projects\mysite\templates\pybo
question_detail.html
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
<div class="card-body">
<!-- (... ์๋ต ...) -->
<div class="my-3" sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}">
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary">์์ </a>
<a href="javascript:void(0);" class="delete btn btn-sm btn-outline-secondary"
th:data-uri="@{|/question/delete/${question.id}|}">์ญ์ </a>
</div>
<!-- ์ง๋ฌธ ๋๊ธ Start -->
<div class="mt-3" th:if="${not #lists.isEmpty(question.commentList)}">
<div th:each="comment,index : ${question.commentList}" class="comment py-2 text-muted">
<span style="white-space: pre-line;" th:text="${comment.content}"></span>
<span th:if="${comment.modifyDate != null}"
th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')} (์์ : ${#temporals.format(comment.modifyDate, 'yyyy-MM-dd HH:mm')})|"></span>
<span th:if="${comment.modifyDate == null}"
th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')}|"></span>
<a sec:authorize="isAuthenticated()"
th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
th:href="@{|/comment/modify/${comment.id}|}" class="small">์์ </a>,
<a sec:authorize="isAuthenticated()"
th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
href="javascript:void(0);" class="small delete" th:data-uri="@{|/comment/delete/${comment.id}|}">์ญ์ </a>
</div>
</div>
<div>
<a th:href="@{|/comment/create/question/${question.id}|}" class="small"><small>๋๊ธ ์ถ๊ฐ ..</small></a>
</div>
<!-- ์ง๋ฌธ ๋๊ธ End -->
</div>
</div>
<h5 class="border-bottom my-3 py-2" th:text="|${#lists.size(question.answerList)}๊ฐ์ ๋ต๋ณ์ด ์์ต๋๋ค.|"></h5>
<!-- (... ์๋ต ...) -->
๋ด์ฉ์ด ๋ง์ง๋ง ์ด๋ ต์ง ์๋ค. ์ฐฌ์ฐฌํ ์ดํด๋ณด์.
์ง๋ฌธ์ ๋ฑ๋ก๋ ๋๊ธ ์ ๋ถ๋ฅผ ๋ณด์ฌ ์ฃผ๊ธฐ์ํด question.commentList
๋ฃจํ๋ฅผ ๋๋ฉฐ ๋๊ธ ๋ด์ฉ๊ณผ ๊ธ์ด์ด, ์์ฑ์ผ์, ์์ ์ผ์๋ฅผ ์ถ๋ ฅํ๋ค. ๋ ๋๊ธ ๊ธ์ด์ด์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ๊ฐ์ผ๋ฉด '์์ ', '์ญ์ ' ๋งํฌ๊ฐ ๋ณด์ด๋๋ก ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฃจํ ๋ฐ๊นฅ์ชฝ์๋ ๋๊ธ์ ์์ฑํ ์ ์๋ '๋๊ธ ์ถ๊ฐ ..' ๋งํฌ๋ ์ถ๊ฐํ๋ค.
๋ฃจํ์ ์ํด ๋ฐ๋ณต๋๋ ๋๊ธ ๊ฐ๊ฐ์ comment
๋ผ๋ ์คํ์ผ ํด๋์ค๋ฅผ ๋ฐ๋ก ์ง์ ํ๋ค. ๋๊ธ ์์ญ์ ์ข ์์ ๊ธ์จ๋ก ๋ณด์ฌ์ง ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์ด๋ค. ์ง๊ธ๊น์ง ๋น ํ์ผ๋ก ๋์ด์๋ style.css
์ comment
ํด๋์ค์ ๋ํ ๋ด์ฉ์ ๋ค์์ฒ๋ผ ์ถ๊ฐํ๋๋ก ํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/static
style.css
.comment {
border-top:dotted 1px #ddd;
font-size:0.7em;
}
comment
ํด๋์ค๋ ๋๊ธ ๊ฐ๊ฐ์ ์๋จ์ ์ ์ ์ ์ถ๊ฐํ๊ณ ๊ธ๊ผด ํฌ๊ธฐ๋ฅผ 0.7em
์ผ๋ก ์ค์ ํ๋ ์คํ์ผ์ด๋ค.
CommentRepository
๋๊ธ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด Comment
๋ฆฌํฌ์งํฐ๋ฆฌ๋ฅผ ์์ฑํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
CommentRepository.java
package com.mysite.sbb.comment;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CommentRepository extends JpaRepository<Comment, Integer> {
}
CommentService
๊ทธ๋ฆฌ๊ณ ๋ฆฌํฌ์งํฐ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋๊ธ์ ์กฐํํ๊ณ ์์ฑ, ์์ , ์ญ์ ํ๋ ์๋น์ค๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ง๋ค์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
CommentService.java
package com.mysite.sbb.comment;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
public Comment create(Question question, SiteUser author, String content) {
Comment c = new Comment();
c.setContent(content);
c.setCreateDate(LocalDateTime.now());
c.setQuestion(question);
c.setAuthor(author);
c = this.commentRepository.save(c);
return c;
}
public Optional<Comment> getComment(Integer id) {
return this.commentRepository.findById(id);
}
public Comment modify(Comment c, String content) {
c.setContent(content);
c.setModifyDate(LocalDateTime.now());
c = this.commentRepository.save(c);
return c;
}
public void delete(Comment c) {
this.commentRepository.delete(c);
}
}
CommentService
ํด๋์ค์ ์์ฑ(create
), ์กฐํ(getComment
), ์์ (modify
), ์ญ์ (delete
) ๋ฉ์๋๋ฅผ ์์ฑํ๋ค.
CommentForm
๊ทธ๋ฆฌ๊ณ ๋๊ธ ์์ฑ์ ํ์ํ CommentForm
์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
CommentForm.java
package com.mysite.sbb.comment;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CommentForm {
@NotEmpty(message = "๋ด์ฉ์ ํ์ํญ๋ชฉ์
๋๋ค.")
private String content;
}
CommentForm
์ ํ์ํ ์์ฑ์ "๋ด์ฉ(content
)" ํ๋ ๋ฟ์ด๋ค.
CommentController
๊ทธ๋ฆฌ๊ณ ์ง๋ฌธ ๋๊ธ์ ์์ฑ, ์์ , ์ญ์ ํ๊ธฐ ์ํ ๋๊ธ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/comment/
CommentController.java
package com.mysite.sbb.comment;
import java.security.Principal;
import java.util.Optional;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private QuestionService questionService;
@Autowired
private UserService userService;
@PreAuthorize("isAuthenticated()")
@GetMapping(value = "/create/question/{id}")
public String createQuestionComment(CommentForm commentForm) {
return "comment_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping(value = "/create/question/{id}")
public String createQuestionComment(@PathVariable("id") Integer id, @Valid CommentForm commentForm,
BindingResult bindingResult, Principal principal) {
Optional<Question> question = this.questionService.getQuestion(id);
Optional<SiteUser> user = this.userService.getUser(principal.getName());
if (question.isPresent() && user.isPresent()) {
if (bindingResult.hasErrors()) {
return "comment_form";
}
Comment c = this.commentService.create(question.get(), user.get(), commentForm.getContent());
return String.format("redirect:/question/detail/%s", c.getQuestionId());
} else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
}
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/modify/{id}")
public String modifyComment(CommentForm commentForm, @PathVariable("id") Integer id, Principal principal) {
Optional<Comment> comment = this.commentService.getComment(id);
if (comment.isPresent()) {
Comment c = comment.get();
if (!c.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์์ ๊ถํ์ด ์์ต๋๋ค.");
}
commentForm.setContent(c.getContent());
}
return "comment_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/modify/{id}")
public String modifyComment(@Valid CommentForm commentForm, BindingResult bindingResult, Principal principal,
@PathVariable("id") Integer id) {
if (bindingResult.hasErrors()) {
return "comment_form";
}
Optional<Comment> comment = this.commentService.getComment(id);
if (comment.isPresent()) {
Comment c = comment.get();
if (!c.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์์ ๊ถํ์ด ์์ต๋๋ค.");
}
c = this.commentService.modify(c, commentForm.getContent());
return String.format("redirect:/question/detail/%s", c.getQuestionId());
} else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
}
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/delete/{id}")
public String deleteComment(Principal principal, @PathVariable("id") Integer id) {
Optional<Comment> comment = this.commentService.getComment(id);
if (comment.isPresent()) {
Comment c = comment.get();
if (!c.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์ญ์ ๊ถํ์ด ์์ต๋๋ค.");
}
this.commentService.delete(c);
return String.format("redirect:/question/detail/%s", c.getQuestionId());
} else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found");
}
}
}
์ง๋ฌธ์ ์์ฑ, ์์ , ์ญ์ ํ๋ ๊ฒ๊ณผ ๋์ผํ ๋ฐฉ๋ฒ์ด๋ผ์ ๊ฐ๊ฐ์ ๋ฉ์๋๋ฅผ ๋ฐ๋ก ์ค๋ช
ํ์ง๋ ์๊ฒ ๋ค. ๋ค๋ง ๋๊ธ์ ์์ฑํ๊ธฐ ์ํด comment_form.html
ํ
ํ๋ฆฟ์ด ํ์ํ๊ณ ๋๊ธ์ ์์ฑ, ์์ , ์ญ์ ํ ํ์๋ ํด๋น ์ง๋ฌธ์ ์์ธ ํ์ด์ง๋ก ์ด๋ํ๊ธฐ ์ํด ์ง๋ฌธ์ id ๊ฐ์ด ํ์ํ์ฌ c.getQuestionId()
๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ ์ ์ํ์.
comment_form.html
๊ทธ๋ฆฌ๊ณ ๋๊ธ ์์ฑ๊ณผ ์์ ์ ํ์ํ comment_form
ํ
ํ๋ฆฟ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
comment_form.html
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<h5 class="my-3 border-bottom pb-2">๋๊ธ ๋ฑ๋ก</h5>
<form th:object="${commentForm}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<nav th:replace="form_errors :: formErrorsFragment"></nav>
<div class="mb-3">
<label for="content" class="form-label">๋ด์ฉ</label>
<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="์ ์ฅํ๊ธฐ" class="btn btn-primary my-2">
</form>
</div>
์ง๋ฌธ, ๋ต๋ณ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ๋๊ธ ๋ฑ๋ก๊ณผ ์์ ์ ํจ๊ป ์ฌ์ฉํ๊ธฐ ์ํด action
์์ฑ์ ์ฌ์ฉํ์ง ์๊ณ CSRF ํญ๋ชฉ๋ ์๋์ผ๋ก ์ถ๊ฐํ๋ค.
์ง๋ฌธ ๋๊ธ ๊ธฐ๋ฅ ํ์ธ
๋ต๋ณ ๋๊ธ
์ง๋ฌธ ๋๊ธ๊ณผ ๋์ผํ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํ ๊ฐ๋ฅํ๋ฏ๋ก ์๋ตํ๋ค.