1.무엇이 필요할까?
react-calendar-heatmap에 들어가면 무엇이 필요한지, 데이터는 어떻게 구성되는지 알 수 있다.
<CalendarHeatmap
startDate={new Date('2016-01-01')}
endDate={new Date('2016-04-01')}
values={[
{ date: '2016-01-01', count: 12 },
{ date: '2016-01-22', count: 122 },
{ date: '2016-01-30', count: 38 },
// ...and so on
]}
/>

startDate, endDate 가 필요하고 클라이언트에게 {date, count} 로 이루어진 리스트를 줘야한다.
2.ERD 설계

create Table `grass` (
`grass_id` int auto_increment NOT NULL,
`user_id` INT NOT NULL,
`post_count` int not null,
`grass_date` DATE NOT NULL,
FOREIGN KEY (user_Id)
REFERENCES user (user_Id),
PRIMARY KEY (grass_id)
);
3.구현 흐름
1.게시글 작성할 때 잔디 엔티티 생성
2.날짜 잘라서 yyyy-mm-dd 만들고 이거로 조회함
3-1.잔디 엔티티가 null 값일 경우 새 잔디 엔티티를 만든다.
3-2 잔디 엔티티가 있을 경우 카운트 올린다.
4.잔디엔티티를 dto로 가져오고 dto 리스트를 만든다.
5.시작기간~끝기간에 대한 리스트를 가져온다.
4.GrassEntity 생성
src/main/java/com/example/backend/entity/GrassEntity.java
package com.example.backend.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "Grass")
@Table(name = "Grass")
public class GrassEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long grassId;
private Long userId;
@Column(name = "post_count")
private int postCount;
@Column(name = "grass_date")
private String grassDate;
public GrassEntity(Long userId, String localDate) {
this.userId = userId;
this.postCount = 1;
this.grassDate = localDate;
}
public void updateGrass(GrassEntity grassEntity) {
this.postCount = grassEntity.getPostCount() + 1;
}
}
5. GrassRepository & BoardService
게시글 작성 시 날짜 조회 후 잔디 엔티티 없으면 새로 생성, 있으면 날짜만 추가
src/main/java/com/example/backend/repository/GrassRepository.java
package com.example.backend.repository;
import com.example.backend.entity.GrassEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GrassRepository extends JpaRepository<GrassEntity,Long> {
GrassEntity findByUserIdAndGrassDate(Long userId, String grassDate);
}
userId와 날짜로 GrassEntity를 조회한다.
src/main/java/com/example/backend/service/BoardService.java
public ResponseEntity<? super PostBoardResponseDto> postBoard(PostBoardRequestDto dto, Long userId) {
LocalDate date = LocalDate.now();
String localDate = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
GrassEntity grassEntity = grassRepository.findByUserIdAndGrassDate(userId, localDate);
if (grassEntity == null) {
GrassEntity grass = new GrassEntity(userId, localDate);
grassRepository.save(grass);
}
if (grassEntity != null) {
grassEntity.updateGrass(grassEntity);
grassRepository.save(grassEntity);
}
}

이런식으로 DB에 입력된다. 날짜 변환하는 방법은 이 포스팅을 참고했다.
현재시간 가져오고 yyyy-MM-dd 형태로, String 타입으로 바꾼다.
앞서 어떤 데이터와 데이터타입이 와야하는지를 확인했다. 날짜로 받을 수도 있지만 Entity 설정할 때 String 으로 설정했기 때문에 localDate를 String으로 변환하는 작업을 했다. 그리고 꼭 Entity 뿐만 아니라 날짜 타입보다는 String 타입이 관리하기 편할 것 같다는 생각에 바꾸었다.
6.Controller
잔디 조회하기. DB에 입력된건 grass_id, user_id, post_count, grass_date 인데 클라이언트에게 줘야할 데이터는 post_count, grass_date 다. 이를 dto에 담고 그 dto를 list로 담아 보낼 것이다.
src/main/java/com/example/backend/controller/BoardController.java
/**
* 잔디 불러오기
* localhost:8080/mystudy/grass/?startDate={startDate}&endDate={endDate}
*
* @parm startDate
* @parm endDate
* @parm loginUserDto
* @return response
*/
@GetMapping("/grass")
public ResponseEntity<? super GetGrassResponseDto> grassList(
@RequestParam String startDate,
@RequestParam String endDate,
@IfLogin LoginUserDto loginUserDto
){
ResponseEntity<? super GetGrassResponseDto> response = boardService.getGrassList(loginUserDto.getUserId(), startDate, endDate);
return response;
}
7. GrassItemDto & GetGrassResponseDto
src/main/java/com/example/backend/dto/object/GrassItemDto.java
엔티티에서 필요한 것만 뽑아서 만드는 Dto다.
package com.example.backend.dto.object;
import com.example.backend.entity.GrassEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class GrassItemDto {
private int postCount;
private String grassDate;
public GrassItemDto(GrassEntity grassEntity){
this.postCount=grassEntity.getPostCount();
this.grassDate=grassEntity.getGrassDate();
}
}
src/main/java/com/example/backend/dto/response/board/GetGrassResponseDto.java
위에서 GrassEntity에서 필요한 정보만 DTO에 담았다. 이를 리스트로 담아서 GetGrassResponseDto에 최종적으로 담을 것이다.
package com.example.backend.dto.response.board;
import com.example.backend.common.ResponseCode;
import com.example.backend.common.ResponseMessage;
import com.example.backend.dto.object.GrassItemDto;
import com.example.backend.dto.response.ResponseDto;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.List;
@Getter
public class GetGrassResponseDto extends ResponseDto {
private List<GrassItemDto> grassList;
private GetGrassResponseDto(List<GrassItemDto> grassList) {
super(ResponseCode.SUCCESS, ResponseMessage.SUCCESS);
this.grassList = grassList;
}
public static ResponseEntity<GetGrassResponseDto> success(List<GrassItemDto> grassList){
GetGrassResponseDto result = new GetGrassResponseDto(grassList);
return ResponseEntity.status(HttpStatus.OK).body(result);
}
public static ResponseEntity<ResponseDto> notExistUser(){
ResponseDto result = new ResponseDto(ResponseCode.NOT_EXISTED_USER, ResponseMessage.NOT_EXISTED_USER);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result);
}
}
8. GrassRepository
src/main/java/com/example/backend/repository/GrassRepository.java
import com.example.backend.entity.GrassEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import javax.transaction.Transactional;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface GrassRepository extends JpaRepository<GrassEntity,Long> {
GrassEntity findByUserIdAndGrassDate(Long userId, String grassDate);
@Transactional
@Query(value = "SELECT g FROM Grass g WHERE g.userId=:userId and (g.grassDate >= :startDate and g.grassDate <= :endDate) order by g.grassDate asc")
List<GrassEntity> findAllGrassList(Long userId, String startDate, String endDate);
}
특정 사용자를 찾고 클라이언트가 앞서 넘긴 startDate와 endDate를 이용해 이 사이에 있는 값들을 조회해서 가져오는 Query를 작성한다.
Query작성법 도움이 되었다. where조건에 parameter 를 넣는 방법 부분을 보면 된다.
부등호를 쓴 이유는 between 연산자를 쓰는 것보다 검색 속도가 더 빠르고 올바른 검색 방법이다. (출처: MySQL datetime 날짜로 조회하기)
잠깐, @Transactional 왜 붙였을까? (출처:@Transactional을 쓰는 이유는?)
먼저 transaction은 데이터베이스의 상태를 변환시키는 하나의 논리적인 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산을 의미한다.
A사람의 통장:5만원 출금
B사람 통장: 5만원 입금
을 a->b 순서대로 한다고 가정하자. 중간에 에러가 나서 a사람은 5만원 출금했지만 b사람의 통장에는 입금된게 없는 상황이 발생할 수 있다. 이때 Transactional을 사용한다.
@Transactional 은 예상하지 못한 상황에서 오류가 발생하여 데이터의 부정합이 발생한 경우 다시 원상복귀 해야하는 상황에 대비 하기 위해 Transactional 을 사용한다. 비정상 처리되어 데이터의 부정합이 발생한다면 원래 상태로 원상 복귀하고 정상적으로 프로그램이 진행되었다면 데이터에 최종적으로 적용하게 되어 데이터의 부정합을 방지하게 되는 것이다.
단순히 조회의 기능이라면 읽기 전용인 @Transactional(readOnly = true)으로 바꿔줘도 된다. @transactionl 을 없애도 에러 없이 작동하는데 문제가 없지만 정상적인 상황인 경우 readOnly는 트랜잭션 범위는 유지하되 조회 기능만 남겨두어 조회속도가 개선되게 하기 위해 사용한다.
9. BoardServiceImplement
src/main/java/com/example/backend/service/implement/BoardServiceImplement.java
public ResponseEntity<? super GetGrassResponseDto> getGrassList(Long userId, String startDate, String endDate) {
List<GrassItemDto> grassList = new ArrayList<>();
try {
UserEntity userEntity=userRepository.findByUserId(userId);
if(userEntity==null) return GetGrassResponseDto.notExistUser();
List<GrassEntity> grassEntities = grassRepository.findAllGrassList(userId, startDate, endDate);
for(GrassEntity grassEntity:grassEntities){
GrassItemDto grassItemDto = new GrassItemDto(grassEntity);
grassList.add(grassItemDto);
}
} catch (Exception exception) {
exception.printStackTrace();
return ResponseDto.databaseError();
}
return GetGrassResponseDto.success(grassList);
}
grassRepository를 통해 userId, startDate, endDate를 가지고 사용자의 잔디를 리스트로 조회한다.
이후 for문을 돌려서 리스트에서 하나씩 꺼낸다. Dto를 만들어서 원하는 값만 grassEntity에서 뽑아낸다.
이후 grassList(dto 리스트)에 저장을 한다.
GetGrassResponseDto 에서 이를 받아주면 된다.
10. 포스트맨

11.github
기타 조건들은 src/main/java/com/example/backend/common 폴더에서 확인 가능하다.
내가 생각한 방법이 효율적인가? 에 대한 고민은 계속 해야겠다.
더 효율적인 방법이나 조언이 있다면 댓글로 남겨주시면 감사하겠습니다.
'Springboot' 카테고리의 다른 글
| String 을 LocalDate 바꾸는 법 (0) | 2024.01.20 |
|---|---|
| Spring boot 구글 소셜로그인 OAuth 로그인 구현 (0) | 2024.01.10 |
| 스프링부트 서블릿 HttpservletResponse 500번 상태코드, 메세지 바꾸기 (0) | 2023.12.30 |
| 홍팍 스프링부트 6 (0) | 2023.08.03 |
| Spring 홍팍 스프링부트, 입문 강의 기록 3 (0) | 2023.08.03 |