feat: 담당자를 별도 contact 테이블로 분리하고 배정 API로 전환
기관에 박혀 있던 담당자 3종(신청기관/문정원/변호사) 컬럼을 contact 테이블로 옮기고,
기관은 담당자 id만 배정으로 참조하게 했다. 신청기관 담당자 배정 여부가 채널 생성
가능(READY) 판정 기준이 되도록 상태 로직도 함께 바꿨다.
- V4 마이그레이션: contact 테이블 생성 + 기존 임베디드 컬럼 백필 후 제거
- kr.itn.itnhub.contact 패키지: 담당자 CRUD API (GET/POST/PUT/DELETE /api/contacts)
- PUT /api/orgs/{id}/assignments가 기존 /contact 엔드포인트를 대체 (구분 불일치 400)
- SeedService: 시트의 담당자명은 신청기관 담당자가 아직 없을 때만 새로 만들어 연결
- ChannelProvisionService 게이트를 hasApplicantAssigned()로 교체
@053356d5cd1a5fbc2eebc8a80aaaf2691ec0bb5d
--- src/main/java/kr/itn/itnhub/config/GlobalExceptionHandler.java
+++ src/main/java/kr/itn/itnhub/config/GlobalExceptionHandler.java
... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 |
package kr.itn.itnhub.config; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.contact.ContactNotFoundException; |
|
| 4 |
+import kr.itn.itnhub.contact.InvalidContactException; |
|
| 3 | 5 |
import kr.itn.itnhub.feed.ChannelNotReadyException; |
| 4 | 6 |
import kr.itn.itnhub.feed.InvalidMessageException; |
| 5 | 7 |
import kr.itn.itnhub.org.OrgNotFoundException; |
... | ... | @@ -40,6 +42,13 @@ |
| 40 | 42 |
* <p>{@link InvalidMessageException}은 채팅 탭에서 메시지 본문과 첨부파일이 둘 다
|
| 41 | 43 |
* 없는 채로 전송을 시도할 때 던진다 - 흔한 사용자 실수이므로 400을 쓴다.</p> |
| 42 | 44 |
* |
| 45 |
+ * <p>{@link ContactNotFoundException}은 존재하지 않는 담당자 id로 조회/수정/삭제를
|
|
| 46 |
+ * 시도했을 때 던진다 - URL의 id가 단순히 틀린 것뿐이므로 404가 맞다.</p> |
|
| 47 |
+ * |
|
| 48 |
+ * <p>{@link InvalidContactException}은 담당자 구분(category)이 APPLICANT/MJ/LAWYER
|
|
| 49 |
+ * 중 하나가 아니거나, 기관에 배정하려는 담당자의 구분이 그 역할과 맞지 않을 때 |
|
| 50 |
+ * 던진다 - 흔한 사용자 실수이므로 400을 쓴다.</p> |
|
| 51 |
+ * |
|
| 43 | 52 |
* <p><b>여기에 {@code Exception.class} catch-all을 추가하지 말 것.</b> 예상하지 못한
|
| 44 | 53 |
* 예외까지 4xx로 감싸버리면 진짜 버그가 조용히 묻힌다. 예상 밖 예외는 기본 500 처리 |
| 45 | 54 |
* 그대로 두는 것이 의도다.</p> |
... | ... | @@ -50,12 +59,10 @@ |
| 50 | 59 |
/** 검증 오류 메시지에 쓸 필드명 → 한글 라벨. 목록에 없는 필드는 원본 이름 그대로 보여준다. */ |
| 51 | 60 |
private static final Map<String, String> FIELD_LABELS = Map.of( |
| 52 | 61 |
"deptName", "부서명", |
| 53 |
- "managerName", "담당자명", |
|
| 54 |
- "managerTitle", "직급/직함", |
|
| 55 |
- "managerPhone", "연락처", |
|
| 56 |
- "managerEmail", "이메일", |
|
| 57 | 62 |
"contactName", "담당자", |
| 58 |
- "body", "메모 내용"); |
|
| 63 |
+ "body", "메모 내용", |
|
| 64 |
+ "category", "구분", |
|
| 65 |
+ "name", "성명"); |
|
| 59 | 66 |
|
| 60 | 67 |
@ExceptionHandler(OrgNotFoundException.class) |
| 61 | 68 |
public ResponseEntity<ApiError> handleNotFound(OrgNotFoundException e) {
|
... | ... | @@ -85,4 +92,14 @@ |
| 85 | 92 |
public ResponseEntity<ApiError> handleInvalidMessage(InvalidMessageException e) {
|
| 86 | 93 |
return ResponseEntity.badRequest().body(new ApiError(e.getMessage())); |
| 87 | 94 |
} |
| 95 |
+ |
|
| 96 |
+ @ExceptionHandler(ContactNotFoundException.class) |
|
| 97 |
+ public ResponseEntity<ApiError> handleContactNotFound(ContactNotFoundException e) {
|
|
| 98 |
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ApiError(e.getMessage())); |
|
| 99 |
+ } |
|
| 100 |
+ |
|
| 101 |
+ @ExceptionHandler(InvalidContactException.class) |
|
| 102 |
+ public ResponseEntity<ApiError> handleInvalidContact(InvalidContactException e) {
|
|
| 103 |
+ return ResponseEntity.badRequest().body(new ApiError(e.getMessage())); |
|
| 104 |
+ } |
|
| 88 | 105 |
} |
+++ src/main/java/kr/itn/itnhub/contact/Contact.java
... | ... | @@ -0,0 +1,42 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 담당자 1명. 신청기관/문정원/변호사 세 구분({@link #category})을 한 테이블에 담는다 - | |
| 5 | + * 세 구분이 필드 구성(성명·소속·부서·연락처 등)을 그대로 공유하기 때문이다. | |
| 6 | + * 신청기관 담당자는 {@code affiliation}에 소속 기관명이 들어간다. | |
| 7 | + */ | |
| 8 | +public class Contact { | |
| 9 | + | |
| 10 | + private Long id; | |
| 11 | + private String category; | |
| 12 | + private String name; | |
| 13 | + private String affiliation; | |
| 14 | + private String deptName; | |
| 15 | + private String title; | |
| 16 | + private String phone; | |
| 17 | + private String email; | |
| 18 | + | |
| 19 | + public Long getId() { return id; } | |
| 20 | + public void setId(Long id) { this.id = id; } | |
| 21 | + | |
| 22 | + public String getCategory() { return category; } | |
| 23 | + public void setCategory(String category) { this.category = category; } | |
| 24 | + | |
| 25 | + public String getName() { return name; } | |
| 26 | + public void setName(String name) { this.name = name; } | |
| 27 | + | |
| 28 | + public String getAffiliation() { return affiliation; } | |
| 29 | + public void setAffiliation(String affiliation) { this.affiliation = affiliation; } | |
| 30 | + | |
| 31 | + public String getDeptName() { return deptName; } | |
| 32 | + public void setDeptName(String deptName) { this.deptName = deptName; } | |
| 33 | + | |
| 34 | + public String getTitle() { return title; } | |
| 35 | + public void setTitle(String title) { this.title = title; } | |
| 36 | + | |
| 37 | + public String getPhone() { return phone; } | |
| 38 | + public void setPhone(String phone) { this.phone = phone; } | |
| 39 | + | |
| 40 | + public String getEmail() { return email; } | |
| 41 | + public void setEmail(String email) { this.email = email; } | |
| 42 | +} |
+++ src/main/java/kr/itn/itnhub/contact/ContactController.java
... | ... | @@ -0,0 +1,49 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +import jakarta.validation.Valid; | |
| 4 | +import org.springframework.http.HttpStatus; | |
| 5 | +import org.springframework.web.bind.annotation.DeleteMapping; | |
| 6 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 7 | +import org.springframework.web.bind.annotation.PathVariable; | |
| 8 | +import org.springframework.web.bind.annotation.PostMapping; | |
| 9 | +import org.springframework.web.bind.annotation.PutMapping; | |
| 10 | +import org.springframework.web.bind.annotation.RequestBody; | |
| 11 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 12 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 13 | +import org.springframework.web.bind.annotation.ResponseStatus; | |
| 14 | +import org.springframework.web.bind.annotation.RestController; | |
| 15 | + | |
| 16 | +import java.util.List; | |
| 17 | + | |
| 18 | +/** 담당자관리 화면 전용 CRUD. 담당자 정보가 입력되는 유일한 경로다. */ | |
| 19 | +@RestController | |
| 20 | +@RequestMapping("/api/contacts") | |
| 21 | +public class ContactController { | |
| 22 | + | |
| 23 | + private final ContactService service; | |
| 24 | + | |
| 25 | + public ContactController(ContactService service) { | |
| 26 | + this.service = service; | |
| 27 | + } | |
| 28 | + | |
| 29 | + @GetMapping | |
| 30 | + public List<Contact> list(@RequestParam(required = false) String category) { | |
| 31 | + return service.findAll(category); | |
| 32 | + } | |
| 33 | + | |
| 34 | + @PostMapping | |
| 35 | + public Contact create(@Valid @RequestBody ContactRequest request) { | |
| 36 | + return service.create(request); | |
| 37 | + } | |
| 38 | + | |
| 39 | + @PutMapping("/{id}") | |
| 40 | + public Contact update(@PathVariable Long id, @Valid @RequestBody ContactRequest request) { | |
| 41 | + return service.update(id, request); | |
| 42 | + } | |
| 43 | + | |
| 44 | + @ResponseStatus(HttpStatus.NO_CONTENT) | |
| 45 | + @DeleteMapping("/{id}") | |
| 46 | + public void delete(@PathVariable Long id) { | |
| 47 | + service.delete(id); | |
| 48 | + } | |
| 49 | +} |
+++ src/main/java/kr/itn/itnhub/contact/ContactMapper.java
... | ... | @@ -0,0 +1,22 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +import org.apache.ibatis.annotations.Mapper; | |
| 4 | +import org.apache.ibatis.annotations.Param; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 8 | +@Mapper | |
| 9 | +public interface ContactMapper { | |
| 10 | + | |
| 11 | + /** category가 null이면 전체를 구분→성명 오름차순으로 돌려준다. */ | |
| 12 | + List<Contact> findAll(@Param("category") String category); | |
| 13 | + | |
| 14 | + Contact findById(@Param("id") Long id); | |
| 15 | + | |
| 16 | + int insert(Contact contact); | |
| 17 | + | |
| 18 | + int update(Contact contact); | |
| 19 | + | |
| 20 | + /** FK는 on delete set null이므로, 이 담당자를 가리키던 기관의 배정은 자동으로 해제된다. */ | |
| 21 | + int delete(@Param("id") Long id); | |
| 22 | +} |
+++ src/main/java/kr/itn/itnhub/contact/ContactNotFoundException.java
... | ... | @@ -0,0 +1,11 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 존재하지 않는 담당자 id로 조회/수정/삭제를 시도했을 때 던진다. | |
| 5 | + * {@link kr.itn.itnhub.config.GlobalExceptionHandler}가 404로 변환한다. | |
| 6 | + */ | |
| 7 | +public class ContactNotFoundException extends RuntimeException { | |
| 8 | + public ContactNotFoundException(String message) { | |
| 9 | + super(message); | |
| 10 | + } | |
| 11 | +} |
+++ src/main/java/kr/itn/itnhub/contact/ContactRequest.java
... | ... | @@ -0,0 +1,18 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.NotBlank; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * 담당자 추가/수정 요청. category는 여기서는 형식(공백 아님)만 검증하고, | |
| 7 | + * APPLICANT/MJ/LAWYER 중 하나인지는 {@link ContactService}가 검증해 | |
| 8 | + * "구분 값이 올바르지 않습니다" 같은 도메인 메시지를 붙일 수 있게 한다. | |
| 9 | + */ | |
| 10 | +public record ContactRequest( | |
| 11 | + @NotBlank String category, | |
| 12 | + @NotBlank String name, | |
| 13 | + String affiliation, | |
| 14 | + String deptName, | |
| 15 | + String title, | |
| 16 | + String phone, | |
| 17 | + String email) { | |
| 18 | +} |
+++ src/main/java/kr/itn/itnhub/contact/ContactService.java
... | ... | @@ -0,0 +1,75 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +import org.springframework.stereotype.Service; | |
| 4 | +import org.springframework.transaction.annotation.Transactional; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | +import java.util.Set; | |
| 8 | + | |
| 9 | +@Service | |
| 10 | +public class ContactService { | |
| 11 | + | |
| 12 | + private static final Set<String> CATEGORIES = Set.of("APPLICANT", "MJ", "LAWYER"); | |
| 13 | + | |
| 14 | + private final ContactMapper mapper; | |
| 15 | + | |
| 16 | + public ContactService(ContactMapper mapper) { | |
| 17 | + this.mapper = mapper; | |
| 18 | + } | |
| 19 | + | |
| 20 | + public List<Contact> findAll(String category) { | |
| 21 | + if (category != null && !CATEGORIES.contains(category)) { | |
| 22 | + throw new InvalidContactException("구분 값이 올바르지 않습니다: " + category); | |
| 23 | + } | |
| 24 | + return mapper.findAll(category); | |
| 25 | + } | |
| 26 | + | |
| 27 | + @Transactional | |
| 28 | + public Contact create(ContactRequest request) { | |
| 29 | + validateCategory(request.category()); | |
| 30 | + | |
| 31 | + Contact contact = new Contact(); | |
| 32 | + applyRequest(contact, request); | |
| 33 | + mapper.insert(contact); | |
| 34 | + return mapper.findById(contact.getId()); | |
| 35 | + } | |
| 36 | + | |
| 37 | + @Transactional | |
| 38 | + public Contact update(Long id, ContactRequest request) { | |
| 39 | + validateCategory(request.category()); | |
| 40 | + | |
| 41 | + Contact existing = mapper.findById(id); | |
| 42 | + if (existing == null) { | |
| 43 | + throw new ContactNotFoundException("담당자를 찾을 수 없습니다: " + id); | |
| 44 | + } | |
| 45 | + applyRequest(existing, request); | |
| 46 | + mapper.update(existing); | |
| 47 | + return mapper.findById(id); | |
| 48 | + } | |
| 49 | + | |
| 50 | + /** FK는 on delete set null이므로, 이 담당자를 배정으로 쓰던 기관들은 자동으로 미지정이 된다. */ | |
| 51 | + @Transactional | |
| 52 | + public void delete(Long id) { | |
| 53 | + Contact existing = mapper.findById(id); | |
| 54 | + if (existing == null) { | |
| 55 | + throw new ContactNotFoundException("담당자를 찾을 수 없습니다: " + id); | |
| 56 | + } | |
| 57 | + mapper.delete(id); | |
| 58 | + } | |
| 59 | + | |
| 60 | + private void applyRequest(Contact contact, ContactRequest request) { | |
| 61 | + contact.setCategory(request.category()); | |
| 62 | + contact.setName(request.name()); | |
| 63 | + contact.setAffiliation(request.affiliation()); | |
| 64 | + contact.setDeptName(request.deptName()); | |
| 65 | + contact.setTitle(request.title()); | |
| 66 | + contact.setPhone(request.phone()); | |
| 67 | + contact.setEmail(request.email()); | |
| 68 | + } | |
| 69 | + | |
| 70 | + private void validateCategory(String category) { | |
| 71 | + if (!CATEGORIES.contains(category)) { | |
| 72 | + throw new InvalidContactException("구분 값이 올바르지 않습니다: " + category); | |
| 73 | + } | |
| 74 | + } | |
| 75 | +} |
+++ src/main/java/kr/itn/itnhub/contact/InvalidContactException.java
... | ... | @@ -0,0 +1,12 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 담당자 구분(category)이 APPLICANT/MJ/LAWYER 중 하나가 아니거나, 기관 담당자 배정 시 | |
| 5 | + * 지정한 contactId가 그 역할에 맞는 구분의 담당자가 아닐 때 던진다. 흔한 사용자 실수이므로 | |
| 6 | + * {@link kr.itn.itnhub.config.GlobalExceptionHandler}가 400으로 변환한다. | |
| 7 | + */ | |
| 8 | +public class InvalidContactException extends RuntimeException { | |
| 9 | + public InvalidContactException(String message) { | |
| 10 | + super(message); | |
| 11 | + } | |
| 12 | +} |
+++ src/main/java/kr/itn/itnhub/org/AssignmentRequest.java
... | ... | @@ -0,0 +1,13 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 채널관리 화면에서 담당자 3종(신청기관/문정원/변호사)을 배정한다. | |
| 5 | + * null은 "그 역할의 배정을 해제한다"는 뜻이며, 매 호출마다 세 배정값 + 배정일을 모두 보낸다 | |
| 6 | + * (부분 갱신이 아니다 - PUT 하나가 항상 현재 상태 전체를 반영한다). | |
| 7 | + */ | |
| 8 | +public record AssignmentRequest( | |
| 9 | + Long applicantContactId, | |
| 10 | + Long mjContactId, | |
| 11 | + Long lawyerContactId, | |
| 12 | + String lawyerAssignedDate) { | |
| 13 | +} |
--- src/main/java/kr/itn/itnhub/org/ContactRequest.java
... | ... | @@ -1,21 +0,0 @@ |
| 1 | -package kr.itn.itnhub.org; | |
| 2 | - | |
| 3 | -import jakarta.validation.constraints.Email; | |
| 4 | -import jakarta.validation.constraints.NotBlank; | |
| 5 | -import jakarta.validation.constraints.Size; | |
| 6 | - | |
| 7 | -public record ContactRequest( | |
| 8 | - @NotBlank String deptName, | |
| 9 | - @NotBlank String managerName, | |
| 10 | - String managerTitle, | |
| 11 | - @NotBlank String managerPhone, | |
| 12 | - @NotBlank @Email String managerEmail, | |
| 13 | - @Size(max = 200) String mjDeptName, | |
| 14 | - @Size(max = 100) String mjManagerName, | |
| 15 | - @Size(max = 50) String mjManagerPhone, | |
| 16 | - @Size(max = 200) String mjManagerEmail, | |
| 17 | - @Size(max = 100) String lawyerName, | |
| 18 | - @Size(max = 50) String lawyerPhone, | |
| 19 | - @Size(max = 200) String lawyerEmail, | |
| 20 | - @Size(max = 20) String lawyerAssignedDate) { | |
| 21 | -} |
--- src/main/java/kr/itn/itnhub/org/OrgResponse.java
+++ src/main/java/kr/itn/itnhub/org/OrgResponse.java
... | ... | @@ -1,22 +1,15 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.contact.Contact; |
|
| 4 |
+ |
|
| 3 | 5 |
public record OrgResponse( |
| 4 | 6 |
Long id, |
| 5 | 7 |
String orgNo, |
| 6 | 8 |
String orgName, |
| 7 | 9 |
OrgStatus status, |
| 8 |
- String deptName, |
|
| 9 |
- String managerName, |
|
| 10 |
- String managerTitle, |
|
| 11 |
- String managerPhone, |
|
| 12 |
- String managerEmail, |
|
| 13 |
- String mjDeptName, |
|
| 14 |
- String mjManagerName, |
|
| 15 |
- String mjManagerPhone, |
|
| 16 |
- String mjManagerEmail, |
|
| 17 |
- String lawyerName, |
|
| 18 |
- String lawyerPhone, |
|
| 19 |
- String lawyerEmail, |
|
| 10 |
+ Contact applicant, |
|
| 11 |
+ Contact mj, |
|
| 12 |
+ Contact lawyer, |
|
| 20 | 13 |
String lawyerAssignedDate, |
| 21 | 14 |
String channelIdMj, |
| 22 | 15 |
String channelIdLaw) {
|
... | ... | @@ -27,18 +20,9 @@ |
| 27 | 20 |
org.getOrgNo(), |
| 28 | 21 |
org.getOrgName(), |
| 29 | 22 |
org.getStatus(), |
| 30 |
- org.getDeptName(), |
|
| 31 |
- org.getManagerName(), |
|
| 32 |
- org.getManagerTitle(), |
|
| 33 |
- org.getManagerPhone(), |
|
| 34 |
- org.getManagerEmail(), |
|
| 35 |
- org.getMjDeptName(), |
|
| 36 |
- org.getMjManagerName(), |
|
| 37 |
- org.getMjManagerPhone(), |
|
| 38 |
- org.getMjManagerEmail(), |
|
| 39 |
- org.getLawyerName(), |
|
| 40 |
- org.getLawyerPhone(), |
|
| 41 |
- org.getLawyerEmail(), |
|
| 23 |
+ org.getApplicant(), |
|
| 24 |
+ org.getMj(), |
|
| 25 |
+ org.getLawyer(), |
|
| 42 | 26 |
org.getLawyerAssignedDate(), |
| 43 | 27 |
org.getChannelIdMj(), |
| 44 | 28 |
org.getChannelIdLaw()); |
--- src/main/java/kr/itn/itnhub/org/Organization.java
+++ src/main/java/kr/itn/itnhub/org/Organization.java
... | ... | @@ -1,50 +1,45 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.contact.Contact; |
|
| 4 |
+ |
|
| 3 | 5 |
public class Organization {
|
| 4 | 6 |
|
| 5 | 7 |
private Long id; |
| 6 | 8 |
private String orgNo; |
| 7 | 9 |
private String orgName; |
| 8 | 10 |
private String channelSlug; |
| 9 |
- private String deptName; |
|
| 10 |
- private String managerName; |
|
| 11 |
- private String managerTitle; |
|
| 12 |
- private String managerPhone; |
|
| 13 |
- private String managerEmail; |
|
| 14 |
- private String mjDeptName; |
|
| 15 |
- private String mjManagerName; |
|
| 16 |
- private String mjManagerPhone; |
|
| 17 |
- private String mjManagerEmail; |
|
| 18 |
- private String lawyerName; |
|
| 19 |
- private String lawyerPhone; |
|
| 20 |
- private String lawyerEmail; |
|
| 11 |
+ private Long applicantContactId; |
|
| 12 |
+ private Long mjContactId; |
|
| 13 |
+ private Long lawyerContactId; |
|
| 21 | 14 |
private String lawyerAssignedDate; |
| 22 | 15 |
private String channelIdMj; |
| 23 | 16 |
private String channelIdLaw; |
| 17 |
+ |
|
| 18 |
+ /** findAll/findById가 contact 테이블과 조인해 채워준다. 시드/upsert 경로에서는 비어 있다. */ |
|
| 19 |
+ private Contact applicant; |
|
| 20 |
+ private Contact mj; |
|
| 21 |
+ private Contact lawyer; |
|
| 24 | 22 |
|
| 25 | 23 |
/** |
| 26 | 24 |
* 상태는 저장하지 않고 항상 파생한다. |
| 27 | 25 |
* 저장하면 채널ID와 상태가 어긋나는 순간이 생기고, 그 어긋남을 고칠 방법이 없다. |
| 28 | 26 |
*/ |
| 29 | 27 |
public OrgStatus getStatus() {
|
| 30 |
- boolean mj = filled(channelIdMj); |
|
| 31 |
- boolean law = filled(channelIdLaw); |
|
| 28 |
+ boolean mjActive = filled(channelIdMj); |
|
| 29 |
+ boolean lawActive = filled(channelIdLaw); |
|
| 32 | 30 |
|
| 33 |
- if (mj && law) {
|
|
| 31 |
+ if (mjActive && lawActive) {
|
|
| 34 | 32 |
return OrgStatus.ACTIVE; |
| 35 | 33 |
} |
| 36 |
- if (mj || law) {
|
|
| 34 |
+ if (mjActive || lawActive) {
|
|
| 37 | 35 |
return OrgStatus.PARTIAL; |
| 38 | 36 |
} |
| 39 |
- return hasRequiredContact() ? OrgStatus.READY : OrgStatus.INFO_PENDING; |
|
| 37 |
+ return hasApplicantAssigned() ? OrgStatus.READY : OrgStatus.INFO_PENDING; |
|
| 40 | 38 |
} |
| 41 | 39 |
|
| 42 |
- /** 직급/직함은 비어 있는 기관이 실제로 있으므로 필수에서 뺀다. */ |
|
| 43 |
- public boolean hasRequiredContact() {
|
|
| 44 |
- return filled(deptName) |
|
| 45 |
- && filled(managerName) |
|
| 46 |
- && filled(managerPhone) |
|
| 47 |
- && filled(managerEmail); |
|
| 40 |
+ /** 신청기관 담당자가 배정되어야 채널을 만들 수 있다(담당자관리에서 등록·배정된 담당자). */ |
|
| 41 |
+ public boolean hasApplicantAssigned() {
|
|
| 42 |
+ return applicantContactId != null; |
|
| 48 | 43 |
} |
| 49 | 44 |
|
| 50 | 45 |
private static boolean filled(String value) {
|
... | ... | @@ -63,41 +58,14 @@ |
| 63 | 58 |
public String getChannelSlug() { return channelSlug; }
|
| 64 | 59 |
public void setChannelSlug(String channelSlug) { this.channelSlug = channelSlug; }
|
| 65 | 60 |
|
| 66 |
- public String getDeptName() { return deptName; }
|
|
| 67 |
- public void setDeptName(String deptName) { this.deptName = deptName; }
|
|
| 61 |
+ public Long getApplicantContactId() { return applicantContactId; }
|
|
| 62 |
+ public void setApplicantContactId(Long applicantContactId) { this.applicantContactId = applicantContactId; }
|
|
| 68 | 63 |
|
| 69 |
- public String getManagerName() { return managerName; }
|
|
| 70 |
- public void setManagerName(String managerName) { this.managerName = managerName; }
|
|
| 64 |
+ public Long getMjContactId() { return mjContactId; }
|
|
| 65 |
+ public void setMjContactId(Long mjContactId) { this.mjContactId = mjContactId; }
|
|
| 71 | 66 |
|
| 72 |
- public String getManagerTitle() { return managerTitle; }
|
|
| 73 |
- public void setManagerTitle(String managerTitle) { this.managerTitle = managerTitle; }
|
|
| 74 |
- |
|
| 75 |
- public String getManagerPhone() { return managerPhone; }
|
|
| 76 |
- public void setManagerPhone(String managerPhone) { this.managerPhone = managerPhone; }
|
|
| 77 |
- |
|
| 78 |
- public String getManagerEmail() { return managerEmail; }
|
|
| 79 |
- public void setManagerEmail(String managerEmail) { this.managerEmail = managerEmail; }
|
|
| 80 |
- |
|
| 81 |
- public String getMjDeptName() { return mjDeptName; }
|
|
| 82 |
- public void setMjDeptName(String mjDeptName) { this.mjDeptName = mjDeptName; }
|
|
| 83 |
- |
|
| 84 |
- public String getMjManagerName() { return mjManagerName; }
|
|
| 85 |
- public void setMjManagerName(String mjManagerName) { this.mjManagerName = mjManagerName; }
|
|
| 86 |
- |
|
| 87 |
- public String getMjManagerPhone() { return mjManagerPhone; }
|
|
| 88 |
- public void setMjManagerPhone(String mjManagerPhone) { this.mjManagerPhone = mjManagerPhone; }
|
|
| 89 |
- |
|
| 90 |
- public String getMjManagerEmail() { return mjManagerEmail; }
|
|
| 91 |
- public void setMjManagerEmail(String mjManagerEmail) { this.mjManagerEmail = mjManagerEmail; }
|
|
| 92 |
- |
|
| 93 |
- public String getLawyerName() { return lawyerName; }
|
|
| 94 |
- public void setLawyerName(String lawyerName) { this.lawyerName = lawyerName; }
|
|
| 95 |
- |
|
| 96 |
- public String getLawyerPhone() { return lawyerPhone; }
|
|
| 97 |
- public void setLawyerPhone(String lawyerPhone) { this.lawyerPhone = lawyerPhone; }
|
|
| 98 |
- |
|
| 99 |
- public String getLawyerEmail() { return lawyerEmail; }
|
|
| 100 |
- public void setLawyerEmail(String lawyerEmail) { this.lawyerEmail = lawyerEmail; }
|
|
| 67 |
+ public Long getLawyerContactId() { return lawyerContactId; }
|
|
| 68 |
+ public void setLawyerContactId(Long lawyerContactId) { this.lawyerContactId = lawyerContactId; }
|
|
| 101 | 69 |
|
| 102 | 70 |
public String getLawyerAssignedDate() { return lawyerAssignedDate; }
|
| 103 | 71 |
public void setLawyerAssignedDate(String lawyerAssignedDate) { this.lawyerAssignedDate = lawyerAssignedDate; }
|
... | ... | @@ -107,4 +75,13 @@ |
| 107 | 75 |
|
| 108 | 76 |
public String getChannelIdLaw() { return channelIdLaw; }
|
| 109 | 77 |
public void setChannelIdLaw(String channelIdLaw) { this.channelIdLaw = channelIdLaw; }
|
| 78 |
+ |
|
| 79 |
+ public Contact getApplicant() { return applicant; }
|
|
| 80 |
+ public void setApplicant(Contact applicant) { this.applicant = applicant; }
|
|
| 81 |
+ |
|
| 82 |
+ public Contact getMj() { return mj; }
|
|
| 83 |
+ public void setMj(Contact mj) { this.mj = mj; }
|
|
| 84 |
+ |
|
| 85 |
+ public Contact getLawyer() { return lawyer; }
|
|
| 86 |
+ public void setLawyer(Contact lawyer) { this.lawyer = lawyer; }
|
|
| 110 | 87 |
} |
--- src/main/java/kr/itn/itnhub/org/OrganizationController.java
+++ src/main/java/kr/itn/itnhub/org/OrganizationController.java
... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 |
-import jakarta.validation.Valid; |
|
| 4 | 3 |
import kr.itn.itnhub.provision.ChannelProvisionService; |
| 5 | 4 |
import kr.itn.itnhub.provision.ProvisionResult; |
| 6 | 5 |
import org.springframework.web.bind.annotation.GetMapping; |
... | ... | @@ -31,10 +30,10 @@ |
| 31 | 30 |
return orgService.findAll(); |
| 32 | 31 |
} |
| 33 | 32 |
|
| 34 |
- @PutMapping("/{id}/contact")
|
|
| 35 |
- public OrgResponse updateContact(@PathVariable Long id, |
|
| 36 |
- @Valid @RequestBody ContactRequest request) {
|
|
| 37 |
- return orgService.updateContact(id, request); |
|
| 33 |
+ @PutMapping("/{id}/assignments")
|
|
| 34 |
+ public OrgResponse updateAssignments(@PathVariable Long id, |
|
| 35 |
+ @RequestBody AssignmentRequest request) {
|
|
| 36 |
+ return orgService.updateAssignments(id, request); |
|
| 38 | 37 |
} |
| 39 | 38 |
|
| 40 | 39 |
/** |
--- src/main/java/kr/itn/itnhub/org/OrganizationMapper.java
+++ src/main/java/kr/itn/itnhub/org/OrganizationMapper.java
... | ... | @@ -12,10 +12,17 @@ |
| 12 | 12 |
|
| 13 | 13 |
Organization findById(@Param("id") Long id);
|
| 14 | 14 |
|
| 15 |
+ /** 연번+기관명 유니크 키로 단건 조회. 시드가 upsert 직후 방금 반영된 행을 다시 읽을 때 쓴다. */ |
|
| 16 |
+ Organization findByOrgNoAndOrgName(@Param("orgNo") String orgNo, @Param("orgName") String orgName);
|
|
| 17 |
+ |
|
| 15 | 18 |
/** 시드 전용. 비어 있는 칸만 채우고 채널ID·기입된 값은 그대로 둔다. */ |
| 16 | 19 |
int upsertBySeed(Organization org); |
| 17 | 20 |
|
| 18 |
- int updateContact(Organization org); |
|
| 21 |
+ int updateAssignments(@Param("id") Long id,
|
|
| 22 |
+ @Param("applicantContactId") Long applicantContactId,
|
|
| 23 |
+ @Param("mjContactId") Long mjContactId,
|
|
| 24 |
+ @Param("lawyerContactId") Long lawyerContactId,
|
|
| 25 |
+ @Param("lawyerAssignedDate") String lawyerAssignedDate);
|
|
| 19 | 26 |
|
| 20 | 27 |
int updateChannelIdMj(@Param("id") Long id,
|
| 21 | 28 |
@Param("channelId") String channelId);
|
--- src/main/java/kr/itn/itnhub/org/OrganizationService.java
+++ src/main/java/kr/itn/itnhub/org/OrganizationService.java
... | ... | @@ -1,5 +1,8 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.contact.Contact; |
|
| 4 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 5 |
+import kr.itn.itnhub.contact.InvalidContactException; |
|
| 3 | 6 |
import org.springframework.stereotype.Service; |
| 4 | 7 |
import org.springframework.transaction.annotation.Transactional; |
| 5 | 8 |
|
... | ... | @@ -9,36 +12,49 @@ |
| 9 | 12 |
public class OrganizationService {
|
| 10 | 13 |
|
| 11 | 14 |
private final OrganizationMapper mapper; |
| 15 |
+ private final ContactMapper contactMapper; |
|
| 12 | 16 |
|
| 13 |
- public OrganizationService(OrganizationMapper mapper) {
|
|
| 17 |
+ public OrganizationService(OrganizationMapper mapper, ContactMapper contactMapper) {
|
|
| 14 | 18 |
this.mapper = mapper; |
| 19 |
+ this.contactMapper = contactMapper; |
|
| 15 | 20 |
} |
| 16 | 21 |
|
| 17 | 22 |
public List<OrgResponse> findAll() {
|
| 18 | 23 |
return mapper.findAll().stream().map(OrgResponse::of).toList(); |
| 19 | 24 |
} |
| 20 | 25 |
|
| 26 |
+ /** |
|
| 27 |
+ * 담당자관리에서 이미 등록된 담당자를 신청기관/문정원/변호사 역할에 배정한다. |
|
| 28 |
+ * null인 필드는 그 역할의 배정을 해제한다는 뜻이며, 매 호출은 세 배정값과 |
|
| 29 |
+ * 배정일을 통째로 덮어쓴다(부분 갱신이 아니다). |
|
| 30 |
+ */ |
|
| 21 | 31 |
@Transactional |
| 22 |
- public OrgResponse updateContact(Long id, ContactRequest request) {
|
|
| 32 |
+ public OrgResponse updateAssignments(Long id, AssignmentRequest request) {
|
|
| 23 | 33 |
Organization org = mapper.findById(id); |
| 24 | 34 |
if (org == null) {
|
| 25 | 35 |
throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + id);
|
| 26 | 36 |
} |
| 27 |
- org.setDeptName(request.deptName()); |
|
| 28 |
- org.setManagerName(request.managerName()); |
|
| 29 |
- org.setManagerTitle(request.managerTitle()); |
|
| 30 |
- org.setManagerPhone(request.managerPhone()); |
|
| 31 |
- org.setManagerEmail(request.managerEmail()); |
|
| 32 |
- org.setMjDeptName(request.mjDeptName()); |
|
| 33 |
- org.setMjManagerName(request.mjManagerName()); |
|
| 34 |
- org.setMjManagerPhone(request.mjManagerPhone()); |
|
| 35 |
- org.setMjManagerEmail(request.mjManagerEmail()); |
|
| 36 |
- org.setLawyerName(request.lawyerName()); |
|
| 37 |
- org.setLawyerPhone(request.lawyerPhone()); |
|
| 38 |
- org.setLawyerEmail(request.lawyerEmail()); |
|
| 39 |
- org.setLawyerAssignedDate(request.lawyerAssignedDate()); |
|
| 40 |
- mapper.updateContact(org); |
|
| 37 |
+ |
|
| 38 |
+ validateAssignment(request.applicantContactId(), "APPLICANT", "신청기관 담당자"); |
|
| 39 |
+ validateAssignment(request.mjContactId(), "MJ", "문정원 담당자"); |
|
| 40 |
+ validateAssignment(request.lawyerContactId(), "LAWYER", "담당 변호사"); |
|
| 41 |
+ |
|
| 42 |
+ mapper.updateAssignments(id, request.applicantContactId(), request.mjContactId(), |
|
| 43 |
+ request.lawyerContactId(), request.lawyerAssignedDate()); |
|
| 41 | 44 |
|
| 42 | 45 |
return OrgResponse.of(mapper.findById(id)); |
| 43 | 46 |
} |
| 47 |
+ |
|
| 48 |
+ private void validateAssignment(Long contactId, String expectedCategory, String roleLabel) {
|
|
| 49 |
+ if (contactId == null) {
|
|
| 50 |
+ return; |
|
| 51 |
+ } |
|
| 52 |
+ Contact contact = contactMapper.findById(contactId); |
|
| 53 |
+ if (contact == null) {
|
|
| 54 |
+ throw new InvalidContactException(roleLabel + "로 지정한 담당자를 찾을 수 없습니다: " + contactId); |
|
| 55 |
+ } |
|
| 56 |
+ if (!expectedCategory.equals(contact.getCategory())) {
|
|
| 57 |
+ throw new InvalidContactException(roleLabel + "에는 해당 구분의 담당자만 지정할 수 있습니다."); |
|
| 58 |
+ } |
|
| 59 |
+ } |
|
| 44 | 60 |
} |
--- src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
+++ src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
... | ... | @@ -47,11 +47,11 @@ |
| 47 | 47 |
throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId);
|
| 48 | 48 |
} |
| 49 | 49 |
|
| 50 |
- if (!org.hasRequiredContact()) {
|
|
| 50 |
+ if (!org.hasApplicantAssigned()) {
|
|
| 51 | 51 |
return new ProvisionResult( |
| 52 | 52 |
ProvisionOutcome.FAILED, |
| 53 | 53 |
ProvisionOutcome.FAILED, |
| 54 |
- "담당자 정보가 채워지지 않아 채널을 만들 수 없습니다."); |
|
| 54 |
+ "신청기관 담당자가 지정되지 않아 채널을 만들 수 없습니다."); |
|
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 | 57 |
StringBuilder messages = new StringBuilder(); |
--- src/main/java/kr/itn/itnhub/seed/SeedService.java
+++ src/main/java/kr/itn/itnhub/seed/SeedService.java
... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 |
package kr.itn.itnhub.seed; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.contact.Contact; |
|
| 4 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 3 | 5 |
import kr.itn.itnhub.org.Organization; |
| 4 | 6 |
import kr.itn.itnhub.org.OrganizationMapper; |
| 5 | 7 |
import org.springframework.stereotype.Service; |
... | ... | @@ -16,12 +18,14 @@ |
| 16 | 18 |
|
| 17 | 19 |
private final SeedParser parser; |
| 18 | 20 |
private final OrganizationMapper mapper; |
| 21 |
+ private final ContactMapper contactMapper; |
|
| 19 | 22 |
private final TransactionTemplate transactionTemplate; |
| 20 | 23 |
|
| 21 |
- public SeedService(SeedParser parser, OrganizationMapper mapper, |
|
| 24 |
+ public SeedService(SeedParser parser, OrganizationMapper mapper, ContactMapper contactMapper, |
|
| 22 | 25 |
PlatformTransactionManager transactionManager) {
|
| 23 | 26 |
this.parser = parser; |
| 24 | 27 |
this.mapper = mapper; |
| 28 |
+ this.contactMapper = contactMapper; |
|
| 25 | 29 |
this.transactionTemplate = new TransactionTemplate(transactionManager); |
| 26 | 30 |
} |
| 27 | 31 |
|
... | ... | @@ -54,9 +58,39 @@ |
| 54 | 58 |
updated++; |
| 55 | 59 |
} |
| 56 | 60 |
mapper.upsertBySeed(toOrganization(row)); |
| 61 |
+ linkApplicantContactIfMissing(row); |
|
| 57 | 62 |
} |
| 58 | 63 |
|
| 59 | 64 |
return new SeedReport(created, updated, rows.size()); |
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ /** |
|
| 68 |
+ * 시트에 담당자 이름이 있고 그 기관이 아직 신청기관 담당자가 배정되지 않았을 때만 |
|
| 69 |
+ * 새 담당자를 만들어 연결한다. 이미 배정돼 있으면(담당자관리 화면에서 사람이 고쳤을 수도 |
|
| 70 |
+ * 있는 값이므로) 절대 건드리지 않는다 - 재시드가 배정을 되돌리거나 중복 생성하면 안 된다. |
|
| 71 |
+ */ |
|
| 72 |
+ private void linkApplicantContactIfMissing(SeedRow row) {
|
|
| 73 |
+ if (row.managerName() == null) {
|
|
| 74 |
+ return; |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ Organization saved = mapper.findByOrgNoAndOrgName(row.orgNo(), row.orgName()); |
|
| 78 |
+ if (saved.getApplicantContactId() != null) {
|
|
| 79 |
+ return; |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ Contact contact = new Contact(); |
|
| 83 |
+ contact.setCategory("APPLICANT");
|
|
| 84 |
+ contact.setName(row.managerName()); |
|
| 85 |
+ contact.setAffiliation(row.orgName()); |
|
| 86 |
+ contact.setDeptName(row.deptName()); |
|
| 87 |
+ contact.setTitle(row.managerTitle()); |
|
| 88 |
+ contact.setPhone(row.managerPhone()); |
|
| 89 |
+ contact.setEmail(row.managerEmail()); |
|
| 90 |
+ contactMapper.insert(contact); |
|
| 91 |
+ |
|
| 92 |
+ mapper.updateAssignments(saved.getId(), contact.getId(), |
|
| 93 |
+ saved.getMjContactId(), saved.getLawyerContactId(), saved.getLawyerAssignedDate()); |
|
| 60 | 94 |
} |
| 61 | 95 |
|
| 62 | 96 |
private String key(String orgNo, String orgName) {
|
... | ... | @@ -68,11 +102,6 @@ |
| 68 | 102 |
org.setOrgNo(row.orgNo()); |
| 69 | 103 |
org.setOrgName(row.orgName()); |
| 70 | 104 |
org.setChannelSlug(row.channelSlug()); |
| 71 |
- org.setDeptName(row.deptName()); |
|
| 72 |
- org.setManagerName(row.managerName()); |
|
| 73 |
- org.setManagerTitle(row.managerTitle()); |
|
| 74 |
- org.setManagerPhone(row.managerPhone()); |
|
| 75 |
- org.setManagerEmail(row.managerEmail()); |
|
| 76 | 105 |
return org; |
| 77 | 106 |
} |
| 78 | 107 |
} |
+++ src/main/resources/db/migration/V4__contact_directory.sql
... | ... | @@ -0,0 +1,99 @@ |
| 1 | +create table contact ( | |
| 2 | + id bigserial primary key, | |
| 3 | + category varchar(20) not null, -- 'APPLICANT' | 'MJ' | 'LAWYER' | |
| 4 | + name varchar(100) not null, | |
| 5 | + affiliation varchar(200), -- 소속 (신청기관 담당자는 기관명이 들어감) | |
| 6 | + dept_name varchar(200), | |
| 7 | + title varchar(100), | |
| 8 | + phone varchar(50), | |
| 9 | + email varchar(200), | |
| 10 | + created_at timestamptz not null default now(), | |
| 11 | + updated_at timestamptz not null default now() | |
| 12 | +); | |
| 13 | + | |
| 14 | +alter table organization | |
| 15 | + add column applicant_contact_id bigint references contact (id) on delete set null, | |
| 16 | + add column mj_contact_id bigint references contact (id) on delete set null, | |
| 17 | + add column lawyer_contact_id bigint references contact (id) on delete set null; | |
| 18 | + | |
| 19 | +-- 백필 1/3: 신청기관 담당자. 기관 행마다 최대 1명이므로 기관 id로 바로 correlate 한다. | |
| 20 | +do $$ | |
| 21 | +declare | |
| 22 | + org_row record; | |
| 23 | + new_contact_id bigint; | |
| 24 | +begin | |
| 25 | + for org_row in | |
| 26 | + select id, org_name, dept_name, manager_name, manager_title, manager_phone, manager_email | |
| 27 | + from organization | |
| 28 | + where manager_name is not null | |
| 29 | + loop | |
| 30 | + insert into contact (category, name, affiliation, dept_name, title, phone, email) | |
| 31 | + values ('APPLICANT', org_row.manager_name, org_row.org_name, org_row.dept_name, | |
| 32 | + org_row.manager_title, org_row.manager_phone, org_row.manager_email) | |
| 33 | + returning id into new_contact_id; | |
| 34 | + | |
| 35 | + update organization set applicant_contact_id = new_contact_id where id = org_row.id; | |
| 36 | + end loop; | |
| 37 | +end $$; | |
| 38 | + | |
| 39 | +-- 백필 2/3: 문정원 담당자. 여러 기관이 같은 사람을 공유할 수 있어 distinct 튜플당 1행만 만든다. | |
| 40 | +do $$ | |
| 41 | +declare | |
| 42 | + mj_row record; | |
| 43 | + new_contact_id bigint; | |
| 44 | +begin | |
| 45 | + for mj_row in | |
| 46 | + select distinct mj_manager_name, mj_dept_name, mj_manager_phone, mj_manager_email | |
| 47 | + from organization | |
| 48 | + where mj_manager_name is not null | |
| 49 | + loop | |
| 50 | + insert into contact (category, name, dept_name, phone, email) | |
| 51 | + values ('MJ', mj_row.mj_manager_name, mj_row.mj_dept_name, | |
| 52 | + mj_row.mj_manager_phone, mj_row.mj_manager_email) | |
| 53 | + returning id into new_contact_id; | |
| 54 | + | |
| 55 | + update organization | |
| 56 | + set mj_contact_id = new_contact_id | |
| 57 | + where mj_manager_name = mj_row.mj_manager_name | |
| 58 | + and mj_dept_name is not distinct from mj_row.mj_dept_name | |
| 59 | + and mj_manager_phone is not distinct from mj_row.mj_manager_phone | |
| 60 | + and mj_manager_email is not distinct from mj_row.mj_manager_email; | |
| 61 | + end loop; | |
| 62 | +end $$; | |
| 63 | + | |
| 64 | +-- 백필 3/3: 담당 변호사. 배정일은 사람이 아니라 기관↔변호사 관계에 속하므로 organization에 남긴다. | |
| 65 | +do $$ | |
| 66 | +declare | |
| 67 | + lawyer_row record; | |
| 68 | + new_contact_id bigint; | |
| 69 | +begin | |
| 70 | + for lawyer_row in | |
| 71 | + select distinct lawyer_name, lawyer_phone, lawyer_email | |
| 72 | + from organization | |
| 73 | + where lawyer_name is not null | |
| 74 | + loop | |
| 75 | + insert into contact (category, name, phone, email) | |
| 76 | + values ('LAWYER', lawyer_row.lawyer_name, lawyer_row.lawyer_phone, lawyer_row.lawyer_email) | |
| 77 | + returning id into new_contact_id; | |
| 78 | + | |
| 79 | + update organization | |
| 80 | + set lawyer_contact_id = new_contact_id | |
| 81 | + where lawyer_name = lawyer_row.lawyer_name | |
| 82 | + and lawyer_phone is not distinct from lawyer_row.lawyer_phone | |
| 83 | + and lawyer_email is not distinct from lawyer_row.lawyer_email; | |
| 84 | + end loop; | |
| 85 | +end $$; | |
| 86 | + | |
| 87 | +alter table organization | |
| 88 | + drop column dept_name, | |
| 89 | + drop column manager_name, | |
| 90 | + drop column manager_title, | |
| 91 | + drop column manager_phone, | |
| 92 | + drop column manager_email, | |
| 93 | + drop column mj_dept_name, | |
| 94 | + drop column mj_manager_name, | |
| 95 | + drop column mj_manager_phone, | |
| 96 | + drop column mj_manager_email, | |
| 97 | + drop column lawyer_name, | |
| 98 | + drop column lawyer_phone, | |
| 99 | + drop column lawyer_email; |
+++ src/main/resources/mapper/ContactMapper.xml
... | ... | @@ -0,0 +1,48 @@ |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" | |
| 3 | + "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |
| 4 | +<mapper namespace="kr.itn.itnhub.contact.ContactMapper"> | |
| 5 | + | |
| 6 | + <sql id="columns"> | |
| 7 | + id, category, name, affiliation, dept_name, title, phone, email | |
| 8 | + </sql> | |
| 9 | + | |
| 10 | + <select id="findAll" resultType="kr.itn.itnhub.contact.Contact"> | |
| 11 | + select <include refid="columns"/> | |
| 12 | + from contact | |
| 13 | + <if test="category != null"> | |
| 14 | + where category = #{category} | |
| 15 | + </if> | |
| 16 | + order by category asc, name asc | |
| 17 | + </select> | |
| 18 | + | |
| 19 | + <select id="findById" resultType="kr.itn.itnhub.contact.Contact"> | |
| 20 | + select <include refid="columns"/> | |
| 21 | + from contact | |
| 22 | + where id = #{id} | |
| 23 | + </select> | |
| 24 | + | |
| 25 | + <insert id="insert" parameterType="kr.itn.itnhub.contact.Contact" | |
| 26 | + useGeneratedKeys="true" keyProperty="id"> | |
| 27 | + insert into contact (category, name, affiliation, dept_name, title, phone, email) | |
| 28 | + values (#{category}, #{name}, #{affiliation}, #{deptName}, #{title}, #{phone}, #{email}) | |
| 29 | + </insert> | |
| 30 | + | |
| 31 | + <update id="update" parameterType="kr.itn.itnhub.contact.Contact"> | |
| 32 | + update contact set | |
| 33 | + category = #{category}, | |
| 34 | + name = #{name}, | |
| 35 | + affiliation = #{affiliation}, | |
| 36 | + dept_name = #{deptName}, | |
| 37 | + title = #{title}, | |
| 38 | + phone = #{phone}, | |
| 39 | + email = #{email}, | |
| 40 | + updated_at = now() | |
| 41 | + where id = #{id} | |
| 42 | + </update> | |
| 43 | + | |
| 44 | + <delete id="delete"> | |
| 45 | + delete from contact where id = #{id} | |
| 46 | + </delete> | |
| 47 | + | |
| 48 | +</mapper> |
--- src/main/resources/mapper/OrganizationMapper.xml
+++ src/main/resources/mapper/OrganizationMapper.xml
... | ... | @@ -3,64 +3,117 @@ |
| 3 | 3 |
"https://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| 4 | 4 |
<mapper namespace="kr.itn.itnhub.org.OrganizationMapper"> |
| 5 | 5 |
|
| 6 |
- <sql id="columns"> |
|
| 7 |
- id, org_no, org_name, channel_slug, |
|
| 8 |
- dept_name, manager_name, manager_title, manager_phone, manager_email, |
|
| 9 |
- mj_dept_name, mj_manager_name, mj_manager_phone, mj_manager_email, |
|
| 10 |
- lawyer_name, lawyer_phone, lawyer_email, lawyer_assigned_date, |
|
| 11 |
- channel_id_mj, channel_id_law |
|
| 6 |
+ <!-- |
|
| 7 |
+ 담당자 3종은 contact 테이블을 세 번 조인해서 채운다(신청기관/문정원/변호사 배정이 각각 |
|
| 8 |
+ 다른 contact 행을 가리킬 수 있으므로). 각 조인에 별칭 컬럼을 붙여야 resultMap의 |
|
| 9 |
+ association 세 개가 서로 컬럼을 침범하지 않는다. |
|
| 10 |
+ --> |
|
| 11 |
+ <resultMap id="organizationResultMap" type="kr.itn.itnhub.org.Organization"> |
|
| 12 |
+ <id property="id" column="id"/> |
|
| 13 |
+ <result property="orgNo" column="org_no"/> |
|
| 14 |
+ <result property="orgName" column="org_name"/> |
|
| 15 |
+ <result property="channelSlug" column="channel_slug"/> |
|
| 16 |
+ <result property="applicantContactId" column="applicant_contact_id"/> |
|
| 17 |
+ <result property="mjContactId" column="mj_contact_id"/> |
|
| 18 |
+ <result property="lawyerContactId" column="lawyer_contact_id"/> |
|
| 19 |
+ <result property="lawyerAssignedDate" column="lawyer_assigned_date"/> |
|
| 20 |
+ <result property="channelIdMj" column="channel_id_mj"/> |
|
| 21 |
+ <result property="channelIdLaw" column="channel_id_law"/> |
|
| 22 |
+ |
|
| 23 |
+ <association property="applicant" javaType="kr.itn.itnhub.contact.Contact"> |
|
| 24 |
+ <id property="id" column="applicant_id"/> |
|
| 25 |
+ <result property="category" column="applicant_category"/> |
|
| 26 |
+ <result property="name" column="applicant_name"/> |
|
| 27 |
+ <result property="affiliation" column="applicant_affiliation"/> |
|
| 28 |
+ <result property="deptName" column="applicant_dept_name"/> |
|
| 29 |
+ <result property="title" column="applicant_title"/> |
|
| 30 |
+ <result property="phone" column="applicant_phone"/> |
|
| 31 |
+ <result property="email" column="applicant_email"/> |
|
| 32 |
+ </association> |
|
| 33 |
+ |
|
| 34 |
+ <association property="mj" javaType="kr.itn.itnhub.contact.Contact"> |
|
| 35 |
+ <id property="id" column="mj_c_id"/> |
|
| 36 |
+ <result property="category" column="mj_c_category"/> |
|
| 37 |
+ <result property="name" column="mj_c_name"/> |
|
| 38 |
+ <result property="affiliation" column="mj_c_affiliation"/> |
|
| 39 |
+ <result property="deptName" column="mj_c_dept_name"/> |
|
| 40 |
+ <result property="title" column="mj_c_title"/> |
|
| 41 |
+ <result property="phone" column="mj_c_phone"/> |
|
| 42 |
+ <result property="email" column="mj_c_email"/> |
|
| 43 |
+ </association> |
|
| 44 |
+ |
|
| 45 |
+ <association property="lawyer" javaType="kr.itn.itnhub.contact.Contact"> |
|
| 46 |
+ <id property="id" column="lawyer_c_id"/> |
|
| 47 |
+ <result property="category" column="lawyer_c_category"/> |
|
| 48 |
+ <result property="name" column="lawyer_c_name"/> |
|
| 49 |
+ <result property="affiliation" column="lawyer_c_affiliation"/> |
|
| 50 |
+ <result property="deptName" column="lawyer_c_dept_name"/> |
|
| 51 |
+ <result property="title" column="lawyer_c_title"/> |
|
| 52 |
+ <result property="phone" column="lawyer_c_phone"/> |
|
| 53 |
+ <result property="email" column="lawyer_c_email"/> |
|
| 54 |
+ </association> |
|
| 55 |
+ </resultMap> |
|
| 56 |
+ |
|
| 57 |
+ <sql id="joinedColumns"> |
|
| 58 |
+ o.id, o.org_no, o.org_name, o.channel_slug, |
|
| 59 |
+ o.applicant_contact_id, o.mj_contact_id, o.lawyer_contact_id, |
|
| 60 |
+ o.lawyer_assigned_date, o.channel_id_mj, o.channel_id_law, |
|
| 61 |
+ ac.id as applicant_id, ac.category as applicant_category, ac.name as applicant_name, |
|
| 62 |
+ ac.affiliation as applicant_affiliation, ac.dept_name as applicant_dept_name, |
|
| 63 |
+ ac.title as applicant_title, ac.phone as applicant_phone, ac.email as applicant_email, |
|
| 64 |
+ mc.id as mj_c_id, mc.category as mj_c_category, mc.name as mj_c_name, |
|
| 65 |
+ mc.affiliation as mj_c_affiliation, mc.dept_name as mj_c_dept_name, |
|
| 66 |
+ mc.title as mj_c_title, mc.phone as mj_c_phone, mc.email as mj_c_email, |
|
| 67 |
+ lc.id as lawyer_c_id, lc.category as lawyer_c_category, lc.name as lawyer_c_name, |
|
| 68 |
+ lc.affiliation as lawyer_c_affiliation, lc.dept_name as lawyer_c_dept_name, |
|
| 69 |
+ lc.title as lawyer_c_title, lc.phone as lawyer_c_phone, lc.email as lawyer_c_email |
|
| 12 | 70 |
</sql> |
| 13 | 71 |
|
| 14 |
- <select id="findAll" resultType="kr.itn.itnhub.org.Organization"> |
|
| 15 |
- select <include refid="columns"/> |
|
| 16 |
- from organization |
|
| 17 |
- order by org_no asc, org_name asc |
|
| 72 |
+ <sql id="joins"> |
|
| 73 |
+ from organization o |
|
| 74 |
+ left join contact ac on ac.id = o.applicant_contact_id |
|
| 75 |
+ left join contact mc on mc.id = o.mj_contact_id |
|
| 76 |
+ left join contact lc on lc.id = o.lawyer_contact_id |
|
| 77 |
+ </sql> |
|
| 78 |
+ |
|
| 79 |
+ <select id="findAll" resultMap="organizationResultMap"> |
|
| 80 |
+ select <include refid="joinedColumns"/> |
|
| 81 |
+ <include refid="joins"/> |
|
| 82 |
+ order by o.org_no asc, o.org_name asc |
|
| 18 | 83 |
</select> |
| 19 | 84 |
|
| 20 |
- <select id="findById" resultType="kr.itn.itnhub.org.Organization"> |
|
| 21 |
- select <include refid="columns"/> |
|
| 22 |
- from organization |
|
| 23 |
- where id = #{id}
|
|
| 85 |
+ <select id="findById" resultMap="organizationResultMap"> |
|
| 86 |
+ select <include refid="joinedColumns"/> |
|
| 87 |
+ <include refid="joins"/> |
|
| 88 |
+ where o.id = #{id}
|
|
| 89 |
+ </select> |
|
| 90 |
+ |
|
| 91 |
+ <select id="findByOrgNoAndOrgName" resultMap="organizationResultMap"> |
|
| 92 |
+ select <include refid="joinedColumns"/> |
|
| 93 |
+ <include refid="joins"/> |
|
| 94 |
+ where o.org_no = #{orgNo} and o.org_name = #{orgName}
|
|
| 24 | 95 |
</select> |
| 25 | 96 |
|
| 26 | 97 |
<!-- |
| 27 |
- 시드 upsert. coalesce(기존값, 새값) 순서가 핵심이다. |
|
| 28 |
- 기존값이 있으면 그대로 두고 비어 있을 때만 시트 값으로 채운다. |
|
| 29 |
- 반대로 쓰면 사람이 웹에서 고친 값이 시트 값으로 되돌아간다. |
|
| 98 |
+ 시드 upsert. 담당자 정보는 이제 여기서 다루지 않는다(담당자관리에서만 입력) - |
|
| 99 |
+ SeedService가 upsert 이후 별도로 신청기관 담당자 연결 여부를 확인해 필요할 때만 |
|
| 100 |
+ contact를 만들어 연결한다. |
|
| 30 | 101 |
--> |
| 31 | 102 |
<insert id="upsertBySeed" parameterType="kr.itn.itnhub.org.Organization"> |
| 32 |
- insert into organization |
|
| 33 |
- (org_no, org_name, channel_slug, |
|
| 34 |
- dept_name, manager_name, manager_title, manager_phone, manager_email) |
|
| 35 |
- values |
|
| 36 |
- (#{orgNo}, #{orgName}, #{channelSlug},
|
|
| 37 |
- #{deptName}, #{managerName}, #{managerTitle}, #{managerPhone}, #{managerEmail})
|
|
| 103 |
+ insert into organization (org_no, org_name, channel_slug) |
|
| 104 |
+ values (#{orgNo}, #{orgName}, #{channelSlug})
|
|
| 38 | 105 |
on conflict (org_no, org_name) do update set |
| 39 |
- channel_slug = excluded.channel_slug, |
|
| 40 |
- dept_name = coalesce(organization.dept_name, excluded.dept_name), |
|
| 41 |
- manager_name = coalesce(organization.manager_name, excluded.manager_name), |
|
| 42 |
- manager_title = coalesce(organization.manager_title, excluded.manager_title), |
|
| 43 |
- manager_phone = coalesce(organization.manager_phone, excluded.manager_phone), |
|
| 44 |
- manager_email = coalesce(organization.manager_email, excluded.manager_email), |
|
| 45 |
- updated_at = now() |
|
| 106 |
+ channel_slug = excluded.channel_slug, |
|
| 107 |
+ updated_at = now() |
|
| 46 | 108 |
</insert> |
| 47 | 109 |
|
| 48 |
- <update id="updateContact" parameterType="kr.itn.itnhub.org.Organization"> |
|
| 110 |
+ <update id="updateAssignments"> |
|
| 49 | 111 |
update organization set |
| 50 |
- dept_name = #{deptName},
|
|
| 51 |
- manager_name = #{managerName},
|
|
| 52 |
- manager_title = #{managerTitle},
|
|
| 53 |
- manager_phone = #{managerPhone},
|
|
| 54 |
- manager_email = #{managerEmail},
|
|
| 55 |
- mj_dept_name = #{mjDeptName},
|
|
| 56 |
- mj_manager_name = #{mjManagerName},
|
|
| 57 |
- mj_manager_phone = #{mjManagerPhone},
|
|
| 58 |
- mj_manager_email = #{mjManagerEmail},
|
|
| 59 |
- lawyer_name = #{lawyerName},
|
|
| 60 |
- lawyer_phone = #{lawyerPhone},
|
|
| 61 |
- lawyer_email = #{lawyerEmail},
|
|
| 62 |
- lawyer_assigned_date = #{lawyerAssignedDate},
|
|
| 63 |
- updated_at = now() |
|
| 112 |
+ applicant_contact_id = #{applicantContactId},
|
|
| 113 |
+ mj_contact_id = #{mjContactId},
|
|
| 114 |
+ lawyer_contact_id = #{lawyerContactId},
|
|
| 115 |
+ lawyer_assigned_date = #{lawyerAssignedDate},
|
|
| 116 |
+ updated_at = now() |
|
| 64 | 117 |
where id = #{id}
|
| 65 | 118 |
</update> |
| 66 | 119 |
|
+++ src/test/java/kr/itn/itnhub/contact/ContactControllerTest.java
... | ... | @@ -0,0 +1,182 @@ |
| 1 | +package kr.itn.itnhub.contact; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import kr.itn.itnhub.org.Organization; | |
| 5 | +import kr.itn.itnhub.org.OrganizationMapper; | |
| 6 | +import org.junit.jupiter.api.BeforeEach; | |
| 7 | +import org.junit.jupiter.api.Test; | |
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 9 | +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | |
| 10 | +import org.springframework.http.MediaType; | |
| 11 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 12 | +import org.springframework.security.test.context.support.WithMockUser; | |
| 13 | +import org.springframework.test.web.servlet.MockMvc; | |
| 14 | + | |
| 15 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 16 | +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; | |
| 17 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; | |
| 18 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | |
| 19 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | |
| 20 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; | |
| 21 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | |
| 22 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | |
| 23 | + | |
| 24 | +@AutoConfigureMockMvc | |
| 25 | +@WithMockUser(roles = "ADMIN") | |
| 26 | +class ContactControllerTest extends AbstractDbTest { | |
| 27 | + | |
| 28 | + @Autowired | |
| 29 | + MockMvc mvc; | |
| 30 | + | |
| 31 | + @Autowired | |
| 32 | + ContactMapper contactMapper; | |
| 33 | + | |
| 34 | + @Autowired | |
| 35 | + OrganizationMapper orgMapper; | |
| 36 | + | |
| 37 | + @Autowired | |
| 38 | + JdbcTemplate jdbc; | |
| 39 | + | |
| 40 | + @BeforeEach | |
| 41 | + void clean() { | |
| 42 | + jdbc.update("delete from organization"); | |
| 43 | + jdbc.update("delete from contact"); | |
| 44 | + } | |
| 45 | + | |
| 46 | + @Test | |
| 47 | + void 담당자를_등록하면_저장된_값을_돌려준다() throws Exception { | |
| 48 | + mvc.perform(post("/api/contacts") | |
| 49 | + .with(csrf()) | |
| 50 | + .contentType(MediaType.APPLICATION_JSON) | |
| 51 | + .content(""" | |
| 52 | + { | |
| 53 | + "category": "APPLICANT", | |
| 54 | + "name": "송민지", | |
| 55 | + "affiliation": "국제방송교류재단", | |
| 56 | + "deptName": "데이터정보화팀", | |
| 57 | + "title": "과장", | |
| 58 | + "phone": "02-3475-5434", | |
| 59 | + "email": "ming@arirang.com" | |
| 60 | + } | |
| 61 | + """)) | |
| 62 | + .andExpect(status().isOk()) | |
| 63 | + .andExpect(jsonPath("$.category").value("APPLICANT")) | |
| 64 | + .andExpect(jsonPath("$.name").value("송민지")) | |
| 65 | + .andExpect(jsonPath("$.affiliation").value("국제방송교류재단")) | |
| 66 | + .andExpect(jsonPath("$.id").isNumber()); | |
| 67 | + | |
| 68 | + assertThat(contactMapper.findAll(null)).hasSize(1); | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Test | |
| 72 | + void 구분_필터로_목록을_조회한다() throws Exception { | |
| 73 | + Contact applicant = new Contact(); | |
| 74 | + applicant.setCategory("APPLICANT"); | |
| 75 | + applicant.setName("송민지"); | |
| 76 | + contactMapper.insert(applicant); | |
| 77 | + | |
| 78 | + Contact lawyer = new Contact(); | |
| 79 | + lawyer.setCategory("LAWYER"); | |
| 80 | + lawyer.setName("이변호"); | |
| 81 | + contactMapper.insert(lawyer); | |
| 82 | + | |
| 83 | + mvc.perform(get("/api/contacts").param("category", "LAWYER")) | |
| 84 | + .andExpect(status().isOk()) | |
| 85 | + .andExpect(jsonPath("$.length()").value(1)) | |
| 86 | + .andExpect(jsonPath("$[0].name").value("이변호")); | |
| 87 | + | |
| 88 | + mvc.perform(get("/api/contacts")) | |
| 89 | + .andExpect(status().isOk()) | |
| 90 | + .andExpect(jsonPath("$.length()").value(2)); | |
| 91 | + } | |
| 92 | + | |
| 93 | + @Test | |
| 94 | + void 구분값이_잘못되면_등록시_400이다() throws Exception { | |
| 95 | + mvc.perform(post("/api/contacts") | |
| 96 | + .with(csrf()) | |
| 97 | + .contentType(MediaType.APPLICATION_JSON) | |
| 98 | + .content(""" | |
| 99 | + { "category": "UNKNOWN", "name": "홍길동" } | |
| 100 | + """)) | |
| 101 | + .andExpect(status().isBadRequest()); | |
| 102 | + } | |
| 103 | + | |
| 104 | + @Test | |
| 105 | + void 성명이_비면_등록시_400이다() throws Exception { | |
| 106 | + mvc.perform(post("/api/contacts") | |
| 107 | + .with(csrf()) | |
| 108 | + .contentType(MediaType.APPLICATION_JSON) | |
| 109 | + .content(""" | |
| 110 | + { "category": "APPLICANT", "name": "" } | |
| 111 | + """)) | |
| 112 | + .andExpect(status().isBadRequest()) | |
| 113 | + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("성명"))); | |
| 114 | + } | |
| 115 | + | |
| 116 | + @Test | |
| 117 | + void 담당자를_수정하면_변경된_값이_저장된다() throws Exception { | |
| 118 | + Contact contact = new Contact(); | |
| 119 | + contact.setCategory("MJ"); | |
| 120 | + contact.setName("김문정"); | |
| 121 | + contactMapper.insert(contact); | |
| 122 | + | |
| 123 | + mvc.perform(put("/api/contacts/{id}", contact.getId()) | |
| 124 | + .with(csrf()) | |
| 125 | + .contentType(MediaType.APPLICATION_JSON) | |
| 126 | + .content(""" | |
| 127 | + { | |
| 128 | + "category": "MJ", | |
| 129 | + "name": "김문정", | |
| 130 | + "phone": "02-1234-5678", | |
| 131 | + "email": "mj@mcst.go.kr" | |
| 132 | + } | |
| 133 | + """)) | |
| 134 | + .andExpect(status().isOk()) | |
| 135 | + .andExpect(jsonPath("$.phone").value("02-1234-5678")) | |
| 136 | + .andExpect(jsonPath("$.email").value("mj@mcst.go.kr")); | |
| 137 | + } | |
| 138 | + | |
| 139 | + @Test | |
| 140 | + void 존재하지_않는_담당자를_수정하면_404다() throws Exception { | |
| 141 | + mvc.perform(put("/api/contacts/{id}", 999999) | |
| 142 | + .with(csrf()) | |
| 143 | + .contentType(MediaType.APPLICATION_JSON) | |
| 144 | + .content(""" | |
| 145 | + { "category": "MJ", "name": "김문정" } | |
| 146 | + """)) | |
| 147 | + .andExpect(status().isNotFound()); | |
| 148 | + } | |
| 149 | + | |
| 150 | + @Test | |
| 151 | + void 존재하지_않는_담당자를_삭제하면_404다() throws Exception { | |
| 152 | + mvc.perform(delete("/api/contacts/{id}", 999999).with(csrf())) | |
| 153 | + .andExpect(status().isNotFound()); | |
| 154 | + } | |
| 155 | + | |
| 156 | + @Test | |
| 157 | + void 담당자를_삭제하면_배정되어_있던_기관의_배정도_함께_해제된다() throws Exception { | |
| 158 | + Contact contact = new Contact(); | |
| 159 | + contact.setCategory("APPLICANT"); | |
| 160 | + contact.setName("송민지"); | |
| 161 | + contactMapper.insert(contact); | |
| 162 | + | |
| 163 | + Organization org = new Organization(); | |
| 164 | + org.setOrgNo("001"); | |
| 165 | + org.setOrgName("국제방송교류재단"); | |
| 166 | + org.setChannelSlug("001"); | |
| 167 | + orgMapper.upsertBySeed(org); | |
| 168 | + Long orgId = orgMapper.findAll().get(0).getId(); | |
| 169 | + orgMapper.updateAssignments(orgId, contact.getId(), null, null, null); | |
| 170 | + | |
| 171 | + assertThat(orgMapper.findById(orgId).getStatus().name()).isEqualTo("READY"); | |
| 172 | + | |
| 173 | + mvc.perform(delete("/api/contacts/{id}", contact.getId()).with(csrf())) | |
| 174 | + .andExpect(status().isNoContent()); | |
| 175 | + | |
| 176 | + assertThat(contactMapper.findById(contact.getId())).isNull(); | |
| 177 | + | |
| 178 | + Organization after = orgMapper.findById(orgId); | |
| 179 | + assertThat(after.getApplicantContactId()).isNull(); | |
| 180 | + assertThat(after.getStatus().name()).isEqualTo("INFO_PENDING"); | |
| 181 | + } | |
| 182 | +} |
--- src/test/java/kr/itn/itnhub/org/OrganizationControllerTest.java
+++ src/test/java/kr/itn/itnhub/org/OrganizationControllerTest.java
... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 | 3 |
import kr.itn.itnhub.AbstractDbTest; |
| 4 |
+import kr.itn.itnhub.contact.Contact; |
|
| 5 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 4 | 6 |
import kr.itn.itnhub.mattermost.MattermostClient; |
| 5 | 7 |
import kr.itn.itnhub.mattermost.MattermostException; |
| 6 | 8 |
import org.junit.jupiter.api.BeforeEach; |
... | ... | @@ -39,6 +41,9 @@ |
| 39 | 41 |
OrganizationMapper mapper; |
| 40 | 42 |
|
| 41 | 43 |
@Autowired |
| 44 |
+ ContactMapper contactMapper; |
|
| 45 |
+ |
|
| 46 |
+ @Autowired |
|
| 42 | 47 |
JdbcTemplate jdbc; |
| 43 | 48 |
|
| 44 | 49 |
@MockBean |
... | ... | @@ -49,6 +54,7 @@ |
| 49 | 54 |
@BeforeEach |
| 50 | 55 |
void setUp() {
|
| 51 | 56 |
jdbc.update("delete from organization");
|
| 57 |
+ jdbc.update("delete from contact");
|
|
| 52 | 58 |
|
| 53 | 59 |
Organization org = new Organization(); |
| 54 | 60 |
org.setOrgNo("001");
|
... | ... | @@ -57,6 +63,14 @@ |
| 57 | 63 |
mapper.upsertBySeed(org); |
| 58 | 64 |
|
| 59 | 65 |
orgId = mapper.findAll().get(0).getId(); |
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ private Long createContact(String category, String name) {
|
|
| 69 |
+ Contact contact = new Contact(); |
|
| 70 |
+ contact.setCategory(category); |
|
| 71 |
+ contact.setName(name); |
|
| 72 |
+ contactMapper.insert(contact); |
|
| 73 |
+ return contact.getId(); |
|
| 60 | 74 |
} |
| 61 | 75 |
|
| 62 | 76 |
@Test |
... | ... | @@ -69,151 +83,115 @@ |
| 69 | 83 |
} |
| 70 | 84 |
|
| 71 | 85 |
@Test |
| 72 |
- void 담당자정보를_저장하면_생성가능_상태가_된다() throws Exception {
|
|
| 73 |
- mvc.perform(put("/api/orgs/{id}/contact", orgId)
|
|
| 86 |
+ void 신청기관_담당자를_배정하면_생성가능_상태가_된다() throws Exception {
|
|
| 87 |
+ Long applicantId = createContact("APPLICANT", "송민지");
|
|
| 88 |
+ |
|
| 89 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 74 | 90 |
.with(csrf()) |
| 75 | 91 |
.contentType(MediaType.APPLICATION_JSON) |
| 76 |
- .content("""
|
|
| 77 |
- {
|
|
| 78 |
- "deptName": "데이터정보화팀", |
|
| 79 |
- "managerName": "송민지", |
|
| 80 |
- "managerTitle": "과장", |
|
| 81 |
- "managerPhone": "02-3475-5434", |
|
| 82 |
- "managerEmail": "ming@arirang.com" |
|
| 83 |
- } |
|
| 84 |
- """)) |
|
| 92 |
+ .content("{\"applicantContactId\": " + applicantId + "}"))
|
|
| 85 | 93 |
.andExpect(status().isOk()) |
| 86 |
- .andExpect(jsonPath("$.status").value("READY"));
|
|
| 94 |
+ .andExpect(jsonPath("$.status").value("READY"))
|
|
| 95 |
+ .andExpect(jsonPath("$.applicant.name").value("송민지"));
|
|
| 87 | 96 |
|
| 88 |
- assertThat(mapper.findById(orgId).getDeptName()).isEqualTo("데이터정보화팀");
|
|
| 97 |
+ assertThat(mapper.findById(orgId).getApplicantContactId()).isEqualTo(applicantId); |
|
| 89 | 98 |
} |
| 90 | 99 |
|
| 91 | 100 |
@Test |
| 92 |
- void 문정원_담당자와_변호사_정보를_포함해_저장하면_응답과_재조회에_모두_반영된다() throws Exception {
|
|
| 93 |
- mvc.perform(put("/api/orgs/{id}/contact", orgId)
|
|
| 101 |
+ void 문정원_담당자와_변호사를_함께_배정하면_응답과_재조회에_모두_반영된다() throws Exception {
|
|
| 102 |
+ Long applicantId = createContact("APPLICANT", "송민지");
|
|
| 103 |
+ Long mjId = createContact("MJ", "김문정");
|
|
| 104 |
+ Long lawyerId = createContact("LAWYER", "이변호");
|
|
| 105 |
+ |
|
| 106 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 94 | 107 |
.with(csrf()) |
| 95 | 108 |
.contentType(MediaType.APPLICATION_JSON) |
| 96 | 109 |
.content("""
|
| 97 | 110 |
{
|
| 98 |
- "deptName": "데이터정보화팀", |
|
| 99 |
- "managerName": "송민지", |
|
| 100 |
- "managerTitle": "과장", |
|
| 101 |
- "managerPhone": "02-3475-5434", |
|
| 102 |
- "managerEmail": "ming@arirang.com", |
|
| 103 |
- "mjDeptName": "문화체육관광부 저작권정책과", |
|
| 104 |
- "mjManagerName": "김문정", |
|
| 105 |
- "mjManagerPhone": "02-1234-5678", |
|
| 106 |
- "mjManagerEmail": "mj@mcst.go.kr", |
|
| 107 |
- "lawyerName": "이변호", |
|
| 108 |
- "lawyerPhone": "02-9876-5432", |
|
| 109 |
- "lawyerEmail": "lawyer@lawfirm.kr", |
|
| 111 |
+ "applicantContactId": %d, |
|
| 112 |
+ "mjContactId": %d, |
|
| 113 |
+ "lawyerContactId": %d, |
|
| 110 | 114 |
"lawyerAssignedDate": "2026-07-21" |
| 111 | 115 |
} |
| 112 |
- """)) |
|
| 116 |
+ """.formatted(applicantId, mjId, lawyerId))) |
|
| 113 | 117 |
.andExpect(status().isOk()) |
| 114 |
- .andExpect(jsonPath("$.mjDeptName").value("문화체육관광부 저작권정책과"))
|
|
| 115 |
- .andExpect(jsonPath("$.mjManagerName").value("김문정"))
|
|
| 116 |
- .andExpect(jsonPath("$.mjManagerPhone").value("02-1234-5678"))
|
|
| 117 |
- .andExpect(jsonPath("$.mjManagerEmail").value("mj@mcst.go.kr"))
|
|
| 118 |
- .andExpect(jsonPath("$.lawyerName").value("이변호"))
|
|
| 119 |
- .andExpect(jsonPath("$.lawyerPhone").value("02-9876-5432"))
|
|
| 120 |
- .andExpect(jsonPath("$.lawyerEmail").value("lawyer@lawfirm.kr"))
|
|
| 118 |
+ .andExpect(jsonPath("$.mj.name").value("김문정"))
|
|
| 119 |
+ .andExpect(jsonPath("$.lawyer.name").value("이변호"))
|
|
| 121 | 120 |
.andExpect(jsonPath("$.lawyerAssignedDate").value("2026-07-21"));
|
| 122 | 121 |
|
| 123 | 122 |
mvc.perform(get("/api/orgs"))
|
| 124 | 123 |
.andExpect(status().isOk()) |
| 125 |
- .andExpect(jsonPath("$[0].mjManagerName").value("김문정"))
|
|
| 126 |
- .andExpect(jsonPath("$[0].lawyerName").value("이변호"));
|
|
| 124 |
+ .andExpect(jsonPath("$[0].mj.name").value("김문정"))
|
|
| 125 |
+ .andExpect(jsonPath("$[0].lawyer.name").value("이변호"));
|
|
| 127 | 126 |
} |
| 128 | 127 |
|
| 129 | 128 |
@Test |
| 130 |
- void 문정원_변호사_정보없이_기존방식_페이로드만으로도_저장에_성공한다() throws Exception {
|
|
| 131 |
- mvc.perform(put("/api/orgs/{id}/contact", orgId)
|
|
| 129 |
+ void 신청기관_배정없이_문정원_변호사만_배정해도_저장에_성공하고_정보대기로_남는다() throws Exception {
|
|
| 130 |
+ Long mjId = createContact("MJ", "김문정");
|
|
| 131 |
+ |
|
| 132 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 132 | 133 |
.with(csrf()) |
| 133 | 134 |
.contentType(MediaType.APPLICATION_JSON) |
| 134 |
- .content("""
|
|
| 135 |
- {
|
|
| 136 |
- "deptName": "데이터정보화팀", |
|
| 137 |
- "managerName": "송민지", |
|
| 138 |
- "managerTitle": "과장", |
|
| 139 |
- "managerPhone": "02-3475-5434", |
|
| 140 |
- "managerEmail": "ming@arirang.com" |
|
| 141 |
- } |
|
| 142 |
- """)) |
|
| 135 |
+ .content("{\"mjContactId\": " + mjId + "}"))
|
|
| 143 | 136 |
.andExpect(status().isOk()) |
| 144 |
- .andExpect(jsonPath("$.status").value("READY"))
|
|
| 145 |
- .andExpect(jsonPath("$.mjManagerName").value(nullValue()))
|
|
| 146 |
- .andExpect(jsonPath("$.lawyerName").value(nullValue()));
|
|
| 137 |
+ .andExpect(jsonPath("$.status").value("INFO_PENDING"))
|
|
| 138 |
+ .andExpect(jsonPath("$.applicant").value(nullValue()))
|
|
| 139 |
+ .andExpect(jsonPath("$.mj.name").value("김문정"));
|
|
| 147 | 140 |
} |
| 148 | 141 |
|
| 149 | 142 |
@Test |
| 150 |
- void 존재하지_않는_기관ID로_담당자정보를_수정하면_404다() throws Exception {
|
|
| 143 |
+ void 배정_해제는_null을_보내면_반영되어_정보대기로_돌아간다() throws Exception {
|
|
| 144 |
+ Long applicantId = createContact("APPLICANT", "송민지");
|
|
| 145 |
+ mapper.updateAssignments(orgId, applicantId, null, null, null); |
|
| 146 |
+ |
|
| 147 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 148 |
+ .with(csrf()) |
|
| 149 |
+ .contentType(MediaType.APPLICATION_JSON) |
|
| 150 |
+ .content("{\"applicantContactId\": null}"))
|
|
| 151 |
+ .andExpect(status().isOk()) |
|
| 152 |
+ .andExpect(jsonPath("$.status").value("INFO_PENDING"))
|
|
| 153 |
+ .andExpect(jsonPath("$.applicant").value(nullValue()));
|
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ @Test |
|
| 157 |
+ void 존재하지_않는_기관ID로_배정을_수정하면_404다() throws Exception {
|
|
| 151 | 158 |
long missingId = orgId + 999999L; |
| 152 | 159 |
|
| 153 |
- mvc.perform(put("/api/orgs/{id}/contact", missingId)
|
|
| 160 |
+ mvc.perform(put("/api/orgs/{id}/assignments", missingId)
|
|
| 154 | 161 |
.with(csrf()) |
| 155 | 162 |
.contentType(MediaType.APPLICATION_JSON) |
| 156 |
- .content("""
|
|
| 157 |
- {
|
|
| 158 |
- "deptName": "데이터정보화팀", |
|
| 159 |
- "managerName": "송민지", |
|
| 160 |
- "managerTitle": "과장", |
|
| 161 |
- "managerPhone": "02-3475-5434", |
|
| 162 |
- "managerEmail": "ming@arirang.com" |
|
| 163 |
- } |
|
| 164 |
- """)) |
|
| 163 |
+ .content("{}"))
|
|
| 165 | 164 |
.andExpect(status().isNotFound()) |
| 166 | 165 |
.andExpect(jsonPath("$.message").value(containsString(String.valueOf(missingId))));
|
| 167 | 166 |
} |
| 168 | 167 |
|
| 169 | 168 |
@Test |
| 170 |
- void 필수값이_비면_400이다() throws Exception {
|
|
| 171 |
- mvc.perform(put("/api/orgs/{id}/contact", orgId)
|
|
| 169 |
+ void 존재하지_않는_담당자ID를_배정하면_400이다() throws Exception {
|
|
| 170 |
+ long missingContactId = 999999L; |
|
| 171 |
+ |
|
| 172 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 172 | 173 |
.with(csrf()) |
| 173 | 174 |
.contentType(MediaType.APPLICATION_JSON) |
| 174 |
- .content("""
|
|
| 175 |
- {
|
|
| 176 |
- "deptName": "", |
|
| 177 |
- "managerName": "송민지", |
|
| 178 |
- "managerPhone": "02-3475-5434", |
|
| 179 |
- "managerEmail": "ming@arirang.com" |
|
| 180 |
- } |
|
| 181 |
- """)) |
|
| 175 |
+ .content("{\"applicantContactId\": " + missingContactId + "}"))
|
|
| 182 | 176 |
.andExpect(status().isBadRequest()); |
| 183 | 177 |
} |
| 184 | 178 |
|
| 185 |
- /** |
|
| 186 |
- * Finding I6 회귀 테스트: 검증 실패 응답 본문이 필드명을 그대로 노출하는 원시 JSON |
|
| 187 |
- * 블롭(예: {"deptName":"must not be blank"})이 아니라, 운영자가 읽을 수 있는
|
|
| 188 |
- * 한글 라벨(부서명 등)이 담긴 {@link kr.itn.itnhub.config.ApiError} 메시지여야 한다.
|
|
| 189 |
- */ |
|
| 190 | 179 |
@Test |
| 191 |
- void 필수값이_비면_한글_필드명이_담긴_메시지를_돌려준다() throws Exception {
|
|
| 192 |
- mvc.perform(put("/api/orgs/{id}/contact", orgId)
|
|
| 180 |
+ void 구분이_맞지_않는_담당자를_배정하면_400이다() throws Exception {
|
|
| 181 |
+ Long mjContactId = createContact("MJ", "김문정");
|
|
| 182 |
+ |
|
| 183 |
+ mvc.perform(put("/api/orgs/{id}/assignments", orgId)
|
|
| 193 | 184 |
.with(csrf()) |
| 194 | 185 |
.contentType(MediaType.APPLICATION_JSON) |
| 195 |
- .content("""
|
|
| 196 |
- {
|
|
| 197 |
- "deptName": "", |
|
| 198 |
- "managerName": "", |
|
| 199 |
- "managerPhone": "02-3475-5434", |
|
| 200 |
- "managerEmail": "ming@arirang.com" |
|
| 201 |
- } |
|
| 202 |
- """)) |
|
| 186 |
+ .content("{\"applicantContactId\": " + mjContactId + "}"))
|
|
| 203 | 187 |
.andExpect(status().isBadRequest()) |
| 204 |
- .andExpect(jsonPath("$.message").value(containsString("부서명")))
|
|
| 205 |
- .andExpect(jsonPath("$.message").value(containsString("담당자명")));
|
|
| 188 |
+ .andExpect(jsonPath("$.message").value(containsString("신청기관 담당자")));
|
|
| 206 | 189 |
} |
| 207 | 190 |
|
| 208 | 191 |
@Test |
| 209 | 192 |
void 채널생성을_요청하면_결과를_돌려준다() throws Exception {
|
| 210 |
- Organization ready = mapper.findById(orgId); |
|
| 211 |
- ready.setDeptName("데이터정보화팀");
|
|
| 212 |
- ready.setManagerName("송민지");
|
|
| 213 |
- ready.setManagerTitle("과장");
|
|
| 214 |
- ready.setManagerPhone("02-3475-5434");
|
|
| 215 |
- ready.setManagerEmail("ming@arirang.com");
|
|
| 216 |
- mapper.updateContact(ready); |
|
| 193 |
+ Long applicantId = createContact("APPLICANT", "송민지");
|
|
| 194 |
+ mapper.updateAssignments(orgId, applicantId, null, null, null); |
|
| 217 | 195 |
|
| 218 | 196 |
when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); |
| 219 | 197 |
when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); |
... | ... | @@ -226,12 +204,12 @@ |
| 226 | 204 |
} |
| 227 | 205 |
|
| 228 | 206 |
@Test |
| 229 |
- void 담당자정보없이_채널생성하면_실패결과를_돌려준다() throws Exception {
|
|
| 207 |
+ void 담당자배정없이_채널생성하면_실패결과를_돌려준다() throws Exception {
|
|
| 230 | 208 |
mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf()))
|
| 231 | 209 |
.andExpect(status().isOk()) |
| 232 | 210 |
.andExpect(jsonPath("$.mj").value("FAILED"))
|
| 233 | 211 |
.andExpect(jsonPath("$.law").value("FAILED"))
|
| 234 |
- .andExpect(jsonPath("$.message").value(containsString("담당자 정보")));
|
|
| 212 |
+ .andExpect(jsonPath("$.message").value(containsString("신청기관 담당자")));
|
|
| 235 | 213 |
} |
| 236 | 214 |
|
| 237 | 215 |
/** |
... | ... | @@ -242,13 +220,8 @@ |
| 242 | 220 |
*/ |
| 243 | 221 |
@Test |
| 244 | 222 |
void 법률검토만_실패해도_200과_부분실패_결과를_돌려준다() throws Exception {
|
| 245 |
- Organization ready = mapper.findById(orgId); |
|
| 246 |
- ready.setDeptName("데이터정보화팀");
|
|
| 247 |
- ready.setManagerName("송민지");
|
|
| 248 |
- ready.setManagerTitle("과장");
|
|
| 249 |
- ready.setManagerPhone("02-3475-5434");
|
|
| 250 |
- ready.setManagerEmail("ming@arirang.com");
|
|
| 251 |
- mapper.updateContact(ready); |
|
| 223 |
+ Long applicantId = createContact("APPLICANT", "송민지");
|
|
| 224 |
+ mapper.updateAssignments(orgId, applicantId, null, null, null); |
|
| 252 | 225 |
|
| 253 | 226 |
when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); |
| 254 | 227 |
when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); |
--- src/test/java/kr/itn/itnhub/org/OrganizationMapperTest.java
+++ src/test/java/kr/itn/itnhub/org/OrganizationMapperTest.java
... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 |
package kr.itn.itnhub.org; |
| 2 | 2 |
|
| 3 | 3 |
import kr.itn.itnhub.AbstractDbTest; |
| 4 |
+import kr.itn.itnhub.contact.Contact; |
|
| 5 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 4 | 6 |
import org.junit.jupiter.api.BeforeEach; |
| 5 | 7 |
import org.junit.jupiter.api.Test; |
| 6 | 8 |
import org.springframework.beans.factory.annotation.Autowired; |
... | ... | @@ -16,54 +18,61 @@ |
| 16 | 18 |
OrganizationMapper mapper; |
| 17 | 19 |
|
| 18 | 20 |
@Autowired |
| 21 |
+ ContactMapper contactMapper; |
|
| 22 |
+ |
|
| 23 |
+ @Autowired |
|
| 19 | 24 |
JdbcTemplate jdbc; |
| 20 | 25 |
|
| 21 | 26 |
@BeforeEach |
| 22 | 27 |
void clean() {
|
| 23 | 28 |
jdbc.update("delete from organization");
|
| 29 |
+ jdbc.update("delete from contact");
|
|
| 24 | 30 |
} |
| 25 | 31 |
|
| 26 |
- private Organization seedRow(String orgNo, String orgName, String dept) {
|
|
| 32 |
+ private Organization seedRow(String orgNo, String orgName) {
|
|
| 27 | 33 |
Organization org = new Organization(); |
| 28 | 34 |
org.setOrgNo(orgNo); |
| 29 | 35 |
org.setOrgName(orgName); |
| 30 | 36 |
org.setChannelSlug(orgNo); |
| 31 |
- org.setDeptName(dept); |
|
| 32 |
- org.setManagerName("담당자");
|
|
| 33 |
- org.setManagerTitle("과장");
|
|
| 34 |
- org.setManagerPhone("02-0000-0000");
|
|
| 35 |
- org.setManagerEmail("a@b.kr");
|
|
| 36 | 37 |
return org; |
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ private Long insertContact(String category, String name) {
|
|
| 41 |
+ Contact contact = new Contact(); |
|
| 42 |
+ contact.setCategory(category); |
|
| 43 |
+ contact.setName(name); |
|
| 44 |
+ contactMapper.insert(contact); |
|
| 45 |
+ return contact.getId(); |
|
| 37 | 46 |
} |
| 38 | 47 |
|
| 39 | 48 |
@Test |
| 40 | 49 |
void 신규_기관을_넣고_다시_읽는다() {
|
| 41 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 50 |
+ mapper.upsertBySeed(seedRow("001", "국제방송교류재단"));
|
|
| 42 | 51 |
|
| 43 | 52 |
List<Organization> all = mapper.findAll(); |
| 44 | 53 |
|
| 45 | 54 |
assertThat(all).hasSize(1); |
| 46 | 55 |
assertThat(all.get(0).getOrgNo()).isEqualTo("001");
|
| 47 | 56 |
assertThat(all.get(0).getOrgName()).isEqualTo("국제방송교류재단");
|
| 48 |
- assertThat(all.get(0).getStatus()).isEqualTo(OrgStatus.READY); |
|
| 57 |
+ assertThat(all.get(0).getStatus()).isEqualTo(OrgStatus.INFO_PENDING); |
|
| 49 | 58 |
} |
| 50 | 59 |
|
| 51 | 60 |
@Test |
| 52 | 61 |
void 연번이_같아도_기관명이_다르면_별도_행이다() {
|
| 53 |
- mapper.upsertBySeed(seedRow("008", "경찰청", "데이터정책계"));
|
|
| 54 |
- mapper.upsertBySeed(seedRow("008", "경찰청_치안정책연구소", "연구지원과"));
|
|
| 62 |
+ mapper.upsertBySeed(seedRow("008", "경찰청"));
|
|
| 63 |
+ mapper.upsertBySeed(seedRow("008", "경찰청_치안정책연구소"));
|
|
| 55 | 64 |
|
| 56 | 65 |
assertThat(mapper.findAll()).hasSize(2); |
| 57 | 66 |
} |
| 58 | 67 |
|
| 59 | 68 |
@Test |
| 60 | 69 |
void 시드_재실행은_기존_채널ID를_지우지_않는다() {
|
| 61 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 70 |
+ mapper.upsertBySeed(seedRow("001", "국제방송교류재단"));
|
|
| 62 | 71 |
Long id = mapper.findAll().get(0).getId(); |
| 63 | 72 |
mapper.updateChannelIdMj(id, "chan-mj"); |
| 64 | 73 |
mapper.updateChannelIdLaw(id, "chan-law"); |
| 65 | 74 |
|
| 66 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 75 |
+ mapper.upsertBySeed(seedRow("001", "국제방송교류재단"));
|
|
| 67 | 76 |
|
| 68 | 77 |
Organization after = mapper.findById(id); |
| 69 | 78 |
assertThat(after.getChannelIdMj()).isEqualTo("chan-mj");
|
... | ... | @@ -72,38 +81,10 @@ |
| 72 | 81 |
} |
| 73 | 82 |
|
| 74 | 83 |
@Test |
| 75 |
- void 시드_재실행은_사람이_입력한_값을_덮어쓰지_않는다() {
|
|
| 76 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 77 |
- Organization org = mapper.findAll().get(0); |
|
| 78 |
- org.setDeptName("사람이 고친 부서");
|
|
| 79 |
- mapper.updateContact(org); |
|
| 80 |
- |
|
| 81 |
- Organization reseeded = seedRow("001", "국제방송교류재단", "시트에 있던 부서");
|
|
| 82 |
- mapper.upsertBySeed(reseeded); |
|
| 83 |
- |
|
| 84 |
- assertThat(mapper.findById(org.getId()).getDeptName()) |
|
| 85 |
- .isEqualTo("사람이 고친 부서");
|
|
| 86 |
- } |
|
| 87 |
- |
|
| 88 |
- @Test |
|
| 89 |
- void 시드_재실행은_비어_있던_칸만_채운다() {
|
|
| 90 |
- Organization blank = new Organization(); |
|
| 91 |
- blank.setOrgNo("009");
|
|
| 92 |
- blank.setOrgName("서울도서관");
|
|
| 93 |
- blank.setChannelSlug("009");
|
|
| 94 |
- mapper.upsertBySeed(blank); |
|
| 95 |
- |
|
| 96 |
- mapper.upsertBySeed(seedRow("009", "서울도서관", "정보서비스과"));
|
|
| 97 |
- |
|
| 98 |
- Organization after = mapper.findAll().get(0); |
|
| 99 |
- assertThat(after.getDeptName()).isEqualTo("정보서비스과");
|
|
| 100 |
- assertThat(after.getManagerEmail()).isEqualTo("a@b.kr");
|
|
| 101 |
- } |
|
| 102 |
- |
|
| 103 |
- @Test |
|
| 104 | 84 |
void 채널ID를_한쪽만_저장할_수_있다() {
|
| 105 |
- mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀"));
|
|
| 85 |
+ mapper.upsertBySeed(seedRow("002", "세종학당재단"));
|
|
| 106 | 86 |
Long id = mapper.findAll().get(0).getId(); |
| 87 |
+ mapper.updateAssignments(id, insertContact("APPLICANT", "담당자"), null, null, null);
|
|
| 107 | 88 |
|
| 108 | 89 |
mapper.updateChannelIdMj(id, "only-mj"); |
| 109 | 90 |
|
... | ... | @@ -114,51 +95,73 @@ |
| 114 | 95 |
} |
| 115 | 96 |
|
| 116 | 97 |
@Test |
| 117 |
- void 문정원_담당자와_변호사_정보가_저장되고_그대로_읽힌다() {
|
|
| 118 |
- mapper.upsertBySeed(seedRow("003", "한국문학번역원", "정보관리팀"));
|
|
| 98 |
+ void 담당자_3종을_배정하면_조인된_담당자_정보가_그대로_읽힌다() {
|
|
| 99 |
+ mapper.upsertBySeed(seedRow("003", "한국문학번역원"));
|
|
| 119 | 100 |
Organization org = mapper.findAll().get(0); |
| 120 | 101 |
|
| 121 |
- org.setMjDeptName("문화체육관광부 저작권정책과");
|
|
| 122 |
- org.setMjManagerName("김문정");
|
|
| 123 |
- org.setMjManagerPhone("02-1234-5678");
|
|
| 124 |
- org.setMjManagerEmail("mj@mcst.go.kr");
|
|
| 125 |
- org.setLawyerName("이변호");
|
|
| 126 |
- org.setLawyerPhone("02-9876-5432");
|
|
| 127 |
- org.setLawyerEmail("lawyer@lawfirm.kr");
|
|
| 128 |
- org.setLawyerAssignedDate("2026-07-21");
|
|
| 129 |
- mapper.updateContact(org); |
|
| 102 |
+ Long applicantId = insertContact("APPLICANT", "송민지");
|
|
| 103 |
+ Long mjId = insertContact("MJ", "김문정");
|
|
| 104 |
+ Long lawyerId = insertContact("LAWYER", "이변호");
|
|
| 105 |
+ |
|
| 106 |
+ mapper.updateAssignments(org.getId(), applicantId, mjId, lawyerId, "2026-07-21"); |
|
| 130 | 107 |
|
| 131 | 108 |
Organization after = mapper.findById(org.getId()); |
| 132 |
- assertThat(after.getMjDeptName()).isEqualTo("문화체육관광부 저작권정책과");
|
|
| 133 |
- assertThat(after.getMjManagerName()).isEqualTo("김문정");
|
|
| 134 |
- assertThat(after.getMjManagerPhone()).isEqualTo("02-1234-5678");
|
|
| 135 |
- assertThat(after.getMjManagerEmail()).isEqualTo("mj@mcst.go.kr");
|
|
| 136 |
- assertThat(after.getLawyerName()).isEqualTo("이변호");
|
|
| 137 |
- assertThat(after.getLawyerPhone()).isEqualTo("02-9876-5432");
|
|
| 138 |
- assertThat(after.getLawyerEmail()).isEqualTo("lawyer@lawfirm.kr");
|
|
| 109 |
+ assertThat(after.getApplicantContactId()).isEqualTo(applicantId); |
|
| 110 |
+ assertThat(after.getMjContactId()).isEqualTo(mjId); |
|
| 111 |
+ assertThat(after.getLawyerContactId()).isEqualTo(lawyerId); |
|
| 139 | 112 |
assertThat(after.getLawyerAssignedDate()).isEqualTo("2026-07-21");
|
| 113 |
+ |
|
| 114 |
+ assertThat(after.getApplicant().getName()).isEqualTo("송민지");
|
|
| 115 |
+ assertThat(after.getApplicant().getCategory()).isEqualTo("APPLICANT");
|
|
| 116 |
+ assertThat(after.getMj().getName()).isEqualTo("김문정");
|
|
| 117 |
+ assertThat(after.getLawyer().getName()).isEqualTo("이변호");
|
|
| 118 |
+ assertThat(after.getStatus()).isEqualTo(OrgStatus.READY); |
|
| 140 | 119 |
} |
| 141 | 120 |
|
| 142 | 121 |
@Test |
| 143 |
- void 시드_재실행은_문정원_변호사_정보를_지우지_않는다() {
|
|
| 144 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 122 |
+ void 배정되지_않은_역할은_담당자가_null이다() {
|
|
| 123 |
+ mapper.upsertBySeed(seedRow("004", "예시기관"));
|
|
| 145 | 124 |
Organization org = mapper.findAll().get(0); |
| 146 |
- org.setMjManagerName("김문정");
|
|
| 147 |
- org.setLawyerName("이변호");
|
|
| 148 |
- mapper.updateContact(org); |
|
| 149 | 125 |
|
| 150 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 126 |
+ Long applicantId = insertContact("APPLICANT", "담당자");
|
|
| 127 |
+ mapper.updateAssignments(org.getId(), applicantId, null, null, null); |
|
| 151 | 128 |
|
| 152 | 129 |
Organization after = mapper.findById(org.getId()); |
| 153 |
- assertThat(after.getMjManagerName()).isEqualTo("김문정");
|
|
| 154 |
- assertThat(after.getLawyerName()).isEqualTo("이변호");
|
|
| 130 |
+ assertThat(after.getApplicant()).isNotNull(); |
|
| 131 |
+ assertThat(after.getMj()).isNull(); |
|
| 132 |
+ assertThat(after.getLawyer()).isNull(); |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ @Test |
|
| 136 |
+ void 배정_해제는_null로_넘기면_반영된다() {
|
|
| 137 |
+ mapper.upsertBySeed(seedRow("005", "예시기관2"));
|
|
| 138 |
+ Organization org = mapper.findAll().get(0); |
|
| 139 |
+ |
|
| 140 |
+ Long applicantId = insertContact("APPLICANT", "담당자");
|
|
| 141 |
+ mapper.updateAssignments(org.getId(), applicantId, null, null, null); |
|
| 142 |
+ mapper.updateAssignments(org.getId(), null, null, null, null); |
|
| 143 |
+ |
|
| 144 |
+ Organization after = mapper.findById(org.getId()); |
|
| 145 |
+ assertThat(after.getApplicantContactId()).isNull(); |
|
| 146 |
+ assertThat(after.getApplicant()).isNull(); |
|
| 147 |
+ assertThat(after.getStatus()).isEqualTo(OrgStatus.INFO_PENDING); |
|
| 148 |
+ } |
|
| 149 |
+ |
|
| 150 |
+ @Test |
|
| 151 |
+ void 연번과_기관명으로_단건_조회할_수_있다() {
|
|
| 152 |
+ mapper.upsertBySeed(seedRow("006", "예시기관3"));
|
|
| 153 |
+ |
|
| 154 |
+ Organization found = mapper.findByOrgNoAndOrgName("006", "예시기관3");
|
|
| 155 |
+ |
|
| 156 |
+ assertThat(found).isNotNull(); |
|
| 157 |
+ assertThat(found.getOrgName()).isEqualTo("예시기관3");
|
|
| 155 | 158 |
} |
| 156 | 159 |
|
| 157 | 160 |
@Test |
| 158 | 161 |
void 목록은_연번_오름차순이다() {
|
| 159 |
- mapper.upsertBySeed(seedRow("010", "국립아시아문화전당", "기획운영과"));
|
|
| 160 |
- mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀"));
|
|
| 161 |
- mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
|
|
| 162 |
+ mapper.upsertBySeed(seedRow("010", "국립아시아문화전당"));
|
|
| 163 |
+ mapper.upsertBySeed(seedRow("002", "세종학당재단"));
|
|
| 164 |
+ mapper.upsertBySeed(seedRow("001", "국제방송교류재단"));
|
|
| 162 | 165 |
|
| 163 | 166 |
assertThat(mapper.findAll()) |
| 164 | 167 |
.extracting(Organization::getOrgNo) |
--- src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
+++ src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
... | ... | @@ -6,20 +6,17 @@ |
| 6 | 6 |
|
| 7 | 7 |
class OrganizationStatusTest {
|
| 8 | 8 |
|
| 9 |
- private Organization withContact() {
|
|
| 9 |
+ private Organization withApplicant() {
|
|
| 10 | 10 |
Organization org = new Organization(); |
| 11 | 11 |
org.setOrgNo("001");
|
| 12 | 12 |
org.setOrgName("국제방송교류재단");
|
| 13 | 13 |
org.setChannelSlug("001");
|
| 14 |
- org.setDeptName("데이터정보화팀");
|
|
| 15 |
- org.setManagerName("송민지");
|
|
| 16 |
- org.setManagerPhone("02-3475-5434");
|
|
| 17 |
- org.setManagerEmail("ming@arirang.com");
|
|
| 14 |
+ org.setApplicantContactId(1L); |
|
| 18 | 15 |
return org; |
| 19 | 16 |
} |
| 20 | 17 |
|
| 21 | 18 |
@Test |
| 22 |
- void 담당자정보가_없으면_정보대기다() {
|
|
| 19 |
+ void 신청기관_담당자가_배정되지_않으면_정보대기다() {
|
|
| 23 | 20 |
Organization org = new Organization(); |
| 24 | 21 |
org.setOrgNo("008");
|
| 25 | 22 |
org.setOrgName("경찰청_치안정책연구소");
|
... | ... | @@ -28,21 +25,22 @@ |
| 28 | 25 |
} |
| 29 | 26 |
|
| 30 | 27 |
@Test |
| 31 |
- void 담당자정보가_다_차면_생성가능이다() {
|
|
| 32 |
- assertThat(withContact().getStatus()).isEqualTo(OrgStatus.READY); |
|
| 28 |
+ void 신청기관_담당자가_배정되면_생성가능이다() {
|
|
| 29 |
+ assertThat(withApplicant().getStatus()).isEqualTo(OrgStatus.READY); |
|
| 33 | 30 |
} |
| 34 | 31 |
|
| 35 | 32 |
@Test |
| 36 |
- void 직급은_없어도_생성가능이다() {
|
|
| 37 |
- Organization org = withContact(); |
|
| 38 |
- org.setManagerTitle(null); |
|
| 33 |
+ void 문정원_변호사_배정이_없어도_신청기관_담당자만_있으면_생성가능이다() {
|
|
| 34 |
+ Organization org = withApplicant(); |
|
| 35 |
+ org.setMjContactId(null); |
|
| 36 |
+ org.setLawyerContactId(null); |
|
| 39 | 37 |
|
| 40 | 38 |
assertThat(org.getStatus()).isEqualTo(OrgStatus.READY); |
| 41 | 39 |
} |
| 42 | 40 |
|
| 43 | 41 |
@Test |
| 44 | 42 |
void 채널이_한쪽만_있으면_부분생성이다() {
|
| 45 |
- Organization org = withContact(); |
|
| 43 |
+ Organization org = withApplicant(); |
|
| 46 | 44 |
org.setChannelIdMj("chan-mj");
|
| 47 | 45 |
|
| 48 | 46 |
assertThat(org.getStatus()).isEqualTo(OrgStatus.PARTIAL); |
... | ... | @@ -50,7 +48,7 @@ |
| 50 | 48 |
|
| 51 | 49 |
@Test |
| 52 | 50 |
void 채널이_둘_다_있으면_운영중이다() {
|
| 53 |
- Organization org = withContact(); |
|
| 51 |
+ Organization org = withApplicant(); |
|
| 54 | 52 |
org.setChannelIdMj("chan-mj");
|
| 55 | 53 |
org.setChannelIdLaw("chan-law");
|
| 56 | 54 |
|
... | ... | @@ -58,7 +56,7 @@ |
| 58 | 56 |
} |
| 59 | 57 |
|
| 60 | 58 |
@Test |
| 61 |
- void 담당자정보가_비어도_채널이_있으면_운영중이다() {
|
|
| 59 |
+ void 신청기관_담당자_배정이_없어도_채널이_있으면_운영중이다() {
|
|
| 62 | 60 |
Organization org = new Organization(); |
| 63 | 61 |
org.setOrgNo("008");
|
| 64 | 62 |
org.setOrgName("경찰청_치안정책연구소");
|
... | ... | @@ -69,10 +67,11 @@ |
| 69 | 67 |
} |
| 70 | 68 |
|
| 71 | 69 |
@Test |
| 72 |
- void 공백만_있는_값은_없는_것으로_본다() {
|
|
| 73 |
- Organization org = withContact(); |
|
| 74 |
- org.setManagerEmail(" ");
|
|
| 70 |
+ void hasApplicantAssigned은_배정된_담당자ID_유무만_본다() {
|
|
| 71 |
+ Organization org = withApplicant(); |
|
| 72 |
+ assertThat(org.hasApplicantAssigned()).isTrue(); |
|
| 75 | 73 |
|
| 76 |
- assertThat(org.getStatus()).isEqualTo(OrgStatus.INFO_PENDING); |
|
| 74 |
+ org.setApplicantContactId(null); |
|
| 75 |
+ assertThat(org.hasApplicantAssigned()).isFalse(); |
|
| 77 | 76 |
} |
| 78 | 77 |
} |
--- src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
+++ src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 |
package kr.itn.itnhub.provision; |
| 2 | 2 |
|
| 3 | 3 |
import kr.itn.itnhub.AbstractDbTest; |
| 4 |
+import kr.itn.itnhub.contact.Contact; |
|
| 5 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 4 | 6 |
import kr.itn.itnhub.mattermost.MattermostClient; |
| 5 | 7 |
import kr.itn.itnhub.mattermost.MattermostException; |
| 6 | 8 |
import kr.itn.itnhub.org.OrgStatus; |
... | ... | @@ -33,6 +35,9 @@ |
| 33 | 35 |
OrganizationMapper mapper; |
| 34 | 36 |
|
| 35 | 37 |
@Autowired |
| 38 |
+ ContactMapper contactMapper; |
|
| 39 |
+ |
|
| 40 |
+ @Autowired |
|
| 36 | 41 |
JdbcTemplate jdbc; |
| 37 | 42 |
|
| 38 | 43 |
@MockBean |
... | ... | @@ -43,18 +48,25 @@ |
| 43 | 48 |
@BeforeEach |
| 44 | 49 |
void setUp() {
|
| 45 | 50 |
jdbc.update("delete from organization");
|
| 51 |
+ jdbc.update("delete from contact");
|
|
| 46 | 52 |
|
| 47 | 53 |
Organization org = new Organization(); |
| 48 | 54 |
org.setOrgNo("001");
|
| 49 | 55 |
org.setOrgName("국제방송교류재단");
|
| 50 | 56 |
org.setChannelSlug("001");
|
| 51 |
- org.setDeptName("데이터정보화팀");
|
|
| 52 |
- org.setManagerName("송민지");
|
|
| 53 |
- org.setManagerPhone("02-3475-5434");
|
|
| 54 |
- org.setManagerEmail("ming@arirang.com");
|
|
| 55 | 57 |
mapper.upsertBySeed(org); |
| 56 | 58 |
|
| 57 | 59 |
orgId = mapper.findAll().get(0).getId(); |
| 60 |
+ assignApplicant(orgId, "송민지"); |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ private Long assignApplicant(Long id, String name) {
|
|
| 64 |
+ Contact contact = new Contact(); |
|
| 65 |
+ contact.setCategory("APPLICANT");
|
|
| 66 |
+ contact.setName(name); |
|
| 67 |
+ contactMapper.insert(contact); |
|
| 68 |
+ mapper.updateAssignments(id, contact.getId(), null, null, null); |
|
| 69 |
+ return contact.getId(); |
|
| 58 | 70 |
} |
| 59 | 71 |
|
| 60 | 72 |
@Test |
... | ... | @@ -173,15 +185,12 @@ |
| 173 | 185 |
special.setOrgNo("008");
|
| 174 | 186 |
special.setOrgName("경찰청_치안정책연구소");
|
| 175 | 187 |
special.setChannelSlug("008-policy");
|
| 176 |
- special.setDeptName("연구지원과");
|
|
| 177 |
- special.setManagerName("담당자");
|
|
| 178 |
- special.setManagerPhone("02-0000-0000");
|
|
| 179 |
- special.setManagerEmail("a@police.go.kr");
|
|
| 180 | 188 |
mapper.upsertBySeed(special); |
| 181 | 189 |
|
| 182 | 190 |
Long specialId = mapper.findAll().stream() |
| 183 | 191 |
.filter(o -> "경찰청_치안정책연구소".equals(o.getOrgName())) |
| 184 | 192 |
.findFirst().orElseThrow().getId(); |
| 193 |
+ assignApplicant(specialId, "담당자"); |
|
| 185 | 194 |
|
| 186 | 195 |
when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); |
| 187 | 196 |
when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); |
... | ... | @@ -196,7 +205,7 @@ |
| 196 | 205 |
} |
| 197 | 206 |
|
| 198 | 207 |
@Test |
| 199 |
- void 담당자정보가_없으면_생성하지_않는다() {
|
|
| 208 |
+ void 신청기관_담당자가_배정되지_않으면_생성하지_않는다() {
|
|
| 200 | 209 |
Organization pending = new Organization(); |
| 201 | 210 |
pending.setOrgNo("099");
|
| 202 | 211 |
pending.setOrgName("정보없는기관");
|
... | ... | @@ -211,7 +220,7 @@ |
| 211 | 220 |
|
| 212 | 221 |
assertThat(result.mj()).isEqualTo(ProvisionOutcome.FAILED); |
| 213 | 222 |
assertThat(result.law()).isEqualTo(ProvisionOutcome.FAILED); |
| 214 |
- assertThat(result.message()).contains("담당자 정보");
|
|
| 223 |
+ assertThat(result.message()).contains("신청기관 담당자");
|
|
| 215 | 224 |
|
| 216 | 225 |
verify(mattermost, never()).createPrivateChannel(anyString(), anyString()); |
| 217 | 226 |
} |
--- src/test/java/kr/itn/itnhub/seed/SeedServiceTest.java
+++ src/test/java/kr/itn/itnhub/seed/SeedServiceTest.java
... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 |
package kr.itn.itnhub.seed; |
| 2 | 2 |
|
| 3 | 3 |
import kr.itn.itnhub.AbstractDbTest; |
| 4 |
+import kr.itn.itnhub.contact.Contact; |
|
| 5 |
+import kr.itn.itnhub.contact.ContactMapper; |
|
| 4 | 6 |
import kr.itn.itnhub.org.OrgStatus; |
| 5 | 7 |
import kr.itn.itnhub.org.Organization; |
| 6 | 8 |
import kr.itn.itnhub.org.OrganizationMapper; |
... | ... | @@ -27,11 +29,15 @@ |
| 27 | 29 |
OrganizationMapper mapper; |
| 28 | 30 |
|
| 29 | 31 |
@Autowired |
| 32 |
+ ContactMapper contactMapper; |
|
| 33 |
+ |
|
| 34 |
+ @Autowired |
|
| 30 | 35 |
JdbcTemplate jdbc; |
| 31 | 36 |
|
| 32 | 37 |
@BeforeEach |
| 33 | 38 |
void clean() {
|
| 34 | 39 |
jdbc.update("delete from organization");
|
| 40 |
+ jdbc.update("delete from contact");
|
|
| 35 | 41 |
} |
| 36 | 42 |
|
| 37 | 43 |
private byte[] workbook(String[]... dataRows) throws Exception {
|
... | ... | @@ -128,4 +134,59 @@ |
| 128 | 134 |
assertThat(all).extracting(Organization::getStatus) |
| 129 | 135 |
.containsExactly(OrgStatus.READY, OrgStatus.INFO_PENDING); |
| 130 | 136 |
} |
| 137 |
+ |
|
| 138 |
+ @Test |
|
| 139 |
+ void 시트에_담당자명이_있으면_신청기관_담당자_연락처를_만들어_연결한다() throws Exception {
|
|
| 140 |
+ byte[] xlsx = workbook( |
|
| 141 |
+ new String[]{"001", "00", "국제방송교류재단", "데이터정보화팀", "송민지", "과장", "02-1", "a@b.kr"});
|
|
| 142 |
+ |
|
| 143 |
+ seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 144 |
+ |
|
| 145 |
+ Organization org = mapper.findAll().get(0); |
|
| 146 |
+ assertThat(org.getApplicantContactId()).isNotNull(); |
|
| 147 |
+ |
|
| 148 |
+ Contact applicant = org.getApplicant(); |
|
| 149 |
+ assertThat(applicant.getCategory()).isEqualTo("APPLICANT");
|
|
| 150 |
+ assertThat(applicant.getName()).isEqualTo("송민지");
|
|
| 151 |
+ assertThat(applicant.getAffiliation()).isEqualTo("국제방송교류재단");
|
|
| 152 |
+ assertThat(applicant.getDeptName()).isEqualTo("데이터정보화팀");
|
|
| 153 |
+ assertThat(applicant.getTitle()).isEqualTo("과장");
|
|
| 154 |
+ assertThat(applicant.getPhone()).isEqualTo("02-1");
|
|
| 155 |
+ assertThat(applicant.getEmail()).isEqualTo("a@b.kr");
|
|
| 156 |
+ } |
|
| 157 |
+ |
|
| 158 |
+ @Test |
|
| 159 |
+ void 재시드는_이미_연결된_신청기관_담당자를_중복생성하거나_다시_연결하지_않는다() throws Exception {
|
|
| 160 |
+ byte[] xlsx = workbook( |
|
| 161 |
+ new String[]{"001", "00", "국제방송교류재단", "팀", "송민지", "과장", "02-1", "a@b.kr"});
|
|
| 162 |
+ |
|
| 163 |
+ seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 164 |
+ Long firstContactId = mapper.findAll().get(0).getApplicantContactId(); |
|
| 165 |
+ |
|
| 166 |
+ seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 167 |
+ |
|
| 168 |
+ Organization after = mapper.findAll().get(0); |
|
| 169 |
+ assertThat(after.getApplicantContactId()).isEqualTo(firstContactId); |
|
| 170 |
+ assertThat(jdbc.queryForObject("select count(*) from contact", Integer.class)).isEqualTo(1);
|
|
| 171 |
+ } |
|
| 172 |
+ |
|
| 173 |
+ @Test |
|
| 174 |
+ void 이미_다른_담당자로_배정된_기관은_재시드가_배정을_바꾸지_않는다() throws Exception {
|
|
| 175 |
+ byte[] xlsx = workbook( |
|
| 176 |
+ new String[]{"001", "00", "국제방송교류재단", "팀", "송민지", "과장", "02-1", "a@b.kr"});
|
|
| 177 |
+ seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 178 |
+ Long id = mapper.findAll().get(0).getId(); |
|
| 179 |
+ |
|
| 180 |
+ Contact manual = new Contact(); |
|
| 181 |
+ manual.setCategory("APPLICANT");
|
|
| 182 |
+ manual.setName("사람이 직접 배정한 담당자");
|
|
| 183 |
+ contactMapper.insert(manual); |
|
| 184 |
+ mapper.updateAssignments(id, manual.getId(), null, null, null); |
|
| 185 |
+ |
|
| 186 |
+ seedService.seed(new ByteArrayInputStream(xlsx)); |
|
| 187 |
+ |
|
| 188 |
+ Organization after = mapper.findById(id); |
|
| 189 |
+ assertThat(after.getApplicantContactId()).isEqualTo(manual.getId()); |
|
| 190 |
+ assertThat(after.getApplicant().getName()).isEqualTo("사람이 직접 배정한 담당자");
|
|
| 191 |
+ } |
|
| 131 | 192 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?