[TOC]
[ ←전편보기 / 1편, 2편, 4편, 5편, 6편 / 다음편보기→ ]
결과물 git주소 : https://gitlab.ntiple.com/developers/study-202403
1. 개요
지난편에서는 맨땅에서 SpringBoot
개발환경 및 공통 유틸리티 등을 작성해 보았다.
이번 편에서는 아래와 같이 본격 업무 로직을 작성해 볼 거다.
src
├── java/my/was/mywas/works
│ ├── atc [게시물]
│ │ ├── ArticleController.java [컨트롤러]
│ │ ├── Article.java [DTO]
│ │ ├── ArticleRepository.java [리포지터리]
│ │ └── ArticleService.java [서비스]
│ ├── cmn [공통]
│ │ ├── CommonController.java [컨트롤러]
│ │ ├── CommonRepository.java [리포지터리]
│ │ └── CommonService.java [서비스]
│ ├── lgn [로그인]
│ │ ├── LoginController.java [컨트롤러]
│ │ └── LoginService.java [서비스]
│ └── usr [회원관리]
│ ├── UserController.java [컨트롤러]
│ ├── User.java [DTO]
│ ├── UserRepository.java [리포지터리]
│ └── UserService.java [서비스]
└── resources
└── mappings [JPA ORM 맵핑 파일들]
├── orm-article.xml [게시물]
└── orm-common.xml [공통]
2. 기존 설정 수정
먼저 기존 파일들을 조금 수정해야 한다.
2-1. SecurityConfig.java
- 먼저 전편에서 깜빡하고 지나쳤던 보안설정 (SecurityConfig)을 아래와 같이 수정한다
package my.was.mywas.configs;
import static my.was.mywas.commons.Constants.TOK_TYP_ACC;
import static my.was.mywas.commons.WebUtils.getAuthToken;
... 중략 ...
@Component public static class AuthFilter extends GenericFilterBean {
/** 토큰발급기 */
@Autowired TokenProvider tokenProvider;
@Override public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) sreq;
req.setAttribute(HttpServletResponse.class.getName(), sres);
String token = getAuthToken(req);
log.trace("TOKEN:{}", token);
Claims claims = null;
/** 토큰이 정상검증되면 인증정보를 현재 컨텍스트(리퀘스트) 에 저장한다. */
if (token != null && (claims = tokenProvider.parseToken(TOK_TYP_ACC, token, req)) != null) {
Authentication auth = tokenProvider.getAuth(claims);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(sreq, sres);
}
}
... 중략 ...
2-2. application.yml
- JPA ORM 파일들을 추가해 주기 위해 아래와 같이 수정한다.
... 중략 ...
jpa:
... 중략 ...
mapping-resources:
- mappings/orm-article.xml
- mappings/orm-common.xml
... 중략 ...
-
이후에 아래와 같은 내용으로 각각
orm-article.xml
,orm-common.xml
2개의 공파일을 만들어준다 -
위치는
/src/main/resources/mappings
폴더 이며 상세 내용은 업무로직 구현시 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
</entity-mappings>
3. 업무 로직 구현
-
업무로직은 1편 : 분석 및 설계 를 참고하여 구현한다
-
파트는 크게
공통
,회원
,게시물
,로그인
등으로 나눈다 -
일반적으로 파트당 컨트롤러(
**Controller
), 서비스(**Service
), 리포지터리(**Repository
) 그리고 DTO 등의 파일을 구현할거다
3-1. 공통 파트 구현 (cmn
)
3-1-1. CommonRepository 구현
-
공통으로 사용하는 DB 작업을 구현한다. 회원가입, 마이페이지 및 로그인 등에 사용되는 DB 암호화 등에 필요.
-
로그인절차 참고
-
/src/main/java/my/was/mywas/works/cmn/CommonRepository.java
JPA 파일을 작성한다.
package my.was.mywas.works.cmn;
import static com.ntiple.commons.ConvertUtil.cast;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Repository
public class CommonRepository {
@Autowired private EntityManager em;
/** DB 자체 기능을 사용한 암호화 encrypt, 단방향 암호화만 사용한다. */
public String dbEncrypt(String value) {
String ret = "";
try {
/** JPA 질의문(Common.dbEncrypt)을 수행한다 */
ret = cast(em.createNamedQuery("Common.dbEncrypt")
.setParameter("value", value)
.getSingleResult(), ret);
log.debug("RESULT:{}", ret);
} catch (Exception e) {
log.error("", e);
}
return ret;
}
}
/src/main/resources/mappings/orm-common.xml
ORM 파일을 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<package>my.was.mywas.works.cmn</package>
<named-native-query name="Common.dbEncrypt">
<query>
<![CDATA[
SELECT RAWTOHEX(HASH('SHA256', :value, 1000))
]]>
</query>
</named-native-query>
</entity-mappings>
3-1-2. CommonService 구현
-
공통 서비스, 환경정보 및 암복호화 키 교환 등을 담당한다.
-
/src/main/java/my/was/mywas/works/cmn/CommonService.java
파일을 작성한다.
package my.was.mywas.works.cmn;
import static com.ntiple.commons.Constants.UTF8;
import static my.was.mywas.commons.Constants.KOKR;
import static my.was.mywas.commons.Constants.RESCD_FAIL;
import static my.was.mywas.commons.Constants.RESCD_OK;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ntiple.commons.CryptoUtil.AES;
import com.ntiple.commons.CryptoUtil.RSA;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.InitObj;
import my.was.mywas.commons.CommonEntity.Result;
import my.was.mywas.commons.SystemSettings;
@Slf4j @Service
public class CommonService {
private static CommonService instance;
@Autowired private SystemSettings settings;
@Autowired private CommonRepository repository;
@PostConstruct public void init() {
log.trace("INIT:{}", CommonService.class);
instance = this;
}
public static CommonService getInstance() {
return instance;
}
/** 시스템 활성화 여부 체크 */
public Result cmn00000000() throws Exception {
String rescd = RESCD_OK;
if (!settings.isAlive()) { rescd = RESCD_FAIL; }
Result ret = Result.builder()
.rescd(rescd)
.build();
return ret;
}
/** 최초 접속 환경정보 조회 */
public InitObj cmn01001a01() throws Exception {
long timestamp = System.currentTimeMillis();
return InitObj.builder()
.current(new Date(timestamp))
.locale(KOKR)
.encoding(UTF8)
.expirecon(settings.getExprAcc())
.check(rsaEncrypt(settings.getKeySecret()))
.build();
}
/** DB 암호화 */
public String dbEncrypt(String value) {
return repository.dbEncrypt(value);
}
/** AES 암호화 */
public String aesEncrypt(String value) {
try {
return AES.encrypt(settings.getKeySecret(), value);
} catch (Exception e) {
log.debug("E:{}", e.getMessage());
}
return "";
}
/** AES 복호화 */
public String aesDecrypt(String value) {
try {
return AES.decrypt(settings.getKeySecret(), value);
} catch (Exception e) {
log.debug("E:{}", e.getMessage());
}
return "";
}
/** RSA 암호화 (AES 키 전송용) */
public String rsaEncrypt(String value) {
try {
return RSA.encrypt(1, settings.getKeyPublic(), value);
} catch (Exception e) {
log.debug("E:{}", e.getMessage());
}
return "";
}
/** RSA 복호화 (AES 키 전송용) */
public String rsaDecrypt(String value) {
try {
return RSA.decrypt(1, settings.getKeyPublic(), value);
} catch (Exception e) {
log.debug("E:{}", e.getMessage());
}
return "";
}
}
3-1-3. CommonController 구현
/src/main/java/my/was/mywas/works/cmn/CommonController.java
파일을 작성한다.
package my.was.mywas.works.cmn;
import static my.was.mywas.commons.RestResponse.response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.InitObj;
import my.was.mywas.commons.CommonEntity.Result;
@Slf4j @RestController
@RequestMapping("/api/cmn")
public class CommonController {
static final String CONTROLLER_TAG1 = "공용 API";
@Autowired CommonService service;
@PostConstruct public void init() {
log.trace("INIT:{}", CommonController.class);
}
@Operation(summary = "시스템 활성화 여부 체크 (cmn00000000)", tags = { CONTROLLER_TAG1 })
@GetMapping("/cmn00000")
public ResponseEntity<Result> cmn00000000() {
return response(() -> service.cmn00000000());
}
@Operation(summary = "최초 접속 환경정보 조회 (cmn01001a01)", tags = { CONTROLLER_TAG1 })
@GetMapping("/cmn01001")
public ResponseEntity<InitObj> cmn01001a01() {
return response(() -> service.cmn01001a01());
}
}
3-2. 회원관리 파트 구현 (usr
)
3-2-1. User 구현
-
회원관리 정보 DTO 를 작성한다.
-
/src/main/java/my/was/mywas/works/usr/User.java
파일을 작성한다.
package my.was.mywas.works.usr;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import my.was.mywas.commons.CommonEntity.SecureOut;
@Entity
@Table(name = "TB_USER")
@Schema(title = "사용자 (User)")
@NoArgsConstructor @AllArgsConstructor
@Getter @Setter @Builder @ToString @SecureOut
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Schema(title = "사용자 고유번호")
private Long id;
@Column(name = "user_id", length = 32, unique = true)
@Schema(title = "사용자 아이디")
private String userId;
@Column(name = "user_nm", length = 32)
@Schema(title = "사용자 이름")
private String userNm;
@Column(name = "passwd", length = 128)
@Schema(title = "사용자 비밀번호")
@SecureOut private String passwd;
@Column(name = "email", length = 128)
@Schema(title = "사용자 이메일")
private String email;
@Column(name = "ctime")
@Schema(title = "생성일시")
private Date ctime;
@Column(name = "utime")
@Schema(title = "수정일시")
private Date utime;
}
3-2-2. UserRepository 구현
/src/main/java/my/was/mywas/works/usr/UserRepository.java
JPA 파일을 작성한다.
package my.was.mywas.works.usr;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/** JPA 에서는 기본적으로 메소드명으로 질의문을 자동생성 가능하므로 필요한 질의문만 기술한다 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
/** 회원id 가 존재하는지 체크 */
Integer countByUserIdEquals(String userId) throws Exception;
/** 회원Id 로 회원정보 조회 */
User findOneByUserIdEquals(String userId) throws Exception;
/** 회원일련번호 로 회원정보조회 */
User findOneById(Long id) throws Exception;
}
- 회원정보 관리는 복잡한 질의가 없으므로 orm 파일이 불필요하다.
3-2-3. UserService 구현
package my.was.mywas.works.usr;
import static com.ntiple.commons.ConvertUtil.convert;
import static my.was.mywas.commons.Constants.NOT_PERMITTED_USER;
import static my.was.mywas.commons.Constants.RESCD_FAIL;
import static my.was.mywas.commons.Constants.RESCD_OK;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.ApiException;
import my.was.mywas.commons.CommonEntity.AuthDetail;
import my.was.mywas.commons.CommonEntity.Result;
import my.was.mywas.works.cmn.CommonService;
@Slf4j @Service
public class UserService {
@Autowired private CommonService cmnservice;
@Autowired private UserRepository repository;
@PostConstruct public void init() {
log.trace("INIT:{}", UserService.class);
}
/** 질의한 userId 로 사용하는 사람이 없다면 OK */
public Result usr01001a01(String userId) throws Exception {
Result ret = Result.builder()
.rescd(RESCD_FAIL)
.build();
if (repository.countByUserIdEquals(userId) == 0) {
ret.setRescd(RESCD_OK);
}
return ret;
}
/** 회원정보 저장 */
public Result usr01001a02(User prm) throws Exception {
Result ret = new Result();
String userId = prm.getUserId();
String password = prm.getPasswd();
Date date = new Date();
/** 비밀번호 평문화 */
String dpassword = cmnservice.aesDecrypt(password);
/** DB 저장을 위해 재암호화 */
String epassword = cmnservice.dbEncrypt(dpassword);
/** 비밀번호 노출 우려가 있으므로 실제로는 출력을 제한한다. */
log.debug("USER-ID:{} / PASSWORD:{} / {} / {}", userId, password, dpassword, epassword);
prm.setPasswd(epassword);
prm.setCtime(date);
prm.setUtime(date);
log.debug("PRM:{}", prm);
repository.save(prm);
ret.setRescd(RESCD_OK);
return ret;
}
/** 마이페이지 조회 */
public User usr01002a01(String userId) throws Exception {
User ret = null;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.debug("AUTH:{} / {}", auth.getName(), auth.getDetails());
AuthDetail detail = convert(auth.getDetails(), new AuthDetail());
log.debug("DETAIL:{}", detail);
/** 로그인된 사용자가 다르다면 조회불가능 */
if (userId != null && userId.equals(auth.getName())) {
ret = repository.findOneByUserIdEquals(userId);
}
return ret;
}
/** 회원정보 수정 (마이페이지 저장) */
public Result usr01002a02(User prm) throws Exception {
Result ret = new Result();
log.debug("PRM:{}", prm);
String password = prm.getPasswd();
Date date = new Date();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
User user = prm;
if (user.getId() != null && user.getId() != 0) {
user = repository.findOneById(user.getId());
/** 로그인된 사용자가 다르다면 오류발생 */
if (!user.getUserId().equals(auth.getName())) { throw new ApiException(0, NOT_PERMITTED_USER); }
if (password != null && !"".equals(password)) {
/** 비밀번호 평문화 */
String dpassword = cmnservice.aesDecrypt(password);
/** DB 저장을 위해 재암호화 */
String epassword = cmnservice.dbEncrypt(dpassword);
/** 비밀번호 노출 우려가 있으므로 실제로는 출력을 제한한다. */
log.debug("USER-ID:{} / PASSWORD:{} / {} / {}", user.getUserId(), password, dpassword, epassword);
user.setPasswd(epassword);
}
user.setEmail(prm.getEmail());
user.setUtime(date);
log.debug("PRM:{}", user);
repository.save(user);
ret.setRescd(RESCD_OK);
}
return ret;
}
/** 회원정보 삭제 */
public Result usr01002a03(String userId) throws Exception {
Result ret = new Result();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (userId != null) {
User user = repository.findOneByUserIdEquals(userId);
if (!user.getUserId().equals(auth.getName())) {
throw new ApiException(0, NOT_PERMITTED_USER);
}
repository.delete(user);
ret.setRescd(RESCD_OK);
}
return ret;
}
}
3-2-4. UserController 구현
/src/main/java/my/was/mywas/works/usr/UserController.java
파일을 작성한다.
package my.was.mywas.works.usr;
import static my.was.mywas.commons.RestResponse.response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.Result;
@Slf4j @RestController
@RequestMapping("/api/usr")
public class UserController {
private static final String CONTROLLER_TAG1 = "사용자 API";
@Autowired UserService usrservice;
@PostConstruct public void init() {
log.trace("INIT:{}", UserController.class);
}
@Operation(summary = "아이디 중복확인 (usr01001a01)", tags = { CONTROLLER_TAG1 })
@GetMapping("/usr01001/{userId}")
public ResponseEntity<Result> usr01001a01(
@PathVariable(value = "userId") @Parameter(description = "사용자 ID") String userId) {
return response(() -> usrservice.usr01001a01(userId));
}
@Operation(summary = "회원가입 (usr01001a02)", tags = { CONTROLLER_TAG1 })
@PutMapping("/usr01001")
public ResponseEntity<Result> usr01001a02(@RequestBody User prm) {
return response(() -> usrservice.usr01001a02(prm));
}
@Operation(summary = "마이페이지 정보조회 (usr01002a01)", tags = { CONTROLLER_TAG1 })
@GetMapping("/usr01002/{userId}")
public ResponseEntity<User> usr01002a01(
@PathVariable(value = "userId") @Parameter(description = "사용자 ID") String userId) {
return response(() -> usrservice.usr01002a01(userId));
}
@Operation(summary = "마이페이지 수정 (usr01002a02)", tags = { CONTROLLER_TAG1 })
@PutMapping("/usr01002")
public ResponseEntity<Result> usr01002a02(@RequestBody User prm) {
return response(() -> usrservice.usr01002a02(prm));
}
@Operation(summary = "회원정보 삭제 (usr01002a03)", tags = { CONTROLLER_TAG1 })
@DeleteMapping("/usr01002/{userId}")
public ResponseEntity<Result> usr01002a03(
@PathVariable(value = "userId") @Parameter(description = "사용자 ID") String userId) {
return response(() -> usrservice.usr01002a03(userId));
}
}
3-3. 로그인 파트 구현 (lgn
)
3-3-1. LoginService 구현
package my.was.mywas.works.lgn;
import static com.ntiple.commons.ConvertUtil.arr;
import static com.ntiple.commons.ConvertUtil.convert;
import static com.ntiple.commons.ConvertUtil.tomap;
import static my.was.mywas.commons.Constants.EXTRA_INFO;
import static my.was.mywas.commons.Constants.TOK_TYP_ACC;
import static my.was.mywas.commons.Constants.TOK_TYP_REF;
import static my.was.mywas.commons.WebUtils.curRequest;
import static my.was.mywas.commons.WebUtils.getAuthToken;
import static my.was.mywas.commons.WebUtils.remoteAddr;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.AuthInfo;
import my.was.mywas.commons.CommonEntity.AuthResult;
import my.was.mywas.commons.CommonEntity.Login;
import my.was.mywas.commons.ApiException;
import my.was.mywas.commons.SystemSettings;
import my.was.mywas.commons.TokenProvider;
import my.was.mywas.works.cmn.CommonService;
import my.was.mywas.works.usr.User;
import my.was.mywas.works.usr.UserRepository;
@Slf4j @Service
public class LoginService {
@Autowired SystemSettings settings;
@Autowired CommonService service;
@Autowired UserRepository usrrepo;
@Autowired TokenProvider tokenprov;
@PostConstruct public void init() {
log.trace("INIT:{}", LoginService.class);
}
/** 로그인 수행 */
public AuthResult lgn01001a01(Login login) throws Exception {
AuthResult ret = new AuthResult();
Authentication auth = null;
String userId = login.getUserId();
String password = login.getPasswd();
try {
/** 평문화 */
String dpassword = service.aesDecrypt(password);
/** DB 에서 비교를 위해 재암호화 */
String epassword = service.dbEncrypt(dpassword);
/** 비밀번호 노출 우려가 있으므로 실제로는 출력을 제한한다. */
log.debug("USER-ID:{} / PASSWORD:{} / {} / {}", userId, password, dpassword, epassword);
User user = usrrepo.findOneByUserIdEquals(userId);
if (user != null) {
if (epassword.equals(user.getPasswd())) {
log.trace("USER:{}", user);
Collection<SimpleGrantedAuthority> authorities =
Arrays.stream(
arr("ROLE_USER"))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
/**
* authenticationToken 객체를 통해 Authentication 객체 생성
* 이 과정에서 CustomUserDetailsService 에서 우리가 재정의한 loadUserByUsername 메서드 호출
* Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken);
*/
auth = new UsernamePasswordAuthenticationToken(userId, dpassword, authorities);
Map<String, Object> info = tomap(AuthInfo.builder()
.ipAddr(remoteAddr())
.userNm(user.getUserNm())
.build());
/** 인증 정보를 기준으로 access 토큰 생성 */
String atoken = tokenprov.createToken(TOK_TYP_ACC, auth, info);
/** 인증 정보를 기준으로 refresh 토큰 생성 */
String rtoken = tokenprov.createToken(TOK_TYP_REF, auth, info);
ret = AuthResult.builder()
.userId(userId)
.userNm(user.getUserNm())
.accessToken(atoken)
.refreshToken(rtoken)
.restyp("")
.rescd("0000")
.build();
} else {
throw new ApiException(0, "USER_NOT_FOUND");
}
} else {
throw new ApiException(0, "USER_NOT_FOUND");
}
} catch (Exception e) {
if (!(e instanceof ApiException)) {
log.debug("E:{}", e);
} else {
throw e;
}
}
return ret;
}
/** 로그인 연장 */
public AuthResult lgn01002a01() {
HttpServletRequest req = curRequest();
String refreshToken = getAuthToken(req);
AuthResult ret = new AuthResult();
try {
/** 리프레시토큰을 받아서 유효하다면 */
Claims claims = tokenprov.parseToken(TOK_TYP_REF, refreshToken, req);
Authentication auth = tokenprov.getAuth(claims);
Object data = claims.get(EXTRA_INFO);
AuthInfo ainfo = convert(data, new AuthInfo());
if (auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
}
/** 시간 연장된 새로운 액세스토큰을 발급하여 리턴한다. */
String atoken = tokenprov.createToken(TOK_TYP_ACC, auth, data);
ret = AuthResult.builder()
.userId(claims.getSubject())
.userNm(ainfo.getUserNm())
.accessToken(atoken)
.restyp("")
.rescd("0000")
.build();
} catch (Exception ignore) {
}
return ret;
}
}
3-3-2. LoginController 구현
/src/main/java/my/was/mywas/works/lgn/LoginController.java
파일을 작성한다.
package my.was.mywas.works.lgn;
import static my.was.mywas.commons.RestResponse.response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.AuthResult;
import my.was.mywas.commons.CommonEntity.Login;
@Slf4j @RestController
@RequestMapping("/api/lgn")
public class LoginController {
static final String CONTROLLER_TAG = "로그인 API";
@Autowired LoginService service;
@PostConstruct public void init() {
log.trace("INIT:{}", LoginController.class);
}
@Operation(summary = "로그인 (lgn01001a01)", tags = { CONTROLLER_TAG })
@PostMapping("/lgn01001")
public ResponseEntity<AuthResult> lgn01001a01(@RequestBody Login login) {
return response(() -> service.lgn01001a01(login));
}
@Operation(summary = "로그인연장 (lgn01002a01)", tags = { CONTROLLER_TAG })
@PostMapping("/lgn01002")
public ResponseEntity<AuthResult> lgn01002a01() {
/** 헤더로 입력된 리프레시 토큰을 사용한다 */
return response(() -> service.lgn01002a01());
}
}
3-4. 게시물 파트 구현 (atc
)
3-4-1. Article 구현
/src/main/java/my/was/mywas/works/atc/Article.java
파일을 작성한다.
package my.was.mywas.works.atc;
import java.io.Serializable;
import java.util.Date;
import org.hibernate.annotations.Comment;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "TB_ARTICLE")
@Schema(title = "게시물 (Article)")
@Comment("게시물 (Article)")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder @ToString
public class Article implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Schema(title = "게시글 고유번호")
private Long id;
@Column(name = "board_id")
@Schema(title = "게시판 ID")
private Long boardId;
@Column(name = "num", length = 32)
@Schema(title = "게시글 번호")
private String num;
@Column(name = "title", length = 128)
@Schema(title = "게시글 제목")
private String title;
@Column(name = "user_id", length = 32)
@Schema(title = "글쓴이 ID")
private String userId;
@Column(name = "user_nm", length = 32)
@Schema(title = "글쓴이 이름")
private String userNm;
@Column(name = "contents", length = 99999)
@Schema(title = "내용")
private String contents;
@Column(name = "ctime")
@Schema(title = "생성일시")
private Date ctime;
@Column(name = "utime")
@Schema(title = "수정일시")
private Date utime;
}
3-4-2. ArticleRepository 구현
/src/main/java/my/was/mywas/works/atc/ArticleRepository.java
파일을 작성한다.
package my.was.mywas.works.atc;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface ArticleRepository extends JpaRepository<Article, Long> {
/** 게시물 상세 조회 (JPA 메소드명에 의한 자동생성) */
Article findOneByIdEquals(Long id);
/** 게시물 갯수 조회 (named-query) */
Integer countArticles(String searchType, String keyword);
/** 게시물 검색 (named-query) */
List<Article> searchArticles(String searchType, String keyword, String orderType, Pageable pageable);
/** 다음 게시물 번호 조회 (named-query) */
Integer getMaxNumber(Long boardId);
/** 게시물 검색 ( SQL 질의문 사용 ) */
@Query(nativeQuery = true)
List<Article> searchArticlesUsingNative(String searchType, String keyword);
}
/src/main/resources/mappings/orm-article.xml
ORM 파일을 작성한다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<package>my.was.mywas.works.atc</package>
<!-- JPQL 질의 -->
<named-query name="Article.searchArticles">
<query>
<![CDATA[
select
new Article(id, boardId, num, title, userId, userNm, '', ctime, utime)
from
Article atc
where
(:searchType is not null or true) and
(:searchType != '1' or title like '%' || :keyword || '%') and
(:searchType != '2' or contents like '%' || :keyword || '%') and
(:searchType != '3' or (
title like '%' || :keyword || '%' or
contents like '%' || :keyword || '%'
))
order by
(case when :orderType = '1' then utime else null end) asc,
(case when :orderType = '2' then utime else null end) desc,
(case when :orderType = '3' then title else null end) asc,
(case when :orderType = '4' then title else null end) desc,
(case when :orderType = '5' then id else null end) asc,
atc.id desc
]]>
</query>
</named-query>
<named-query name="Article.countArticles">
<query>
<![CDATA[
select
count(atc)
from
Article atc
where
(:searchType is not null or true) and
(:searchType != '1' or title like '%' || :keyword || '%') and
(:searchType != '2' or contents like '%' || :keyword || '%') and
(:searchType != '3' or (
title like '%' || :keyword || '%' or
contents like '%' || :keyword || '%'
))
]]>
</query>
</named-query>
<named-query name="Article.getMaxNumber">
<query>
<![CDATA[
select max(case when n is null then 0 else n end) as max from (
select
cast(max(num) as int) as n
from Article a
where boardId = :boardId
union
select
cast(count(num) as int) as n
from Article b
where boardId = :boardId
) a
]]>
</query>
</named-query>
<!-- SQL 질의 -->
<named-native-query name="Article.searchArticlesUsingNative" result-set-mapping="Article">
<query>
<![CDATA[
SELECT
*
FROM
TB_ARTICLE
WHERE
(:searchType IS NOT NULL OR TRUE) AND
(:searchType != '1' OR TITLE LIKE '%' || :keyword || '%') AND
(:searchType != '2' OR CONTENTS LIKE '%' || :keyword || '%') AND
(:searchType != '3' OR (
TITLE LIKE '%' || :keyword || '%' OR
CONTENTS LIKE '%' || :keyword || '%'
))
]]>
</query>
</named-native-query>
<sql-result-set-mapping name="Article">
<entity-result entity-class="my.was.mywas.works.atc.Article"/>
</sql-result-set-mapping>
</entity-mappings>
3-4-3. ArticleService 구현
-
/src/main/java/my/was/mywas/works/atc/ArticleService.java
파일을 작성한다.
package my.was.mywas.works.atc;
import static com.ntiple.commons.ConvertUtil.convert;
import static my.was.mywas.commons.Constants.NOT_PERMITTED_USER;
import static my.was.mywas.commons.Constants.RESCD_OK;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.ApiException;
import my.was.mywas.commons.CommonEntity.AuthDetail;
import my.was.mywas.commons.CommonEntity.Result;
import my.was.mywas.commons.CommonEntity.SearchEntity;
@Slf4j @Service
public class ArticleService {
@Autowired private ArticleRepository repository;
@PostConstruct public void init() {
log.trace("INIT:{}", ArticleService.class);
}
/** 게시글 조회 */
public Article atc01001a02(Long id) throws Exception {
Article ret = null;
ret = repository.findOneByIdEquals(id);
return ret;
}
/** 글 검색 */
public SearchEntity<Article> atc01001a04(SearchEntity<?> prm) throws Exception {
SearchEntity<Article> ret = convert(prm, new SearchEntity<>());
Integer total = repository.countArticles(prm.getSearchType(), prm.getKeyword());
List<Article> list = repository.searchArticles(prm.getSearchType(), prm.getKeyword(),
prm.getOrderType(), prm.getPageable());
ret.setRowTotal(total);
ret.setList(list);
return ret;
}
/** 글 저장 */
public Result atc01001a01(Article prm) throws Exception {
Result ret = new Result();
log.debug("PRM:{}", prm);
Article article = prm;
Date date = new Date();
Long pid = prm.getId();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = auth.getName();
if (pid != null && pid != 0) {
/** 본인이 쓴 글인지 먼저 확인해야 한다. */
article = repository.findOneByIdEquals(pid);
if (article != null && article.getUserId() != null) {
if (userId == null || !userId.equals(article.getUserId())) {
throw new ApiException(0, NOT_PERMITTED_USER);
}
article.setTitle(prm.getTitle());
article.setContents(prm.getContents());
article.setUtime(date);
repository.save(article);
ret.setRescd(RESCD_OK);
}
} else {
/** 새글등록 */
AuthDetail detail = convert(auth.getDetails(), new AuthDetail());
String num = String.valueOf(repository.getMaxNumber(article.getBoardId()) + 1);
article.setBoardId(1L);
article.setNum(num);
article.setUserId(userId);
article.setUserNm(detail.getExtraInfo().getUserNm());
article.setCtime(date);
article.setUtime(date);
repository.save(article);
ret.setRescd(RESCD_OK);
}
return ret;
}
/** 글 삭제 */
public Result atc01001a03(Long id) throws Exception {
Result ret = new Result();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = auth.getName();
Article article = null;
/** 본인이 쓴 글인지 먼저 확인해야 한다. */
article = repository.findOneByIdEquals(id);
if (article != null && article.getUserId() != null) {
if (userId == null || !userId.equals(article.getUserId())) {
throw new ApiException(0, NOT_PERMITTED_USER);
}
repository.deleteById(id);
}
return ret;
}
}
3-4-4. ArticleController 구현
/src/main/java/my/was/mywas/works/atc/ArticleController.java
파일을 작성한다.
package my.was.mywas.works.atc;
import static my.was.mywas.commons.RestResponse.response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.Result;
import my.was.mywas.commons.CommonEntity.SearchEntity;
@Slf4j @RestController
@RequestMapping("/api/atc")
public class ArticleController {
static final String CONTROLLER_TAG1 = "게시글 API";
@Autowired ArticleService service;
@PostConstruct public void init() {
log.trace("INIT:{}", ArticleController.class);
}
@Operation(summary = "게시물 저장/수정 (atc01001a01)", tags = { CONTROLLER_TAG1 })
@PutMapping("/atc01001")
public ResponseEntity<Result> atc01001a01(
@RequestBody Article prm) {
return response(() -> service.atc01001a01(prm));
}
@Operation(summary = "게시글 조회 (atc01001a02)", tags = { CONTROLLER_TAG1 })
@GetMapping("/atc01001/{id}")
public ResponseEntity<Article> atc01001a02(
@PathVariable(value = "id") @Parameter(description = "게시물 고유번호") Long id) {
return response(() -> service.atc01001a02(id));
}
@Operation(summary = "게시글 삭제 (atc01001a03)", tags = { CONTROLLER_TAG1 })
@DeleteMapping("/atc01001/{id}")
public ResponseEntity<Result> atc01001a03(
@PathVariable(value = "id") @Parameter(description = "게시물 고유번호") Long id) {
return response(() -> service.atc01001a03(id));
}
@Operation(summary = "게시글 검색 (atc01001a04)", tags = { CONTROLLER_TAG1 })
@PostMapping("/atc01001")
public ResponseEntity<SearchEntity<Article>> atc01001a04(
@RequestBody SearchEntity<?> prm) {
return response(() -> service.atc01001a04(prm));
}
}
4. 마무리
여기까지 하고나면 게시판 백엔드에서 필요한 거의 모든걸 구현했다고 볼 수 있다.
gradlew bootRun
명령으로 실행하고나서 'http://localhost:8080/swagger/swagger-ui/index.html' 페이지를 접속해 보면
아래와 같이 모든 API 가 기재되어 있고, 실행해 볼 수 있는 SWAGGER 페이지가 뜰 것이다
처음 스터디 시작 할 때에는 게시판이라 심플하게 필요한것만 딱 딱 골라서 작성하고 마무리 하려 했는데...
일이 너무 커져버린거 같다... (이정도면 연습용이 아니라 준 프로젝트 급....)
(사실 초반에 하겠다고 했던 기능둘 중에서도 몇 몇은 빠진거임..)
여튼 JAVA 백엔드 기능들까지는 모두 완료 했으니.
다음부터는 프론트엔드 진도 나가보기로 하자!
결과물 git주소 : https://gitlab.ntiple.com/developers/study-202403