From 86690a1be27b8337cb71e917851d73a2f6eaf325 Mon Sep 17 00:00:00 2001 From: DoHoon Yoon <148332333+hodoon@users.noreply.github.com> Date: Tue, 8 Apr 2025 22:52:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=86=9C=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EB=AA=A8=EC=A7=91=20API=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 면접 예약 API 면접 일정 조회시 status도 같이 반환 * fix: 면접 예약 API 예약자 명단 조회 API 추가 * fix: 면접 예약 API 예약되어있는 슬롯은 삭제하지 않는 기능 추가 * fix: 면접 예약자 조회 API 면접일, 면접시간, 면접을 예약한 날짜과 시간 추가 * feat: 솜커톤 참가자 모집 API 생성 * feat: 솜커톤 참가자 모집 API 요청 URL에 somkathon 추가 * fix: 솜커톤 참가자 모집 API comment 해결 * fix: 솜커톤 참가자 모집 API comment 해결 --- .../domain/common/exception/ErrorCode.java | 1 + .../InterviewReservationRepository.java | 2 +- .../InterviewSlotRepository.java | 2 +- .../service/InterviewServiceImpl.java | 4 +- .../controller/SomParticipantController.java | 126 ++++++++++++++++++ .../dto/SomParticipantRequestDto.java | 44 ++++++ .../dto/SomParticipantResponseDto.java | 34 +++++ .../somkathon/entity/SomParticipant.java | 50 +++++++ .../repository/SomParticipantRepository.java | 12 ++ .../service/SomParticipantService.java | 102 ++++++++++++++ 10 files changed, 373 insertions(+), 4 deletions(-) rename src/main/java/dmu/dasom/api/domain/interview/{repositoty => repository}/InterviewReservationRepository.java (87%) rename src/main/java/dmu/dasom/api/domain/interview/{repositoty => repository}/InterviewSlotRepository.java (95%) create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/controller/SomParticipantController.java create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/repository/SomParticipantRepository.java create mode 100644 src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java diff --git a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java index 19ab326..8910414 100644 --- a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java +++ b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java @@ -36,6 +36,7 @@ public enum ErrorCode { SLOT_NOT_ACTIVE(400, "C027", "해당 슬롯이 비활성화 되었습니다."), FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다."), RECRUITMENT_NOT_ACTIVE(400, "C029", "모집 기간이 아닙니다."), + NOT_FOUND_PARTICIPANT(400, "C030", "참가자를 찾을 수 없습니다.") ; private final int status; diff --git a/src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewReservationRepository.java b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java similarity index 87% rename from src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewReservationRepository.java rename to src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java index 65a8f0a..4b7e402 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewReservationRepository.java +++ b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewReservationRepository.java @@ -1,4 +1,4 @@ -package dmu.dasom.api.domain.interview.repositoty; +package dmu.dasom.api.domain.interview.repository; import dmu.dasom.api.domain.interview.entity.InterviewReservation; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewSlotRepository.java b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java similarity index 95% rename from src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewSlotRepository.java rename to src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java index 9c718f9..67d005c 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/repositoty/InterviewSlotRepository.java +++ b/src/main/java/dmu/dasom/api/domain/interview/repository/InterviewSlotRepository.java @@ -1,4 +1,4 @@ -package dmu.dasom.api.domain.interview.repositoty; +package dmu.dasom.api.domain.interview.repository; import dmu.dasom.api.domain.interview.entity.InterviewSlot; import dmu.dasom.api.domain.interview.enums.InterviewStatus; diff --git a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java index 0d0dfb6..6a94895 100644 --- a/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/interview/service/InterviewServiceImpl.java @@ -10,8 +10,8 @@ import dmu.dasom.api.domain.interview.entity.InterviewReservation; import dmu.dasom.api.domain.interview.entity.InterviewSlot; import dmu.dasom.api.domain.interview.enums.InterviewStatus; -import dmu.dasom.api.domain.interview.repositoty.InterviewReservationRepository; -import dmu.dasom.api.domain.interview.repositoty.InterviewSlotRepository; +import dmu.dasom.api.domain.interview.repository.InterviewReservationRepository; +import dmu.dasom.api.domain.interview.repository.InterviewSlotRepository; import dmu.dasom.api.domain.recruit.service.RecruitServiceImpl; import jakarta.persistence.EntityListeners; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/controller/SomParticipantController.java b/src/main/java/dmu/dasom/api/domain/somkathon/controller/SomParticipantController.java new file mode 100644 index 0000000..48517c0 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/controller/SomParticipantController.java @@ -0,0 +1,126 @@ +package dmu.dasom.api.domain.somkathon.controller; + +import dmu.dasom.api.domain.somkathon.dto.SomParticipantRequestDto; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantResponseDto; +import dmu.dasom.api.domain.somkathon.service.SomParticipantService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/somkathon/participants") +@RequiredArgsConstructor +@Validated +public class SomParticipantController { + + private final SomParticipantService somParticipantService; + + /** + * 참가자 등록 + */ + @Operation(summary = "솜커톤 참가자 등록", description = "솜커톤 참가자를 등록합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자 등록 성공"), + @ApiResponse(responseCode = "400", description = "중복 학번 또는 필수 값 누락", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "중복된 학번", + value = "{ \"code\": \"C002\", \"message\": \"이미 등록된 학번입니다.\" }" + ), + @ExampleObject( + name = "필수 값 누락", + value = "{ \"code\": \"C001\", \"message\": \"요청한 값이 올바르지 않습니다.\" }" + )}))}) + @PostMapping("/create") + public ResponseEntity create(@Valid @RequestBody final SomParticipantRequestDto request) { + return ResponseEntity.ok(somParticipantService.createParticipant(request)); + } + + /** + * 모든 참가자 조회 + */ + @Operation(summary = "솜커톤 참가자 목록 조회", description = "모든 솜커톤 참가자를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자 목록 조회 성공")}) + @GetMapping + public ResponseEntity> findAll() { + return ResponseEntity.ok(somParticipantService.getAllParticipants()); + } + + /** + * 특정 참가자 조회 + */ + @Operation(summary = "솜커톤 참가자 상세 조회", description = "특정 솜커톤 참가자의 상세 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자 상세 조회 성공"), + @ApiResponse(responseCode = "400", description = "존재하지 않는 ID", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation=ErrorResponse.class), + examples={ + @ExampleObject( + name="존재하지 않는 ID", + value="{\"code\":\"C004\",\"message\":\"참가자를 찾을 수 없습니다.\"}")}))}) + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable final Long id) { + return ResponseEntity.ok(somParticipantService.getParticipant(id)); + } + + /** + * 참가자 정보 수정 + */ + @Operation(summary = "솜커톤 참가자 정보 수정", description = "특정 솜커톤 참가자의 정보를 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자 정보 수정 성공"), + @ApiResponse(responseCode = "400", description = "중복 학번 또는 존재하지 않는 ID", + content = @Content( + mediaType = "application/json", + schema=@Schema(implementation=ErrorResponse.class), + examples={ + @ExampleObject( + name="중복된 학번", + value="{\"code\":\"C002\",\"message\":\"이미 등록된 학번입니다.\"}"), + @ExampleObject( + name="존재하지 않는 ID", + value="{\"code\":\"C004\",\"message\":\"참가자를 찾을 수 없습니다.\"}")}))}) + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable final Long id, + @Valid @RequestBody final SomParticipantRequestDto request) { + return ResponseEntity.ok(somParticipantService.updateParticipant(id, request)); + } + + /** + * 참가자 삭제 (Delete) + */ + @Operation(summary = "솜커톤 참가자 삭제", description = "특정 솜커톤 참가자를 삭제합니다.") + @ApiResponses(value={ + @ApiResponse(responseCode="204",description="참가자 삭제 성공"), + @ApiResponse(responseCode="400",description="존재하지 않는 ID", + content=@Content( + mediaType="application/json", + schema=@Schema(implementation=ErrorResponse.class), + examples={ + @ExampleObject( + name="존재하지 않는 ID", + value="{\"code\":\"C004\",\"message\":\"참가자를 찾을 수 없습니다.\"}")}))}) + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable final Long id) { + somParticipantService.deleteParticipant(id); + return ResponseEntity.noContent().build(); + } + +} diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java new file mode 100644 index 0000000..261c4ae --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantRequestDto.java @@ -0,0 +1,44 @@ +package dmu.dasom.api.domain.somkathon.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Schema(description = "솜커톤 참가자 요청 DTO") +public class SomParticipantRequestDto { + @NotBlank(message = "참가자 이름은 필수 입력 값입니다.") + @Size(max = 50, message = "참가자 이름은 최대 50자까지 입력 가능합니다.") + @Schema(description = "참가자 이름", example = "유승완", required = true) + private String participantName; // 참가자 이름 + + @NotBlank(message = "학번은 필수 입력 값입니다.") + @Pattern(regexp = "^[0-9]{8}$", message = "학번은 8자리 숫자여야 합니다.") + @Schema(description = "학번 (8자리 숫자)", example = "20250001", required = true) + private String studentId; + + @NotBlank(message = "학과는 필수 입력 값입니다.") + @Size(max = 100, message = "학과는 최대 100자까지 입력 가능합니다.") + @Schema(description = "학과", example = "컴퓨터소프트웨어공학과", required = true) + private String department; // 학과 + + @NotBlank(message = "학년은 필수 입력 값입니다.") + @Pattern(regexp = "^[1-4]$", message = "학년은 1~4 사이의 숫자여야 합니다.") + @Schema(description = "학년 (1~4)", example = "3", required = true) + private String grade; // 학년 + + @NotBlank(message = "연락처는 필수 입력 값입니다.") + @Pattern(regexp = "^010-[0-9]{4}-[0-9]{4}$", message = "연락처는 '010-XXXX-XXXX' 형식이어야 합니다.") + @Schema(description = "연락처 (010-XXXX-XXXX 형식)", example = "010-1234-5678", required = true) + private String contact; // 연락처 + + @NotBlank(message = "이메일은 필수 입력 값입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") + @Schema(description = "이메일 주소", example = "hong@example.com", required = true) + private String email; // 이메일 +} diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java new file mode 100644 index 0000000..6e20c99 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/dto/SomParticipantResponseDto.java @@ -0,0 +1,34 @@ +package dmu.dasom.api.domain.somkathon.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +@Schema(description = "솜커톤 참가자 응답 DTO") +public class SomParticipantResponseDto { + + @Schema(description = "참가자 ID", example = "1", required = true) + private Long id; // 참가자 ID + + @Schema(description = "참가자 이름", example = "홍길동", required = true) + private String participantName; // 참가자 이름 + + @Schema(description = "학번 (8자리 숫자)", example = "20230001", required = true) + private String studentId; // 학번 + + @Schema(description = "학과", example = "컴퓨터공학과", required = true) + private String department; // 학과 + + @Schema(description = "학년 (1~4)", example = "3", required = true) + private String grade; // 학년 + + @Schema(description = "연락처 (010-XXXX-XXXX 형식)", example = "010-1234-5678", required = true) + private String contact; // 연락처 + + @Schema(description = "이메일 주소", example = "hong@example.com", required = true) + private String email; // 이메일 +} diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java b/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java new file mode 100644 index 0000000..7ab055c --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/entity/SomParticipant.java @@ -0,0 +1,50 @@ +package dmu.dasom.api.domain.somkathon.entity; + +import dmu.dasom.api.domain.common.BaseEntity; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantRequestDto; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@AllArgsConstructor +@Builder +@Entity +@EntityListeners(AuditingEntityListener.class) +@Getter +@NoArgsConstructor +public class SomParticipant extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String participantName; // 참가자 이름 + + @Column(nullable = false, unique = true) + private String studentId; // 학번 + + @Column(nullable = false) + private String department; // 학과 + + @Column(nullable = false) + private String grade; // 학년 + + @Column(nullable = false) + private String contact; // 연락처 + + @Column(nullable = false) + private String email; // 이메일 + + public void update(SomParticipantRequestDto requestDto) { + this.participantName = requestDto.getParticipantName(); + this.studentId = requestDto.getStudentId(); + this.department = requestDto.getDepartment(); + this.grade = requestDto.getGrade(); + this.contact = requestDto.getContact(); + this.email = requestDto.getEmail(); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/repository/SomParticipantRepository.java b/src/main/java/dmu/dasom/api/domain/somkathon/repository/SomParticipantRepository.java new file mode 100644 index 0000000..7b650ab --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/repository/SomParticipantRepository.java @@ -0,0 +1,12 @@ +package dmu.dasom.api.domain.somkathon.repository; + +import dmu.dasom.api.domain.somkathon.entity.SomParticipant; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SomParticipantRepository extends JpaRepository { + Optional findByStudentId(String studentId); // 학번으로 참가자 조회 +} diff --git a/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java b/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java new file mode 100644 index 0000000..0169d8e --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/somkathon/service/SomParticipantService.java @@ -0,0 +1,102 @@ +package dmu.dasom.api.domain.somkathon.service; + +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantRequestDto; +import dmu.dasom.api.domain.somkathon.dto.SomParticipantResponseDto; +import dmu.dasom.api.domain.somkathon.entity.SomParticipant; +import dmu.dasom.api.domain.somkathon.repository.SomParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class SomParticipantService { + private final SomParticipantRepository somParticipantRepository; + + // 참가자 등록 + public SomParticipantResponseDto createParticipant(SomParticipantRequestDto requestDto) { + + validateDuplicatedStudentId(requestDto.getStudentId()); + + SomParticipant participant = SomParticipant.builder() + .participantName(requestDto.getParticipantName()) + .studentId(requestDto.getStudentId()) + .department(requestDto.getDepartment()) + .grade(requestDto.getGrade()) + .contact(requestDto.getContact()) + .email(requestDto.getEmail()) + .build(); + + SomParticipant saved = somParticipantRepository.save(participant); + + return toResponseDto(saved); + } + + /** + * 모든 참가자 조회 (Read) + */ + public List getAllParticipants() { + return somParticipantRepository.findAll().stream() + .map(this::toResponseDto) + .collect(Collectors.toList()); + } + + /** + * 특정 참가자 조회 (Read) + */ + public SomParticipantResponseDto getParticipant(Long id){ + SomParticipant participant = findParticipantById(id); + + return toResponseDto(participant); + } + + public SomParticipantResponseDto updateParticipant(Long id, SomParticipantRequestDto requestDto){ + SomParticipant participant = findParticipantById(id); + + participant.update(requestDto); + + return toResponseDto(participant); + } + + /** + * 참가자 삭제 (Delete) + */ + public void deleteParticipant(Long id) { + findParticipantById(id); + somParticipantRepository.deleteById(id); + } + + + /** + * Entity → Response DTO 변환 메서드 + */ + private SomParticipantResponseDto toResponseDto(SomParticipant participant) { + return SomParticipantResponseDto.builder() + .id(participant.getId()) + .participantName(participant.getParticipantName()) + .studentId(participant.getStudentId()) + .department(participant.getDepartment()) + .grade(participant.getGrade()) + .contact(participant.getContact()) + .email(participant.getEmail()) + .build(); + } + + /** + * ID로 참가자 조회 (공통 처리) + */ + private SomParticipant findParticipantById(Long id) { + return somParticipantRepository.findById(id) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_PARTICIPANT)); + } + + private void validateDuplicatedStudentId(String studentId) { + if (somParticipantRepository.findByStudentId(studentId).isPresent()) { + throw new CustomException(ErrorCode.DUPLICATED_STUDENT_NO); + } + } +} From 6ee719d8af21c3d2362d27a411c86b288b8f306d Mon Sep 17 00:00:00 2001 From: Dohyeon Choi Date: Sat, 16 Aug 2025 19:25:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EB=89=B4=EC=8A=A4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dmu/dasom/api/domain/news/dto/NewsListResponseDto.java | 3 +++ src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/dmu/dasom/api/domain/news/dto/NewsListResponseDto.java b/src/main/java/dmu/dasom/api/domain/news/dto/NewsListResponseDto.java index 2749c3a..ec2910f 100644 --- a/src/main/java/dmu/dasom/api/domain/news/dto/NewsListResponseDto.java +++ b/src/main/java/dmu/dasom/api/domain/news/dto/NewsListResponseDto.java @@ -22,6 +22,9 @@ public class NewsListResponseDto { @Schema(description = "뉴스 제목", example = "제목") private String title; + @Schema(description = "뉴스 내용", example = "내용") + private String content; + @Schema(description = "작성일", example = "2025-02-14T12:00:00") private LocalDateTime createdAt; diff --git a/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java b/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java index 05a7661..20be028 100644 --- a/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java +++ b/src/main/java/dmu/dasom/api/domain/news/entity/NewsEntity.java @@ -52,6 +52,7 @@ public NewsListResponseDto toListResponseDto(FileResponseDto file) { return NewsListResponseDto.builder() .id(this.id) .title(this.title) + .content(this.content) .createdAt(getCreatedAt()) .image(file) .build();