diff --git a/src/main/java/com/bmilab/backend/domain/project/controller/ProjectApi.java b/src/main/java/com/bmilab/backend/domain/project/controller/ProjectApi.java index 88d2ebac..62b57490 100644 --- a/src/main/java/com/bmilab/backend/domain/project/controller/ProjectApi.java +++ b/src/main/java/com/bmilab/backend/domain/project/controller/ProjectApi.java @@ -234,17 +234,17 @@ ResponseEntity searchProject( @RequestParam(required = false) String keyword ); - @Operation(summary = "외부교수 목록 조회", description = "사용자가 연구 생성/수정할 때 외부교수 목록을 조회하는 GET API") + @Operation(summary = "외부교수 목록 조회", description = "사용자가 연구 생성/수정할 때 외부교수 목록을 조회하는 GET API. 이름, 기관, 부서로 통합 검색 가능") @ApiResponses( value = { @ApiResponse( responseCode = "200", - description = "내 연구 조회 성공" + description = "외부교수 조회 성공" ) } ) ResponseEntity getExternalProfessors( - @RequestParam(required = false) String name + @RequestParam(required = false) String keyword ); @Operation(summary = "사용자 연구 조회", description = "사용자 ID로 사용자가 참여하는 연구 목록을 조회하는 GET API") diff --git a/src/main/java/com/bmilab/backend/domain/project/controller/ProjectController.java b/src/main/java/com/bmilab/backend/domain/project/controller/ProjectController.java index 933430da..3be86069 100644 --- a/src/main/java/com/bmilab/backend/domain/project/controller/ProjectController.java +++ b/src/main/java/com/bmilab/backend/domain/project/controller/ProjectController.java @@ -168,10 +168,9 @@ public ResponseEntity searchProject( @GetMapping("/external-professors") public ResponseEntity getExternalProfessors( - @RequestParam(required = false) String name + @RequestParam(required = false) String keyword ) { - - return ResponseEntity.ok(projectService.getExternalProfessorsByName(name)); + return ResponseEntity.ok(projectService.getExternalProfessorsByKeyword(keyword)); } diff --git a/src/main/java/com/bmilab/backend/domain/project/repository/ExternalProfessorRepository.java b/src/main/java/com/bmilab/backend/domain/project/repository/ExternalProfessorRepository.java index 7743ac82..10b93157 100644 --- a/src/main/java/com/bmilab/backend/domain/project/repository/ExternalProfessorRepository.java +++ b/src/main/java/com/bmilab/backend/domain/project/repository/ExternalProfessorRepository.java @@ -10,10 +10,12 @@ public interface ExternalProfessorRepository extends JpaRepository findAllByOrderByNameAsc(); - @Query("select ex from ExternalProfessor ex where :name is null " - + "or :name = '' " - + "or ex.name like concat('%', :name, '%')") - List findAllByNameContaining(String name); + @Query("select ex from ExternalProfessor ex where :keyword is null " + + "or :keyword = '' " + + "or ex.name like concat('%', :keyword, '%') " + + "or ex.organization like concat('%', :keyword, '%') " + + "or ex.department like concat('%', :keyword, '%')") + List findAllByKeyword(String keyword); boolean existsByNameAndOrganizationAndDepartmentAndPosition(String name, String organization, String department, String position); } diff --git a/src/main/java/com/bmilab/backend/domain/project/service/ProjectService.java b/src/main/java/com/bmilab/backend/domain/project/service/ProjectService.java index f307d4f1..5e9f7221 100644 --- a/src/main/java/com/bmilab/backend/domain/project/service/ProjectService.java +++ b/src/main/java/com/bmilab/backend/domain/project/service/ProjectService.java @@ -528,8 +528,8 @@ public void deleteExternalProfessor(Long professorId) { externalProfessorRepository.delete(externalProfessor); } - public ExternalProfessorFindAllResponse getExternalProfessorsByName(String name) { - List externalProfessors = externalProfessorRepository.findAllByNameContaining(name); + public ExternalProfessorFindAllResponse getExternalProfessorsByKeyword(String keyword) { + List externalProfessors = externalProfessorRepository.findAllByKeyword(keyword); return ExternalProfessorFindAllResponse.of(externalProfessors); } diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreateJournalRequest.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreateJournalRequest.java index fccee399..b34ee51f 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreateJournalRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreateJournalRequest.java @@ -10,6 +10,10 @@ public record CreateJournalRequest( @NotBlank(message = "저널명은 필수입니다.") String journalName, + @Schema(description = "년도", example = "2025") + @NotNull(message = "년도는 필수입니다.") + Integer year, + @Schema(description = "저널 분류", example = "SCI") @NotNull(message = "저널 분류는 필수입니다.") JournalCategory category, diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreatePaperRequest.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreatePaperRequest.java index 750cac03..335a9edb 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreatePaperRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/CreatePaperRequest.java @@ -2,6 +2,7 @@ import com.bmilab.backend.domain.research.paper.enums.ProfessorRole; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; @@ -11,6 +12,7 @@ public record CreatePaperRequest( @Schema(description = "Accept 날짜", example = "2025-10-20") LocalDate acceptDate, @Schema(description = "Publish 날짜", example = "2025-12-05") + @NotNull(message = "Publish 날짜는 필수입니다.") LocalDate publishDate, @Schema(description = "저널 ID", example = "1") Long journalId, diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdateJournalRequest.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdateJournalRequest.java index 0e4c7a75..04a51e96 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdateJournalRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdateJournalRequest.java @@ -10,6 +10,10 @@ public record UpdateJournalRequest( @NotBlank(message = "저널명은 필수입니다.") String journalName, + @Schema(description = "년도", example = "2025") + @NotNull(message = "년도는 필수입니다.") + Integer year, + @Schema(description = "저널 분류", example = "SCI") @NotNull(message = "저널 분류는 필수입니다.") JournalCategory category, diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdatePaperRequest.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdatePaperRequest.java index 59abc69b..65d251d8 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdatePaperRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/request/UpdatePaperRequest.java @@ -2,6 +2,7 @@ import com.bmilab.backend.domain.research.paper.enums.ProfessorRole; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; @@ -11,6 +12,7 @@ public record UpdatePaperRequest( @Schema(description = "Accept 날짜", example = "2025-10-21") LocalDate acceptDate, @Schema(description = "Publish 날짜", example = "2025-12-06") + @NotNull(message = "Publish 날짜는 필수입니다.") LocalDate publishDate, @Schema(description = "저널 ID", example = "1") Long journalId, diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalResponse.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalResponse.java index be3bb608..196ba7a8 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalResponse.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalResponse.java @@ -11,6 +11,9 @@ public record JournalResponse( @Schema(description = "저널명") String journalName, + @Schema(description = "년도") + Integer year, + @Schema(description = "저널 분류") JournalCategory category, @@ -39,9 +42,13 @@ public record JournalResponse( String issue ) { public static JournalResponse from(Journal journal) { + if (journal == null) { + return null; + } return new JournalResponse( journal.getId(), journal.getJournalName(), + journal.getYear(), journal.getCategory(), journal.getPublisher(), journal.getPublishCountry(), diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalSummaryResponse.java b/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalSummaryResponse.java index b60425f1..3b209196 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalSummaryResponse.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/dto/response/JournalSummaryResponse.java @@ -11,6 +11,9 @@ public record JournalSummaryResponse( @Schema(description = "저널명") String journalName, + @Schema(description = "년도") + Integer year, + @Schema(description = "저널 분류") JournalCategory category, @@ -42,6 +45,7 @@ public static JournalSummaryResponse from(Journal journal) { return new JournalSummaryResponse( journal.getId(), journal.getJournalName(), + journal.getYear(), journal.getCategory(), journal.getPublisher(), journal.getPublishCountry(), diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/entity/Journal.java b/src/main/java/com/bmilab/backend/domain/research/paper/entity/Journal.java index 79645db8..c1fbcc5e 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/entity/Journal.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/entity/Journal.java @@ -9,7 +9,9 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "research_journals") +@Table(name = "research_journals", uniqueConstraints = { + @UniqueConstraint(columnNames = {"journal_name", "year"}) +}) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Journal extends BaseTimeEntity { @@ -18,9 +20,12 @@ public class Journal extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(name = "journal_name", nullable = false) private String journalName; + @Column(nullable = false) + private Integer year; + @Enumerated(EnumType.STRING) @Column(nullable = false) private JournalCategory category; @@ -49,10 +54,11 @@ public class Journal extends BaseTimeEntity { private String issue; @Builder - public Journal(String journalName, JournalCategory category, String publisher, + public Journal(String journalName, Integer year, JournalCategory category, String publisher, String publishCountry, String isbn, String issn, String eissn, String jif, String jcrRank, String issue) { this.journalName = journalName; + this.year = year; this.category = category; this.publisher = publisher; this.publishCountry = publishCountry; @@ -64,10 +70,11 @@ public Journal(String journalName, JournalCategory category, String publisher, this.issue = issue; } - public void update(String journalName, JournalCategory category, String publisher, + public void update(String journalName, Integer year, JournalCategory category, String publisher, String publishCountry, String isbn, String issn, String eissn, String jif, String jcrRank, String issue) { this.journalName = journalName; + this.year = year; this.category = category; this.publisher = publisher; this.publishCountry = publishCountry; diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/entity/Paper.java b/src/main/java/com/bmilab/backend/domain/research/paper/entity/Paper.java index d239bbf0..228812a8 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/entity/Paper.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/entity/Paper.java @@ -24,13 +24,12 @@ public class Paper extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) private LocalDate acceptDate; private LocalDate publishDate; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "journal_id", nullable = false) + @JoinColumn(name = "journal_id") private Journal journal; @Column(nullable = false) diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/exception/PaperErrorCode.java b/src/main/java/com/bmilab/backend/domain/research/paper/exception/PaperErrorCode.java index 315ff175..f0b22ce4 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/exception/PaperErrorCode.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/exception/PaperErrorCode.java @@ -12,6 +12,7 @@ public enum PaperErrorCode implements ErrorCode { PAPER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 논문을 찾을 수 없습니다."), JOURNAL_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 저널을 찾을 수 없습니다."), PAPER_ACCESS_DENIED(HttpStatus.FORBIDDEN, "해당 논문에 접근할 권한이 없습니다."), + DUPLICATE_CORRESPONDING_AUTHOR(HttpStatus.BAD_REQUEST, "중복된 교신저자가 존재합니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/service/JournalService.java b/src/main/java/com/bmilab/backend/domain/research/paper/service/JournalService.java index 06497968..504bfa01 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/service/JournalService.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/service/JournalService.java @@ -27,6 +27,7 @@ public class JournalService { public JournalResponse createJournal(CreateJournalRequest dto) { Journal newJournal = Journal.builder() .journalName(dto.journalName()) + .year(dto.year()) .category(dto.category()) .publisher(dto.publisher()) .publishCountry(dto.publishCountry()) @@ -60,6 +61,7 @@ public JournalResponse updateJournal(Long journalId, UpdateJournalRequest dto) { .orElseThrow(() -> new ApiException(PaperErrorCode.JOURNAL_NOT_FOUND)); journal.update( dto.journalName(), + dto.year(), dto.category(), dto.publisher(), dto.publishCountry(), diff --git a/src/main/java/com/bmilab/backend/domain/research/paper/service/PaperService.java b/src/main/java/com/bmilab/backend/domain/research/paper/service/PaperService.java index 84162ff2..0e09388c 100644 --- a/src/main/java/com/bmilab/backend/domain/research/paper/service/PaperService.java +++ b/src/main/java/com/bmilab/backend/domain/research/paper/service/PaperService.java @@ -55,8 +55,9 @@ public class PaperService { private final AuthorSyncService authorSyncService; public PaperResponse createPaper(CreatePaperRequest dto) { - Journal journal = journalRepository.findById(dto.journalId()) - .orElseThrow(() -> new ApiException(PaperErrorCode.JOURNAL_NOT_FOUND)); + Journal journal = dto.journalId() != null + ? journalRepository.findById(dto.journalId()).orElseThrow(() -> new ApiException(PaperErrorCode.JOURNAL_NOT_FOUND)) + : null; Task task = dto.taskId() != null ? taskRepository.findById(dto.taskId()).orElseThrow(() -> new ApiException(TaskErrorCode.TASK_NOT_FOUND)) : null; @@ -109,6 +110,12 @@ public PaperResponse createPaper(CreatePaperRequest dto) { List externalProfessorIds = dto.correspondingAuthors().stream() .map(CreatePaperRequest.PaperCorrespondingAuthorRequest::externalProfessorId) .collect(Collectors.toList()); + + // 교신저자 중복 체크 + if (externalProfessorIds.size() != externalProfessorIds.stream().distinct().count()) { + throw new ApiException(PaperErrorCode.DUPLICATE_CORRESPONDING_AUTHOR); + } + List externalProfessors = externalProfessorRepository.findAllById(externalProfessorIds); if (externalProfessors.size() != externalProfessorIds.size()) { throw new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND); @@ -157,8 +164,9 @@ public PaperResponse getPaper(Long paperId) { public PaperResponse updatePaper(Long paperId, UpdatePaperRequest dto) { Paper paper = paperRepository.findById(paperId) .orElseThrow(() -> new ApiException(PaperErrorCode.PAPER_NOT_FOUND)); - Journal journal = journalRepository.findById(dto.journalId()) - .orElseThrow(() -> new ApiException(PaperErrorCode.JOURNAL_NOT_FOUND)); + Journal journal = dto.journalId() != null + ? journalRepository.findById(dto.journalId()).orElseThrow(() -> new ApiException(PaperErrorCode.JOURNAL_NOT_FOUND)) + : null; Task task = dto.taskId() != null ? taskRepository.findById(dto.taskId()).orElseThrow(() -> new ApiException(TaskErrorCode.TASK_NOT_FOUND)) : null; @@ -174,6 +182,12 @@ public PaperResponse updatePaper(Long paperId, UpdatePaperRequest dto) { List externalProfessorIds = dto.correspondingAuthors().stream() .map(UpdatePaperRequest.PaperCorrespondingAuthorRequest::externalProfessorId) .collect(Collectors.toList()); + + // 교신저자 중복 체크 + if (externalProfessorIds.size() != externalProfessorIds.stream().distinct().count()) { + throw new ApiException(PaperErrorCode.DUPLICATE_CORRESPONDING_AUTHOR); + } + List externalProfessors = externalProfessorRepository.findAllById(externalProfessorIds); if (externalProfessors.size() != externalProfessorIds.size()) { throw new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND); diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/CreatePatentRequest.java b/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/CreatePatentRequest.java index f85dd65c..40605e39 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/CreatePatentRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/CreatePatentRequest.java @@ -19,17 +19,19 @@ public record CreatePatentRequest( List patentAuthors, @Schema(description = "비고", example = "국내 특허") String remarks, - @Schema(description = "연계 프로젝트 ID", example = "1") + @Schema(description = "연계 프로젝트 ID (선택적)", example = "1") Long projectId, @Schema(description = "연계 과제 ID (선택적)", example = "1") Long taskId, @Schema(description = "첨부 파일 ID 목록", example = "[\"a1b2c3d4-e5f6-7890-1234-567890abcdef\"]") List fileIds ) { - @Schema(description = "특허 저자 요청 DTO") + @Schema(description = "특허 저자 요청 DTO (userId 또는 externalProfessorId 중 하나만 입력)") public record PatentAuthorRequest( - @Schema(description = "사용자 ID", example = "1") + @Schema(description = "내부 사용자 ID (선택)", example = "1") Long userId, + @Schema(description = "외부 인사 ID (선택)", example = "1") + Long externalProfessorId, @Schema(description = "저자 역할", example = "발명자") String role ) {} diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/UpdatePatentRequest.java b/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/UpdatePatentRequest.java index 508d4c56..820df9b1 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/UpdatePatentRequest.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/dto/request/UpdatePatentRequest.java @@ -18,17 +18,19 @@ public record UpdatePatentRequest( List patentAuthors, @Schema(description = "비고", example = "국내/미국 특허") String remarks, - @Schema(description = "연계 프로젝트 ID", example = "2") + @Schema(description = "연계 프로젝트 ID (선택적)", example = "2") Long projectId, @Schema(description = "연계 과제 ID (선택적)", example = "1") Long taskId, @Schema(description = "첨부 파일 ID 목록", example = "[\"a1b2c3d4-e5f6-7890-1234-567890abcdef\"]") List fileIds ) { - @Schema(description = "특허 저자 요청 DTO") + @Schema(description = "특허 저자 요청 DTO (userId 또는 externalProfessorId 중 하나만 입력)") public record PatentAuthorRequest( - @Schema(description = "사용자 ID", example = "1") + @Schema(description = "내부 사용자 ID (선택)", example = "1") Long userId, + @Schema(description = "외부 인사 ID (선택)", example = "1") + Long externalProfessorId, @Schema(description = "저자 역할", example = "발명자") String role ) {} diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/dto/response/PatentResponse.java b/src/main/java/com/bmilab/backend/domain/research/patent/dto/response/PatentResponse.java index 3c81c5d9..8d3a5ecf 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/dto/response/PatentResponse.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/dto/response/PatentResponse.java @@ -29,10 +29,10 @@ public PatentResponse(Patent patent, List patentAuthors, List patentAuthors, List paten patent.getApplicationNumber(), patent.getPatentName(), patent.getApplicantsAll(), - patentAuthors.stream().map(PatentResponse.PatentAuthorResponse::new).collect(Collectors.toList()), + patentAuthors.stream().map(PatentResponse.PatentAuthorResponse::from).collect(Collectors.toList()), patent.getRemarks(), - patent.getProject().getId(), - patent.getProject().getTitle(), + patent.getProject() != null ? patent.getProject().getId() : null, + patent.getProject() != null ? patent.getProject().getTitle() : null, patent.getTask() != null ? patent.getTask().getId() : null, patent.getTask() != null ? patent.getTask().getTitle() : null, files diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/entity/Patent.java b/src/main/java/com/bmilab/backend/domain/research/patent/entity/Patent.java index 5b10d23f..5ef2cdff 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/entity/Patent.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/entity/Patent.java @@ -41,7 +41,7 @@ public class Patent extends BaseTimeEntity { private String remarks; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "project_id", nullable = false) + @JoinColumn(name = "project_id") private Project project; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/entity/PatentAuthor.java b/src/main/java/com/bmilab/backend/domain/research/patent/entity/PatentAuthor.java index 836cd73c..12892580 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/entity/PatentAuthor.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/entity/PatentAuthor.java @@ -1,7 +1,8 @@ package com.bmilab.backend.domain.research.patent.entity; -import com.bmilab.backend.domain.research.entity.ResearchAuthorEntity; +import com.bmilab.backend.domain.project.entity.ExternalProfessor; import com.bmilab.backend.domain.user.entity.User; +import com.bmilab.backend.global.entity.BaseTimeEntity; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -14,16 +15,51 @@ @Table(name = "research_patent_authors") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PatentAuthor extends ResearchAuthorEntity { +public class PatentAuthor extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; @Setter @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "patent_id", nullable = false) private Patent patent; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ex_professor_id") + private ExternalProfessor externalProfessor; + + @Column(nullable = false) + private String role; + @Builder - public PatentAuthor(Patent patent, User user, String role) { - super(user, role); + public PatentAuthor(Patent patent, User user, ExternalProfessor externalProfessor, String role) { this.patent = patent; + this.user = user; + this.externalProfessor = externalProfessor; + this.role = role; + } + + public boolean isInternal() { + return user != null; + } + + public boolean isExternal() { + return externalProfessor != null; + } + + public String getAuthorName() { + if (user != null) { + return user.getName(); + } + if (externalProfessor != null) { + return externalProfessor.getName(); + } + return null; } } diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/exception/PatentErrorCode.java b/src/main/java/com/bmilab/backend/domain/research/patent/exception/PatentErrorCode.java index 74acf58c..2a96c7b7 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/exception/PatentErrorCode.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/exception/PatentErrorCode.java @@ -11,6 +11,7 @@ public enum PatentErrorCode implements ErrorCode { PATENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 특허 정보를 찾을 수 없습니다."), PATENT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "해당 특허 정보에 접근할 권한이 없습니다."), + INVALID_AUTHOR_REQUEST(HttpStatus.BAD_REQUEST, "발명자는 내부 사용자 또는 외부 인사 중 하나만 지정해야 합니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/bmilab/backend/domain/research/patent/service/PatentService.java b/src/main/java/com/bmilab/backend/domain/research/patent/service/PatentService.java index d844b4d8..425c426f 100644 --- a/src/main/java/com/bmilab/backend/domain/research/patent/service/PatentService.java +++ b/src/main/java/com/bmilab/backend/domain/research/patent/service/PatentService.java @@ -3,7 +3,9 @@ import com.bmilab.backend.domain.file.dto.response.FileSummary; import com.bmilab.backend.domain.file.enums.FileDomainType; import com.bmilab.backend.domain.file.service.FileService; +import com.bmilab.backend.domain.project.entity.ExternalProfessor; import com.bmilab.backend.domain.project.entity.Project; +import com.bmilab.backend.domain.project.repository.ExternalProfessorRepository; import com.bmilab.backend.domain.project.repository.ProjectRepository; import com.bmilab.backend.domain.research.patent.exception.PatentErrorCode; import com.bmilab.backend.domain.research.patent.dto.request.CreatePatentRequest; @@ -15,9 +17,10 @@ import com.bmilab.backend.domain.research.patent.entity.PatentAuthor; import com.bmilab.backend.domain.research.patent.repository.PatentAuthorRepository; import com.bmilab.backend.domain.research.patent.repository.PatentRepository; -import com.bmilab.backend.domain.research.service.AuthorSyncService; import com.bmilab.backend.domain.task.entity.Task; import com.bmilab.backend.domain.task.repository.TaskRepository; +import com.bmilab.backend.domain.user.entity.User; +import com.bmilab.backend.domain.user.repository.UserRepository; import com.bmilab.backend.global.exception.ApiException; import com.bmilab.backend.global.exception.GlobalErrorCode; import lombok.RequiredArgsConstructor; @@ -26,7 +29,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @Transactional @@ -37,12 +43,14 @@ public class PatentService { private final PatentAuthorRepository patentAuthorRepository; private final ProjectRepository projectRepository; private final TaskRepository taskRepository; + private final UserRepository userRepository; + private final ExternalProfessorRepository externalProfessorRepository; private final FileService fileService; - private final AuthorSyncService authorSyncService; public PatentResponse createPatent(CreatePatentRequest dto) { - Project project = projectRepository.findById(dto.projectId()) - .orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)); + Project project = dto.projectId() != null + ? projectRepository.findById(dto.projectId()).orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)) + : null; Task task = dto.taskId() != null ? taskRepository.findById(dto.taskId()).orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)) : null; @@ -63,17 +71,13 @@ public PatentResponse createPatent(CreatePatentRequest dto) { .map(FileSummary::from) .toList(); - List patentAuthors = authorSyncService.syncAuthors( + List patentAuthors = syncPatentAuthors( + newPatent, dto.patentAuthors(), CreatePatentRequest.PatentAuthorRequest::userId, - CreatePatentRequest.PatentAuthorRequest::role, - (user, role) -> PatentAuthor.builder() - .patent(newPatent) - .user(user) - .role(role) - .build() + CreatePatentRequest.PatentAuthorRequest::externalProfessorId, + CreatePatentRequest.PatentAuthorRequest::role ); - patentAuthors.forEach(patentAuthorRepository::save); return new PatentResponse(newPatent, patentAuthors, fileSummaries); } @@ -102,8 +106,9 @@ public PatentResponse getPatent(Long patentId) { public PatentResponse updatePatent(Long patentId, UpdatePatentRequest dto) { Patent patent = patentRepository.findById(patentId) .orElseThrow(() -> new ApiException(PatentErrorCode.PATENT_NOT_FOUND)); - Project project = projectRepository.findById(dto.projectId()) - .orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)); + Project project = dto.projectId() != null + ? projectRepository.findById(dto.projectId()).orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)) + : null; Task task = dto.taskId() != null ? taskRepository.findById(dto.taskId()).orElseThrow(() -> new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND)) : null; @@ -116,18 +121,14 @@ public PatentResponse updatePatent(Long patentId, UpdatePatentRequest dto) { .toList(); // Update PatentAuthors - patentAuthorRepository.deleteAllByPatentId(patentId); // Delete existing - List patentAuthors = authorSyncService.syncAuthors( + patentAuthorRepository.deleteAllByPatentId(patentId); + List patentAuthors = syncPatentAuthors( + patent, dto.patentAuthors(), UpdatePatentRequest.PatentAuthorRequest::userId, - UpdatePatentRequest.PatentAuthorRequest::role, - (user, role) -> PatentAuthor.builder() - .patent(patent) - .user(user) - .role(role) - .build() + UpdatePatentRequest.PatentAuthorRequest::externalProfessorId, + UpdatePatentRequest.PatentAuthorRequest::role ); - patentAuthors.forEach(patentAuthorRepository::save); return new PatentResponse(patent, patentAuthors, fileSummaries); } @@ -151,4 +152,68 @@ public PatentFindAllResponse getPatents(String keyword, Pageable pageable) { return PatentFindAllResponse.of(patents, patentPage.getTotalPages()); } + + private List syncPatentAuthors( + Patent patent, + List authorRequests, + java.util.function.Function userIdExtractor, + java.util.function.Function externalProfessorIdExtractor, + java.util.function.Function roleExtractor + ) { + if (authorRequests == null || authorRequests.isEmpty()) { + return new ArrayList<>(); + } + + // 내부 사용자 ID 추출 + List userIds = authorRequests.stream() + .map(userIdExtractor) + .filter(id -> id != null) + .collect(Collectors.toList()); + + // 외부 인사 ID 추출 + List externalProfessorIds = authorRequests.stream() + .map(externalProfessorIdExtractor) + .filter(id -> id != null) + .collect(Collectors.toList()); + + // User 조회 + Map userMap = userIds.isEmpty() ? Map.of() : + userRepository.findAllById(userIds).stream() + .collect(Collectors.toMap(User::getId, user -> user)); + + // ExternalProfessor 조회 + Map externalProfessorMap = externalProfessorIds.isEmpty() ? Map.of() : + externalProfessorRepository.findAllById(externalProfessorIds).stream() + .collect(Collectors.toMap(ExternalProfessor::getId, ep -> ep)); + + // 검증: 요청한 ID가 모두 존재하는지 + if (userMap.size() != userIds.size() || externalProfessorMap.size() != externalProfessorIds.size()) { + throw new ApiException(GlobalErrorCode.GLOBAL_NOT_FOUND); + } + + // PatentAuthor 생성 + List patentAuthors = new ArrayList<>(); + for (T request : authorRequests) { + Long userId = userIdExtractor.apply(request); + Long externalProfessorId = externalProfessorIdExtractor.apply(request); + String role = roleExtractor.apply(request); + + // userId와 externalProfessorId 중 하나만 있어야 함 + if ((userId == null && externalProfessorId == null) || (userId != null && externalProfessorId != null)) { + throw new ApiException(PatentErrorCode.INVALID_AUTHOR_REQUEST); + } + + PatentAuthor author = PatentAuthor.builder() + .patent(patent) + .user(userId != null ? userMap.get(userId) : null) + .externalProfessor(externalProfessorId != null ? externalProfessorMap.get(externalProfessorId) : null) + .role(role) + .build(); + patentAuthorRepository.save(author); + patentAuthors.add(author); + } + + return patentAuthors; + } + } diff --git a/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskBasicInfoUpdateRequest.java b/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskBasicInfoUpdateRequest.java index 65f56efe..b2165b07 100644 --- a/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskBasicInfoUpdateRequest.java +++ b/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskBasicInfoUpdateRequest.java @@ -1,5 +1,6 @@ package com.bmilab.backend.domain.task.dto.request; +import com.bmilab.backend.domain.task.enums.ThreeFiveRuleType; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.format.annotation.DateTimeFormat; @@ -18,8 +19,8 @@ public record TaskBasicInfoUpdateRequest( @Schema(description = "공고번호", example = "RFP-2025-001") String announcementNumber, - @Schema(description = "3책5공", example = "true") - Boolean threeFiveRule, + @Schema(description = "3책5공", example = "RESPONSIBLE") + ThreeFiveRuleType threeFiveRule, @Schema(description = "공고 시작일", example = "2025-02-01") @DateTimeFormat(pattern = "yyyy-MM-dd") diff --git a/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskRequest.java b/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskRequest.java index cae5e24f..a39d52e9 100644 --- a/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskRequest.java +++ b/src/main/java/com/bmilab/backend/domain/task/dto/request/TaskRequest.java @@ -3,6 +3,7 @@ import com.bmilab.backend.domain.task.enums.TaskProfessorRole; import com.bmilab.backend.domain.task.enums.TaskStatus; import com.bmilab.backend.domain.task.enums.TaskSupportType; +import com.bmilab.backend.domain.task.enums.ThreeFiveRuleType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -39,9 +40,9 @@ public record TaskRequest( @NotNull(message = "연구과제지원은 필수입니다.") TaskSupportType supportType, - @Schema(description = "3책5공 포함 여부", example = "true") - @NotNull(message = "3책5공 포함 여부는 필수입니다.") - Boolean threeFiveRule, + @Schema(description = "3책5공", example = "RESPONSIBLE") + @NotNull(message = "3책5공은 필수입니다.") + ThreeFiveRuleType threeFiveRule, @Schema(description = "총 연차", example = "3") @NotNull(message = "총 연차는 필수입니다.") diff --git a/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskBasicInfoResponse.java b/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskBasicInfoResponse.java index 2d5a5983..f0f9a42e 100644 --- a/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskBasicInfoResponse.java +++ b/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskBasicInfoResponse.java @@ -2,6 +2,7 @@ import com.bmilab.backend.domain.file.dto.response.FileSummary; import com.bmilab.backend.domain.task.entity.TaskBasicInfo; +import com.bmilab.backend.domain.task.enums.ThreeFiveRuleType; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; @@ -39,7 +40,7 @@ public record TaskBasicInfoResponse( String announcementLink, @Schema(description = "3책5공") - Boolean threeFiveRule, + ThreeFiveRuleType threeFiveRule, @Schema(description = "과제제안요구서(RFP) 파일") List rfpFiles, @@ -50,7 +51,7 @@ public record TaskBasicInfoResponse( @Schema(description = "연차별 목록") List periods ) { - public static TaskBasicInfoResponse from(TaskBasicInfo basicInfo, LocalDate announcementStartDate, LocalDate announcementEndDate, Boolean threeFiveRule, List rfpFiles, List announcementFiles, List periods) { + public static TaskBasicInfoResponse from(TaskBasicInfo basicInfo, LocalDate announcementStartDate, LocalDate announcementEndDate, ThreeFiveRuleType threeFiveRule, List rfpFiles, List announcementFiles, List periods) { return new TaskBasicInfoResponse( basicInfo != null ? basicInfo.getMinistry() : null, basicInfo != null ? basicInfo.getSpecializedAgency() : null, diff --git a/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskSummaryResponse.java b/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskSummaryResponse.java index 1ec07d8b..79219705 100644 --- a/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskSummaryResponse.java +++ b/src/main/java/com/bmilab/backend/domain/task/dto/response/TaskSummaryResponse.java @@ -4,6 +4,7 @@ import com.bmilab.backend.domain.task.enums.TaskProfessorRole; import com.bmilab.backend.domain.task.enums.TaskStatus; import com.bmilab.backend.domain.task.enums.TaskSupportType; +import com.bmilab.backend.domain.task.enums.ThreeFiveRuleType; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; @@ -37,8 +38,8 @@ public record TaskSummaryResponse( @Schema(description = "연구과제지원") TaskSupportType supportType, - @Schema(description = "삼책오공") - Boolean threeFiveRule, + @Schema(description = "3책5공") + ThreeFiveRuleType threeFiveRule, @Schema(description = "총 연차") Integer totalYears, diff --git a/src/main/java/com/bmilab/backend/domain/task/entity/Task.java b/src/main/java/com/bmilab/backend/domain/task/entity/Task.java index 8464dd9f..07280508 100644 --- a/src/main/java/com/bmilab/backend/domain/task/entity/Task.java +++ b/src/main/java/com/bmilab/backend/domain/task/entity/Task.java @@ -3,6 +3,7 @@ import com.bmilab.backend.domain.task.enums.TaskProfessorRole; import com.bmilab.backend.domain.task.enums.TaskStatus; import com.bmilab.backend.domain.task.enums.TaskSupportType; +import com.bmilab.backend.domain.task.enums.ThreeFiveRuleType; import com.bmilab.backend.domain.user.entity.User; import com.bmilab.backend.global.entity.BaseTimeEntity; import jakarta.persistence.Column; @@ -64,8 +65,9 @@ public class Task extends BaseTimeEntity { @Column(name = "support_type", nullable = false) private TaskSupportType supportType; - @Column(name = "three_five_rule", columnDefinition = "TINYINT(1)", nullable = false) - private Boolean threeFiveRule; + @Enumerated(EnumType.STRING) + @Column(name = "three_five_rule", nullable = false) + private ThreeFiveRuleType threeFiveRule; @Column(name = "start_date", nullable = false) private LocalDate startDate; @@ -112,7 +114,7 @@ public void update( String businessName, String issuingAgency, TaskSupportType supportType, - Boolean threeFiveRule, + ThreeFiveRuleType threeFiveRule, LocalDate startDate, LocalDate endDate, Integer totalYears, diff --git a/src/main/java/com/bmilab/backend/domain/task/enums/ThreeFiveRuleType.java b/src/main/java/com/bmilab/backend/domain/task/enums/ThreeFiveRuleType.java new file mode 100644 index 00000000..fb14d33c --- /dev/null +++ b/src/main/java/com/bmilab/backend/domain/task/enums/ThreeFiveRuleType.java @@ -0,0 +1,7 @@ +package com.bmilab.backend.domain.task.enums; + +public enum ThreeFiveRuleType { + RESPONSIBLE, // 책임 + JOINT, // 공동 + NOT_APPLICABLE // 해당없음 +}