커미션 블로그에서 내가 그린 그림들을 한눈에 보여줄 페이지를 만들 것이다. 또한 커미션 타입별로 탭으로 나누어 보여줄 생각이다.
UI를 크게 벌일 생각이 없어서 아래 좋아하는 작가 사이트처럼 간단히 하자. 상세 페이지도 없고 제목, 이미지, 링크만 있다.
참고 https://uekuraeku.com/
개발 환경 : Windows + Spring boot + Gradle
전체 프로젝트 구조
Post~ 관련 클래스들에 포커스를 두자.
import com.cms.world.utils.GlobalCode;
import com.cms.world.utils.StringUtil;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Entity
@Getter
@Setter
@Table(name = "post")
public class PostDto {
public PostDto() {
}
@Builder
public PostDto(String TITLE, String CONTENT, String IMG_URL, String TP_CD) {
this.TITLE = TITLE;
this.CONTENT = CONTENT;
this.IMG_URL = IMG_URL;
this.TP_CD = TP_CD;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long ID;
@Column
private String TITLE;
@Column
private String CONTENT;
@Column
private String IMG_URL;
@Column
private String TP_CD; // 커미션 타입 코드
@Column
private String RGTR_DT;
@PrePersist
public void onPrePersist() { // 디비에 넣기 전에 현재 시각 날짜를 format해서 insert
this.RGTR_DT = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
if (StringUtil.isEmpty(this.TP_CD)) this.TP_CD = GlobalCode.TYPE_SINGLE.getCode(); // 타입 코드가 없으면 1인 기본 설정
}
}
//참고로 StringUtil.isEmpty()는 커스텀이다.
//StringUtil.java
public static boolean isEmpty(String str) {
return (str == null || str.isEmpty());
}
나도 JPA가 익숙지 않아서 insert, findall만 하는데 시간이 오래 걸렸다;;
간단히 문법을 설명하자면
@Table
: 디비 테이블이 될 객체이다.
@Entity
: JPA의 관리 대상이다.
@Getter
, @Setter
: 이거 없으면 쿼리만 돌아가고 findAll이 안된다. (사실 당연함)
@Column(name ="id")
: 테이블 컬럼 (name 지정 가능)
@Id
, @GeneratedValue(strategy = GenerationType.IDENTITY)
: pk 컬럼에 추가한다. @GeneratedValue는 DB의 auto_increment다.
<기타>
@Data
를 지양하기 때문에 별도로 get, set 으로 추가한 것이다. @AllArgsConstructor
를 지양하려고 @Builder
를 추가했다. @PrePersist
를 사용하면 여러 작업을 할 수 있다.
1. 현재 시간을 vo때부터 설정하지 않고 테이블 insert 전에 format해서 넣는다.
2. 타입 코드 등이 null일 때 기본값을 설정할 수 있다.
@PrePersist
public void onPrePersist() { // 디비에 넣기 전에 현재 시각 날짜를 format해서 insert
this.RGTR_DT = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
if (StringUtil.isEmpty(this.TP_CD)) this.TP_CD = GlobalCode.TYPE_SINGLE.getCode(); // 타입 코드가 없으면 1인 기본 설정
}
타입 코드는 아래와 같이 정리한다.
GlobalCode.java
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum GlobalCode { //타입 등의 코드
TYPE_SINGLE("TY01", "1인 기본"),
TYPE_SINGLE_BG("TY02", "1인 배경"),
TYPE_COUP("TY03", "2인 기본"),
TYPE_COUP_BG("TY04", "2인 배경"),
TYPE_MULTI_BG("TY05", "3인 이상 배경");
private final String code;
private final String desc;
}
개발 중에 insert할 때의 이미지 타입과 select할 때의 이미지 타입이 달라서 entity 하나로 안되더라.
구조도가 대략 아래 같다.
그래서 client -> server로 가는 쪽의 폼은 vo로, 서버단에서 가져오거나 디비랑 연동되는 쪽은 dto로 각각 추가했다.
vo와 dto가 뭐가 다른가에 대해서는 다루지 않겠다;;
PostVo.java
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;
@Getter
@Setter
@ToString
public class PostVo {
private Long id;
private String title;
private String content;
private String type; //커미션 타입
private MultipartFile img; //dto와 다른 부분.
private String reg_date;
@Builder
public PostVo (String title, String content, MultipartFile img, String type,String reg_date) {
this.title = title;
this.content = content;
this.img = img;
this.type = type;
this.reg_date = reg_date;
}
}
나의 경우 원인이 아주~ 많았다.
1. 테이블, 컬럼명 대소문자 차이
2. @Getter
, @Setter
부재
3. import 잘못해서 꼬임.
왜 컬럼명만 소문자인 것일까....
참고 https://velog.io/@gillog/JPA-Spring-Boot-JPA-Entity-Table-대-소문자-구분-못하는-경우-해결
application.yml에 가서
... 생략
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl # 테이블 컬럼명 대소문자 구분 못할 때 추가
ddl-auto: update
show-sql: true
physical-strategy 부분을 추가해 준다. 또한 show-sql을 쓰면 쿼리를 로그로 볼 수 있어서 추가했다.
따란
PostRepository.java
import com.cms.world.domain.dto.PostDto;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<PostDto, Long> {
}
<객체, pk의 데이터 타입> 식으로 들어간다. JpaRepository 또는 CRUDRepository를 상속받을 수 있다.
PostService.java
import com.cms.world.domain.dto.PostDto;
import com.cms.world.domain.vo.PostVo;
import com.cms.world.repository.PostRepository;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
@Service
public class PostService {
private PostRepository repository;
private S3UploadService s3UploadService;
public PostService (PostRepository postRepository, S3UploadService s3UploadService) {
this.repository = postRepository;
this.s3UploadService = s3UploadService;
}
public void add(PostVo vo) throws IOException {
String imgUrl = s3UploadService.saveFile(vo.getImg());
PostDto dto = PostDto.builder().TITLE(vo.getTitle()).CONTENT(vo.getContent()).IMG_URL(imgUrl).build();
repository.save(dto);
}
public List<PostDto> list () {
return repository.findAll();
}
}
PostController.java
import com.cms.world.domain.dto.PostDto;
import com.cms.world.domain.vo.PostVo;
import com.cms.world.service.PostService;
import com.cms.world.utils.GlobalStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/post")
public class PostController { // 블로그 이미지 리스트
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@PostMapping("/insert")
public ResponseEntity<String> add(PostVo vo) throws IOException {
try {
service.add(vo);
return ResponseEntity.ok().body(GlobalStatus.SUCCESS.getMsg());
} catch (IOException e) {
return ResponseEntity.status(GlobalStatus.INTERNAL_SERVER_ERR.getStatus()).body(GlobalStatus.INTERNAL_SERVER_ERR.getMsg());
}
}
@GetMapping("/list")
public List<PostDto> list() {
return service.list();
}
}
GlobalStatus.java
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum GlobalStatus {
SUCCESS(200, "성공"),
INVALID_PARAMETER(400, "잘못된 요청"),
INTERNAL_SERVER_ERR(500, "서버 에러");
private final int status;
private final String msg;
}