diff --git a/README.md b/README.md index faaef33..ee0a4e7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,50 @@ -## TO-DO +## 배운점 -### Document +### SimpleJdbcInsertOperations, SqlParameterSource -- [ ] 결재자는 생성자에서만 추가하고 불변으로 관리해야 하지 않나? 왜 따로 add하지? -- [ ] 결재자 중복 확인이 필요할 것 같다. -- [ ] ApprovalState 필드를 가져야 하는가? DocumentApprovals에다가 물어보면 되는 거 아닌가? 연산 시간때문에 별도의 필드를 갖는게 나은가? +SQL을 하드코딩하는 것보다는 한 단계 추상화된 JDBC 기술들을 사용하는 게 더 낫다. -### DocumentApproval +- 오탈자 등 휴먼 에러로 인한 버그를 줄일 수 있다. +- 재사용성과 유지보수성이 개선된다. + - SQL을 직접 작성하면 끽해봐야 매직 넘버나 스트링을 빼는 정도로만 재사용성을 높일 수 있다. + - 반면 얘네들은 엔티티를 자동 매핑해주기도 하고... 스트링의 나열로 보는 것보다 가독성도 낫다. +- 그래도 여전히 불편하다. -- [ ] approvalOrder 필드를 가져야 하는가? 어차피 DocumentApprovals의 리스트가 순서를 다 알고 있지 않나? +## 의문점 +### 왜 엔티티를 쓸 일이 없는가 +DocumentApproval 엔티티를 만들지 않았는데도 API가 동작한다....? 이래도 되는 건가... 무슨 짓을 저지른 건가... +### documentService.findById() + +1. 테이블별로 메서드 분리해서 각각 찾아오기 + - document, user, document_approval 테이블 각각 조회...? 에반데 + - 게다가 SELECT문에서 조인해서 document 가져오면 document rowMapper도 같이 수정해야됨 진짜 구리다 + - 우선 response 형식에 approvers가 없으니까 document_approval 테이블은 패스 + - 근데 documentDao가 user 테이블까지 접근하는건 짱별로인듯 + +2. 테이블 조인해서 한 번에 찾아오기 + - 이럴거면 테이블별로 dao를 왜 나누냐..? + +### DB 테스트 시 초기 데이터로의 의존성 + +현재 dao 테스트가 data.sql에서 제공하는 초기 데이터셋에 의존하고 있다. + +- findById(): 문서가 이미 저장되어 있어야 find가 가능하다. +- addDocument(): pk를 검증할 수 없다. auto_increment pk 값을 모르기 때문이다. + +해결책은 뭘까 + +1. 그냥 초기 적재 데이터에 의존한다. + - 적재 데이터가 바뀌면 그 때마다 테스트가 깨지지 않나.. + - 영향 크게 안 받게 해놔도 언젠간 깨지지 않나.. +2. find 하기 전에 데이터를 넣던지, add 하기 전에 pk를 미리 알아 온다. + - 이전에 수행하는 로직에 의존하지 않나.. + +### 테스트 실행 방식에 따른 결과 차이 + +DocumentH2DaoTest의 find_by_drafter_success() 결과가 독립적이지 않다. + +- 하나만 따로 돌리면 성공하는데, 전체 테스트를 한 번에 돌리면 fail한다. +- 이유를 모르겠다.... 머냐... \ No newline at end of file diff --git a/build.gradle b/build.gradle index fe00c55..1ad792b 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ dependencies { testImplementation('org.springframework.boot:spring-boot-starter-test') // JDBC -// implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' // JPA // implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/src/main/java/playground/learning/ApprovalState.java b/src/main/java/learning/ApprovalState.java similarity index 95% rename from src/main/java/playground/learning/ApprovalState.java rename to src/main/java/learning/ApprovalState.java index dfc8e7b..659d4cb 100644 --- a/src/main/java/playground/learning/ApprovalState.java +++ b/src/main/java/learning/ApprovalState.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -21,7 +21,7 @@ public ApprovalState approve() { if (this == CANCELED) { throw new IllegalArgumentException("이미 결재자로부터 거절된 문서입니다."); } - + return APPROVED; } diff --git a/src/main/java/playground/learning/Category.java b/src/main/java/learning/Category.java similarity index 89% rename from src/main/java/playground/learning/Category.java rename to src/main/java/learning/Category.java index 6570774..5365213 100644 --- a/src/main/java/playground/learning/Category.java +++ b/src/main/java/learning/Category.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/playground/learning/Document.java b/src/main/java/learning/Document.java similarity index 97% rename from src/main/java/playground/learning/Document.java rename to src/main/java/learning/Document.java index ce6fe5d..bb3d57b 100644 --- a/src/main/java/playground/learning/Document.java +++ b/src/main/java/learning/Document.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/playground/learning/DocumentApproval.java b/src/main/java/learning/DocumentApproval.java similarity index 97% rename from src/main/java/playground/learning/DocumentApproval.java rename to src/main/java/learning/DocumentApproval.java index 8be4f13..3789a22 100644 --- a/src/main/java/playground/learning/DocumentApproval.java +++ b/src/main/java/learning/DocumentApproval.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import lombok.Builder; diff --git a/src/main/java/playground/learning/DocumentApprovals.java b/src/main/java/learning/DocumentApprovals.java similarity index 91% rename from src/main/java/playground/learning/DocumentApprovals.java rename to src/main/java/learning/DocumentApprovals.java index ae0aa82..a009d56 100644 --- a/src/main/java/playground/learning/DocumentApprovals.java +++ b/src/main/java/learning/DocumentApprovals.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import java.util.ArrayList; import java.util.List; @@ -20,8 +20,8 @@ public static DocumentApprovals empty() { public void addApprovals(List approvers) { List documentApprovals = IntStream.range(0, approvers.size()) - .mapToObj(index -> DocumentApproval.of(approvers.get(index), index + 1)) - .collect(Collectors.toList()); + .mapToObj(index -> DocumentApproval.of(approvers.get(index), index + 1)) + .collect(Collectors.toList()); this.approvals.addAll(documentApprovals); } diff --git a/src/main/java/playground/learning/User.java b/src/main/java/learning/User.java similarity index 86% rename from src/main/java/playground/learning/User.java rename to src/main/java/learning/User.java index de8ec69..96cebc1 100644 --- a/src/main/java/playground/learning/User.java +++ b/src/main/java/learning/User.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import lombok.Builder; import lombok.EqualsAndHashCode; diff --git a/src/main/java/playground/domain/document/DocumentController.java b/src/main/java/playground/domain/document/DocumentController.java new file mode 100644 index 0000000..c4cfcf2 --- /dev/null +++ b/src/main/java/playground/domain/document/DocumentController.java @@ -0,0 +1,21 @@ +package playground.domain.document; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import playground.domain.document.dto.AddDocumentRequest; +import playground.domain.document.dto.BoxDocument; +import playground.domain.document.dto.SingleDocument; + +import java.util.List; + +@RequestMapping(path = "/api/documents", + produces = MediaType.APPLICATION_JSON_VALUE) +public interface DocumentController { + + ResponseEntity findDocument(Long id); + + ResponseEntity addDocument(AddDocumentRequest addDocumentRequest); + + ResponseEntity> findOutbox(Long drafterId); +} diff --git a/src/main/java/playground/domain/document/DocumentControllerImpl.java b/src/main/java/playground/domain/document/DocumentControllerImpl.java new file mode 100644 index 0000000..796c073 --- /dev/null +++ b/src/main/java/playground/domain/document/DocumentControllerImpl.java @@ -0,0 +1,50 @@ +package playground.domain.document; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import playground.domain.document.dto.AddDocumentRequest; +import playground.domain.document.dto.BoxDocument; +import playground.domain.document.dto.OutBox; +import playground.domain.document.dto.SingleDocument; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class DocumentControllerImpl implements DocumentController { + + private final DocumentService documentService; + + @Override + @GetMapping("/{id}") + public ResponseEntity findDocument(@PathVariable Long id) { + SingleDocument result = documentService.findById(id); + + return ResponseEntity + .status(HttpStatus.OK) + .body(result); + } + + @Override + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity addDocument(@RequestBody AddDocumentRequest addDocumentRequest) { + Long documentId = documentService.addDocument(addDocumentRequest); + + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(documentId); + } + + @Override + @GetMapping("/outbox") + public ResponseEntity> findOutbox(@RequestParam Long drafterId) { + OutBox outbox = documentService.findOutboxOf(drafterId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(outbox.getElements()); + } +} diff --git a/src/main/java/playground/domain/document/DocumentService.java b/src/main/java/playground/domain/document/DocumentService.java new file mode 100644 index 0000000..e503e74 --- /dev/null +++ b/src/main/java/playground/domain/document/DocumentService.java @@ -0,0 +1,14 @@ +package playground.domain.document; + +import playground.domain.document.dto.AddDocumentRequest; +import playground.domain.document.dto.OutBox; +import playground.domain.document.dto.SingleDocument; + +public interface DocumentService { + + SingleDocument findById(Long id); + + Long addDocument(AddDocumentRequest addDocumentRequest); + + OutBox findOutboxOf(Long drafterId); +} diff --git a/src/main/java/playground/domain/document/DocumentServiceImpl.java b/src/main/java/playground/domain/document/DocumentServiceImpl.java new file mode 100644 index 0000000..07e6816 --- /dev/null +++ b/src/main/java/playground/domain/document/DocumentServiceImpl.java @@ -0,0 +1,68 @@ +package playground.domain.document; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.stereotype.Service; +import playground.domain.document.dao.DocumentApprovalDao; +import playground.domain.document.dao.DocumentDao; +import playground.domain.document.dto.AddDocumentRequest; +import playground.domain.document.dto.OutBox; +import playground.domain.document.dto.SingleDocument; +import playground.domain.document.dto.param.AddDocumentApprovalParam; +import playground.domain.document.dto.param.AddDocumentParam; +import playground.domain.document.entity.Document; +import playground.domain.document.entity.Documents; +import playground.domain.user.dao.UserDao; +import playground.domain.user.entity.User; + +import java.util.List; +import java.util.NoSuchElementException; + +@Service +@RequiredArgsConstructor +public class DocumentServiceImpl implements DocumentService { + + private final DocumentDao documentDao; + private final DocumentApprovalDao documentApprovalDao; + private final UserDao userDao; + + @Override + public SingleDocument findById(Long id) { + try { + Document document = documentDao.findById(id); + User drafter = userDao.findDrafterOf(document); + + return SingleDocument.of(document, drafter); + } catch (IncorrectResultSizeDataAccessException e) { + throw new NoSuchElementException("id에 맞는 document가 없음"); + } + } + + @Override + public Long addDocument(AddDocumentRequest addDocumentRequest) { + AddDocumentParam addDocumentParam = AddDocumentParam.of(addDocumentRequest); + Long documentId = documentDao.addDocument(addDocumentParam); + + AddDocumentApprovalParam addDocumentApprovalParam = AddDocumentApprovalParam.of(documentId, addDocumentRequest); + addDocumentApproval(addDocumentApprovalParam); + + return documentId; + } + + @Override + public OutBox findOutboxOf(Long drafterId) { + List elements = documentDao.findByDrafter(drafterId); + Documents documents = new Documents(elements); + + return OutBox.of(documents); + } + + private void addDocumentApproval(AddDocumentApprovalParam addDocumentApprovalParam) { + try { + documentApprovalDao.addApprovals(addDocumentApprovalParam); + } catch (DataIntegrityViolationException e) { + throw new IllegalArgumentException("존재하지 않는 유저를 결재자로 등록함"); + } + } +} diff --git a/src/main/java/playground/domain/document/dao/DocumentApprovalDao.java b/src/main/java/playground/domain/document/dao/DocumentApprovalDao.java new file mode 100644 index 0000000..a5759d3 --- /dev/null +++ b/src/main/java/playground/domain/document/dao/DocumentApprovalDao.java @@ -0,0 +1,8 @@ +package playground.domain.document.dao; + +import playground.domain.document.dto.param.AddDocumentApprovalParam; + +public interface DocumentApprovalDao { + + void addApprovals(AddDocumentApprovalParam addDocumentApprovalParam); +} diff --git a/src/main/java/playground/domain/document/dao/DocumentDao.java b/src/main/java/playground/domain/document/dao/DocumentDao.java new file mode 100644 index 0000000..fe16e85 --- /dev/null +++ b/src/main/java/playground/domain/document/dao/DocumentDao.java @@ -0,0 +1,15 @@ +package playground.domain.document.dao; + +import playground.domain.document.dto.param.AddDocumentParam; +import playground.domain.document.entity.Document; + +import java.util.List; + +public interface DocumentDao { + + Document findById(Long id); + + Long addDocument(AddDocumentParam addDocumentParam); + + List findByDrafter(Long drafterId); +} diff --git a/src/main/java/playground/domain/document/dao/DocumentRowMapper.java b/src/main/java/playground/domain/document/dao/DocumentRowMapper.java new file mode 100644 index 0000000..05433cc --- /dev/null +++ b/src/main/java/playground/domain/document/dao/DocumentRowMapper.java @@ -0,0 +1,30 @@ +package playground.domain.document.dao; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import playground.domain.document.entity.ApprovalState; +import playground.domain.document.entity.Category; +import playground.domain.document.entity.Document; +import playground.domain.user.entity.User; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class DocumentRowMapper implements RowMapper { + + @Override + public Document mapRow(ResultSet rs, int rowNum) throws SQLException { + return Document.builder() + .id(rs.getLong("id")) + .title(rs.getString("title")) + .contents(rs.getString("contents")) + .drafter(User.builder() + .id(rs.getLong("drafter_id")) + .build()) + .category(Category.valueOf(rs.getString("category"))) + .approvalState(ApprovalState.valueOf(rs.getString("approval_state"))) + .createdAt(rs.getTimestamp("created_at")) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dao/h2/DocumentApprovalH2Dao.java b/src/main/java/playground/domain/document/dao/h2/DocumentApprovalH2Dao.java new file mode 100644 index 0000000..a52babd --- /dev/null +++ b/src/main/java/playground/domain/document/dao/h2/DocumentApprovalH2Dao.java @@ -0,0 +1,33 @@ +package playground.domain.document.dao.h2; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.core.simple.SimpleJdbcInsertOperations; +import org.springframework.stereotype.Repository; +import playground.domain.document.dao.DocumentApprovalDao; +import playground.domain.document.dto.param.AddDocumentApprovalParam; +import playground.domain.document.dto.param.sql.AddDocumentApprovalSqlParam; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class DocumentApprovalH2Dao implements DocumentApprovalDao { + + private final JdbcTemplate jdbcTemplate; + + @Override + public void addApprovals(AddDocumentApprovalParam addDocumentApprovalParam) throws DataIntegrityViolationException { + Long documentId = addDocumentApprovalParam.getDocumentId(); + List approversId = addDocumentApprovalParam.getApproversId(); + + SimpleJdbcInsertOperations insertOperations = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("DOCUMENT_APPROVAL"); + + approversId.stream() + .map(approverId -> AddDocumentApprovalSqlParam.of(documentId, approverId)) + .forEach(insertOperations::execute); + } +} diff --git a/src/main/java/playground/domain/document/dao/h2/DocumentH2Dao.java b/src/main/java/playground/domain/document/dao/h2/DocumentH2Dao.java new file mode 100644 index 0000000..e9f4456 --- /dev/null +++ b/src/main/java/playground/domain/document/dao/h2/DocumentH2Dao.java @@ -0,0 +1,48 @@ +package playground.domain.document.dao.h2; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.core.simple.SimpleJdbcInsertOperations; +import org.springframework.stereotype.Repository; +import playground.domain.document.dao.DocumentDao; +import playground.domain.document.dao.DocumentRowMapper; +import playground.domain.document.dto.param.AddDocumentParam; +import playground.domain.document.dto.param.sql.AddDocumentSqlParam; +import playground.domain.document.entity.Document; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class DocumentH2Dao implements DocumentDao { + + private final JdbcTemplate jdbcTemplate; + private final DocumentRowMapper documentRowMapper; + + @Override + public Document findById(Long id) throws EmptyResultDataAccessException { + String query = "SELECT * FROM DOCUMENT WHERE ID=" + id; + + return jdbcTemplate.queryForObject(query, documentRowMapper); + } + + @Override + public Long addDocument(AddDocumentParam addDocumentParam) { + SimpleJdbcInsertOperations insertOperations = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("DOCUMENT") + .usingGeneratedKeyColumns("ID"); + + return insertOperations + .executeAndReturnKey(AddDocumentSqlParam.of(addDocumentParam)) + .longValue(); + } + + @Override + public List findByDrafter(Long drafterId) { + String query = "SELECT * FROM DOCUMENT WHERE DRAFTER_ID=" + drafterId; + + return jdbcTemplate.query(query, documentRowMapper); + } +} diff --git a/src/main/java/playground/domain/document/dto/AddDocumentRequest.java b/src/main/java/playground/domain/document/dto/AddDocumentRequest.java new file mode 100644 index 0000000..01f813d --- /dev/null +++ b/src/main/java/playground/domain/document/dto/AddDocumentRequest.java @@ -0,0 +1,17 @@ +package playground.domain.document.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AddDocumentRequest { + + private final String title; + private final String category; + private final String contents; + private final Long drafterId; + private final List approverIds; +} diff --git a/src/main/java/playground/domain/document/dto/BoxDocument.java b/src/main/java/playground/domain/document/dto/BoxDocument.java new file mode 100644 index 0000000..d04a414 --- /dev/null +++ b/src/main/java/playground/domain/document/dto/BoxDocument.java @@ -0,0 +1,28 @@ +package playground.domain.document.dto; + +import lombok.Builder; +import lombok.Getter; +import playground.domain.document.entity.Document; + +@Getter +@Builder +public class BoxDocument { + + private final Long id; + private final String title; + private final String category; + private final String categoryText; + private final String approvalState; + private final String approvalStateText; + + public static BoxDocument of(Document document) { + return BoxDocument.builder() + .id(document.getId()) + .title(document.getTitle()) + .category(document.getCategory().name()) + .categoryText(document.getCategory().getText()) + .approvalState(document.getApprovalState().name()) + .approvalStateText(document.getApprovalState().getText()) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dto/OutBox.java b/src/main/java/playground/domain/document/dto/OutBox.java new file mode 100644 index 0000000..a6484d1 --- /dev/null +++ b/src/main/java/playground/domain/document/dto/OutBox.java @@ -0,0 +1,23 @@ +package playground.domain.document.dto; + +import lombok.Builder; +import lombok.Getter; +import playground.domain.document.entity.Documents; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter +@Builder +public class OutBox { + + private final List elements; + + public static OutBox of(Documents documents) { + return OutBox.builder() + .elements(documents.stream() + .map(BoxDocument::of) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dto/SingleDocument.java b/src/main/java/playground/domain/document/dto/SingleDocument.java new file mode 100644 index 0000000..6c097eb --- /dev/null +++ b/src/main/java/playground/domain/document/dto/SingleDocument.java @@ -0,0 +1,37 @@ +package playground.domain.document.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import playground.domain.document.entity.Document; +import playground.domain.user.entity.User; + +@Getter +@Builder +@RequiredArgsConstructor +public class SingleDocument { + + private final Long id; + private final String title; + private final String contents; + private final Long userId; + private final String userName; + private final String category; + private final String categoryText; + private final String approvalState; + private final String approvalStateText; + + public static SingleDocument of(Document document, User drafter) { + return SingleDocument.builder() + .id(document.getId()) + .title(document.getTitle()) + .contents(document.getContents()) + .userId(drafter.getId()) + .userName(drafter.getName()) + .category(document.getCategory().name()) + .categoryText(document.getCategory().getText()) + .approvalState(document.getApprovalState().name()) + .approvalStateText(document.getApprovalState().getText()) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dto/param/AddDocumentApprovalParam.java b/src/main/java/playground/domain/document/dto/param/AddDocumentApprovalParam.java new file mode 100644 index 0000000..9306bc8 --- /dev/null +++ b/src/main/java/playground/domain/document/dto/param/AddDocumentApprovalParam.java @@ -0,0 +1,24 @@ +package playground.domain.document.dto.param; + +import lombok.Builder; +import lombok.Getter; +import playground.domain.document.dto.AddDocumentRequest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Getter +@Builder +public class AddDocumentApprovalParam { + + private final Long documentId; + private final List approversId; + + public static AddDocumentApprovalParam of(Long documentId, AddDocumentRequest addDocumentRequest) { + return AddDocumentApprovalParam.builder() + .documentId(documentId) + .approversId(Collections.unmodifiableList(new ArrayList<>(addDocumentRequest.getApproverIds()))) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dto/param/AddDocumentParam.java b/src/main/java/playground/domain/document/dto/param/AddDocumentParam.java new file mode 100644 index 0000000..efc080d --- /dev/null +++ b/src/main/java/playground/domain/document/dto/param/AddDocumentParam.java @@ -0,0 +1,28 @@ +package playground.domain.document.dto.param; + +import lombok.Builder; +import lombok.Getter; +import playground.domain.document.dto.AddDocumentRequest; + +import static playground.domain.document.entity.ApprovalState.DEFAULT_APPROVAL_STATE_TEXT; + +@Getter +@Builder +public class AddDocumentParam { + + private final String title; + private final String contents; + private final Long drafterId; + private final String categoryText; + private final String approvalStateText; + + public static AddDocumentParam of(AddDocumentRequest addDocumentRequest) { + return AddDocumentParam.builder() + .title(addDocumentRequest.getTitle()) + .contents(addDocumentRequest.getContents()) + .drafterId(addDocumentRequest.getDrafterId()) + .categoryText(addDocumentRequest.getCategory()) + .approvalStateText(DEFAULT_APPROVAL_STATE_TEXT) + .build(); + } +} diff --git a/src/main/java/playground/domain/document/dto/param/sql/AddDocumentApprovalSqlParam.java b/src/main/java/playground/domain/document/dto/param/sql/AddDocumentApprovalSqlParam.java new file mode 100644 index 0000000..595b13a --- /dev/null +++ b/src/main/java/playground/domain/document/dto/param/sql/AddDocumentApprovalSqlParam.java @@ -0,0 +1,16 @@ +package playground.domain.document.dto.param.sql; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import static playground.domain.document.entity.ApprovalState.DEFAULT_APPROVAL_STATE_TEXT; + +public class AddDocumentApprovalSqlParam extends MapSqlParameterSource { + + public static SqlParameterSource of(Long documentId, Long approverId) { + return new MapSqlParameterSource() + .addValue("document_id", documentId) + .addValue("approver_id", approverId) + .addValue("approval_state", DEFAULT_APPROVAL_STATE_TEXT); + } +} diff --git a/src/main/java/playground/domain/document/dto/param/sql/AddDocumentSqlParam.java b/src/main/java/playground/domain/document/dto/param/sql/AddDocumentSqlParam.java new file mode 100644 index 0000000..f0a8c98 --- /dev/null +++ b/src/main/java/playground/domain/document/dto/param/sql/AddDocumentSqlParam.java @@ -0,0 +1,20 @@ +package playground.domain.document.dto.param.sql; + +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import playground.domain.document.dto.param.AddDocumentParam; + +import java.time.LocalDateTime; + +public class AddDocumentSqlParam extends MapSqlParameterSource { + + public static SqlParameterSource of(AddDocumentParam addDocumentParam) { + return new MapSqlParameterSource() + .addValue("title", addDocumentParam.getTitle()) + .addValue("contents", addDocumentParam.getContents()) + .addValue("drafter_id", addDocumentParam.getDrafterId()) + .addValue("category", addDocumentParam.getCategoryText()) + .addValue("approval_state", addDocumentParam.getApprovalStateText()) + .addValue("created_at", LocalDateTime.now()); + } +} diff --git a/src/main/java/playground/domain/document/entity/ApprovalState.java b/src/main/java/playground/domain/document/entity/ApprovalState.java new file mode 100644 index 0000000..3880558 --- /dev/null +++ b/src/main/java/playground/domain/document/entity/ApprovalState.java @@ -0,0 +1,17 @@ +package playground.domain.document.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ApprovalState { + + DRAFTING("결재중"), + APPROVED("승인"), + CANCELED("거절"); + + private final String text; + + public static final String DEFAULT_APPROVAL_STATE_TEXT = DRAFTING.name(); +} diff --git a/src/main/java/playground/domain/document/entity/Category.java b/src/main/java/playground/domain/document/entity/Category.java new file mode 100644 index 0000000..cde8158 --- /dev/null +++ b/src/main/java/playground/domain/document/entity/Category.java @@ -0,0 +1,15 @@ +package playground.domain.document.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Category { + + OPERATING_EXPENSES("운영비"), + EDUCATION("교육"), + PRODUCT_PURCHASING("물품구매"); + + private final String text; +} diff --git a/src/main/java/playground/domain/document/entity/Document.java b/src/main/java/playground/domain/document/entity/Document.java new file mode 100644 index 0000000..af21452 --- /dev/null +++ b/src/main/java/playground/domain/document/entity/Document.java @@ -0,0 +1,35 @@ +package playground.domain.document.entity; + +import lombok.Builder; +import lombok.Getter; +import playground.domain.user.entity.User; + +import java.sql.Timestamp; +import java.util.Objects; + +@Builder +@Getter +public class Document { + + private Long id; + private String title; + private String contents; + private User drafter; + private Category category; + private ApprovalState approvalState; + private Timestamp createdAt; +// private DocumentApprovals approvals; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Document document = (Document) o; + return Objects.equals(id, document.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/playground/domain/document/entity/Documents.java b/src/main/java/playground/domain/document/entity/Documents.java new file mode 100644 index 0000000..94221bc --- /dev/null +++ b/src/main/java/playground/domain/document/entity/Documents.java @@ -0,0 +1,29 @@ +package playground.domain.document.entity; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +@Getter +public class Documents { + + private final List elements; + + public Documents(List elements) { + this.elements = Collections.unmodifiableList(sort(elements)); + } + + private List sort(List documents) { + List elements = new ArrayList<>(documents); + elements.sort(Comparator.comparing(Document::getCreatedAt).reversed()); + return elements; + } + + public Stream stream() { + return elements.stream(); + } +} diff --git a/src/main/java/playground/domain/user/dao/UserDao.java b/src/main/java/playground/domain/user/dao/UserDao.java new file mode 100644 index 0000000..1394e2a --- /dev/null +++ b/src/main/java/playground/domain/user/dao/UserDao.java @@ -0,0 +1,9 @@ +package playground.domain.user.dao; + +import playground.domain.document.entity.Document; +import playground.domain.user.entity.User; + +public interface UserDao { + + User findDrafterOf(Document document); +} diff --git a/src/main/java/playground/domain/user/dao/UserH2Dao.java b/src/main/java/playground/domain/user/dao/UserH2Dao.java new file mode 100644 index 0000000..fa9fb2f --- /dev/null +++ b/src/main/java/playground/domain/user/dao/UserH2Dao.java @@ -0,0 +1,22 @@ +package playground.domain.user.dao; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import playground.domain.document.entity.Document; +import playground.domain.user.entity.User; + +@Repository +@RequiredArgsConstructor +public class UserH2Dao implements UserDao { + + private final JdbcTemplate jdbcTemplate; + private final UserRowMapper userRowMapper; + + @Override + public User findDrafterOf(Document document) { + Long id = document.getDrafter().getId(); + String query = "SELECT * FROM USER WHERE ID=" + id; + return jdbcTemplate.queryForObject(query, userRowMapper); + } +} diff --git a/src/main/java/playground/domain/user/dao/UserRowMapper.java b/src/main/java/playground/domain/user/dao/UserRowMapper.java new file mode 100644 index 0000000..2d1abf2 --- /dev/null +++ b/src/main/java/playground/domain/user/dao/UserRowMapper.java @@ -0,0 +1,20 @@ +package playground.domain.user.dao; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import playground.domain.user.entity.User; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet rs, int rowNum) throws SQLException { + return User.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .build(); + } +} diff --git a/src/main/java/playground/domain/user/entity/User.java b/src/main/java/playground/domain/user/entity/User.java new file mode 100644 index 0000000..ef18bda --- /dev/null +++ b/src/main/java/playground/domain/user/entity/User.java @@ -0,0 +1,27 @@ +package playground.domain.user.entity; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Objects; + +@Getter +@Builder +public class User { + + private Long id; + private String name; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/playground/test.http b/src/main/java/playground/test.http new file mode 100644 index 0000000..8ff8c73 --- /dev/null +++ b/src/main/java/playground/test.http @@ -0,0 +1,19 @@ +GET http://localhost:8080/api/documents/1 + +### +POST http://localhost:8080/api/documents +Content-Type: application/json + +{ + "title": "교육비 결재 요청", + "category": "EDUCATION", + "contents": "사외교육비 결재 요청드립니다.", + "drafterId": 1, + "approverIds": [ + 1, + 2 + ] +} + +### +GET http://localhost:8080/api/documents/outbox?drafterId=1 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..40e7286 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,9 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:db + username: sa + password: + h2: + console: + enabled: true \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..aee03be --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,11 @@ +insert into user(name) +values ('유저1'); + +insert into user(name) +values ('유저2'); + +insert into document(title, contents, drafter_id, category, approval_state, created_at) +values ('1번 문서', '내용1', 1, 'OPERATING_EXPENSES', 'DRAFTING', '2021-10-23 23:59:59'); + +insert into document(title, contents, drafter_id, category, approval_state, created_at) +values ('2번 문서', '내용2', 1, 'OPERATING_EXPENSES', 'APPROVED', '2021-10-24 23:59:59'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..0191413 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,30 @@ +drop table if exists user; +drop table if exists document; + +create table user +( + id bigint not null auto_increment primary key, + name varchar(255) not null +); + + +create table document +( + id bigint not null auto_increment primary key, + title varchar(255) not null, + contents varchar(255), + drafter_id bigint not null, + category varchar(255) not null, + approval_state varchar(255) not null, + created_at timestamp not null, + foreign key (drafter_id) references user (id) +); + +create table document_approval +( + id bigint not null auto_increment primary key, + approver_id bigint not null, + approval_state varchar(255) not null, + approval_comment varchar(255), + foreign key (approver_id) references user (id) +); \ No newline at end of file diff --git a/src/test/java/playground/learning/DocumentTest.java b/src/test/java/learning/DocumentTest.java similarity index 99% rename from src/test/java/playground/learning/DocumentTest.java rename to src/test/java/learning/DocumentTest.java index 2ebfafb..09f373d 100644 --- a/src/test/java/playground/learning/DocumentTest.java +++ b/src/test/java/learning/DocumentTest.java @@ -1,4 +1,4 @@ -package playground.learning; +package learning; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/playground/domain/document/dao/h2/DocumentApprovalH2DaoTest.java b/src/test/java/playground/domain/document/dao/h2/DocumentApprovalH2DaoTest.java new file mode 100644 index 0000000..c17ae14 --- /dev/null +++ b/src/test/java/playground/domain/document/dao/h2/DocumentApprovalH2DaoTest.java @@ -0,0 +1,50 @@ +package playground.domain.document.dao.h2; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import playground.domain.document.dao.DocumentApprovalDao; +import playground.domain.document.dto.param.AddDocumentApprovalParam; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +@SpringBootTest +class DocumentApprovalH2DaoTest { + + @Autowired + private DocumentApprovalDao documentApprovalDao; + + @Test + @DisplayName("새 결재건을 추가한다.") + void add_document_approval_success() { + // given + AddDocumentApprovalParam addDocumentApprovalParam = AddDocumentApprovalParam.builder() + .documentId(1L) + .approversId(Arrays.asList(1L, 2L)) + .build(); + + // when, then + assertThatNoException() + .isThrownBy(() -> documentApprovalDao.addApprovals(addDocumentApprovalParam)); + } + + @Test + @DisplayName("존재하지 않는 유저를 결재자로 추가할 경우 예외가 발생한다.") + void add_document_approval_fail_no_such_approver() { + // given + AddDocumentApprovalParam addDocumentApprovalParam = AddDocumentApprovalParam.builder() + .documentId(1L) + .approversId(Collections.singletonList(999999L)) + .build(); + + // when, then + assertThatExceptionOfType(DataIntegrityViolationException.class) + .isThrownBy(() -> documentApprovalDao.addApprovals(addDocumentApprovalParam)); + } +} \ No newline at end of file diff --git a/src/test/java/playground/domain/document/dao/h2/DocumentH2DaoTest.java b/src/test/java/playground/domain/document/dao/h2/DocumentH2DaoTest.java new file mode 100644 index 0000000..95ea60a --- /dev/null +++ b/src/test/java/playground/domain/document/dao/h2/DocumentH2DaoTest.java @@ -0,0 +1,94 @@ +package playground.domain.document.dao.h2; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.EmptyResultDataAccessException; +import playground.domain.document.dto.param.AddDocumentParam; +import playground.domain.document.entity.Document; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +@SpringBootTest +class DocumentH2DaoTest { + + @Autowired + private DocumentH2Dao documentH2Dao; + + @Test + @DisplayName("식별자로 문서를 찾아온다.") + void find_by_id_success() { + // given + Document expected = Document.builder() + .id(1L) + .build(); + + // when + Document result = documentH2Dao.findById(expected.getId()); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("존재하지 않는 문서를 조회하면 예외가 발생한다.") + void find_by_id_fail_no_such_document() { + // given + Long id = 999999999L; + + // when, then + assertThatExceptionOfType(EmptyResultDataAccessException.class) + .isThrownBy(() -> documentH2Dao.findById(id)); + } + + @Test + @DisplayName("새 문서를 저장한다.") + void add_document_success() { + // given + AddDocumentParam addDocumentParam = AddDocumentParam.builder() + .title("title") + .contents("content") + .drafterId(1L) + .categoryText("category") + .approvalStateText("DRAFTING") + .build(); + + // when + Long documentId = documentH2Dao.addDocument(addDocumentParam); + + // then + assertThat(documentId).isNotNull(); + } + + @Test + @DisplayName("작성자 ID로 문서를 찾아온다.") + void find_by_drafter_success() { + // given + Long drafterId = 1L; + + // when + List documents = documentH2Dao.findByDrafter(drafterId); + + // then + assertThat(documents) + .extracting("drafter.id") + .containsOnly(drafterId); + } + + @Test + @DisplayName("작성자가 작성한 문서가 없으면 빈 리스트를 반환한다.") + void find_by_drafter_success_empty() { + // given + Long drafterId = 2L; + + // when + List documents = documentH2Dao.findByDrafter(drafterId); + + // then + assertThat(documents).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/playground/domain/document/entity/DocumentTest.java b/src/test/java/playground/domain/document/entity/DocumentTest.java new file mode 100644 index 0000000..4ba325f --- /dev/null +++ b/src/test/java/playground/domain/document/entity/DocumentTest.java @@ -0,0 +1,30 @@ +package playground.domain.document.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DocumentTest { + + @Test + @DisplayName("식별자로 동등성을 비교한다.") + void equality_with_id() { + // given + Long id = 1L; + Document document_one = Document.builder() + .id(id) + .title("1번 문서") + .build(); + Document document_two = Document.builder() + .id(id) + .title("제목이 달라요") + .build(); + + // when + boolean result = document_one.equals(document_two); + + // then + assertThat(result).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/playground/domain/user/dao/UserH2DaoTest.java b/src/test/java/playground/domain/user/dao/UserH2DaoTest.java new file mode 100644 index 0000000..0cfb8f1 --- /dev/null +++ b/src/test/java/playground/domain/user/dao/UserH2DaoTest.java @@ -0,0 +1,36 @@ +package playground.domain.user.dao; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import playground.domain.document.entity.Document; +import playground.domain.user.entity.User; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class UserH2DaoTest { + + @Autowired + private UserH2Dao userH2Dao; + + @Test + @DisplayName("문서가 주어지면 작성자 유저를 찾아온다.") + void find_drafter_of_success() { + // given + User expected = User.builder() + .id(1L) + .build(); + Document document = Document.builder() + .id(1L) + .drafter(expected) + .build(); + + // when + User result = userH2Dao.findDrafterOf(document); + + // then + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/playground/domain/user/entity/UserTest.java b/src/test/java/playground/domain/user/entity/UserTest.java new file mode 100644 index 0000000..120e100 --- /dev/null +++ b/src/test/java/playground/domain/user/entity/UserTest.java @@ -0,0 +1,30 @@ +package playground.domain.user.entity; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserTest { + + @Test + @DisplayName("식별자로 동등성을 비교한다.") + void equality_with_id() { + // given + Long id = 1L; + User user_one = User.builder() + .id(id) + .name("이름1") + .build(); + User user_two = User.builder() + .id(id) + .name("이름2") + .build(); + + // when + boolean result = user_one.equals(user_two); + + // then + assertThat(result).isTrue(); + } +}