Skip to content

feat: 반려동물용 강의 관리기능 추가#1

Open
yakpoong wants to merge 3 commits intomainfrom
feature/pet-course
Open

feat: 반려동물용 강의 관리기능 추가#1
yakpoong wants to merge 3 commits intomainfrom
feature/pet-course

Conversation

@yakpoong
Copy link
Copy Markdown
Collaborator

@yakpoong yakpoong commented Mar 23, 2025

요약

반려동물용 강의 관리 기능을 추가하였습니다.

주요 코드 구현사항

  • 강의(Course) Entity 를 추가하였습니다.
  • 강의 생성 API(POST /courses) 를 구현하였습니다.

Summary by CodeRabbit

  • 새로운 기능
    • 코스 관리 기능이 추가되어, 사용자가 새로운 코스를 생성하고 강사 정보와 연동할 수 있습니다.
    • 프로젝트 구성 옵션이 강화되어 한국어 지원, 초기 액세스 및 무료 사용 옵션 등의 설정이 도입되었습니다.
    • 코스 관련 데이터 저장 및 검색 기능이 최적화되어, 전반적인 데이터 관리가 개선되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2025

워크스루

이 풀 리퀘스트는 새로운 설정 파일과 코스 관련 기능을 추가합니다. .coderabbit.yaml 파일에 프로젝트의 언어, 리뷰, 도구, 채팅 자동 응답, 지식 베이스 관리 등 다양한 설정이 정의되었으며, build.gradle에 Lombok 관련 의존성이 추가되었습니다. 또한, 코스 관리를 위한 JPA 엔티티, 리포지토리, 컨트롤러, 서비스, DTO가 새로 도입되고, 데이터베이스 스키마에 courses 테이블과 인덱스가 생성되었습니다. VetRepository에도 findById 메서드가 추가되어 있습니다.

Changes

파일 변경 요약
.coderabbit.yaml 프로젝트 설정 파일 추가: 언어(ko-KR), 초기 액세스, 무료 티어, 리뷰, 도구, 코드 품질 체크, 채팅 자동응답 및 지식 베이스 관리 옵션 정의
build.gradle Lombok 의존성 추가: compileOnly 'org.projectlombok:lombok'annotationProcessor 'org.projectlombok:lombok'
src/.../course/*.java
src/.../course/dto/*.java
코스 관련 신규 파일 추가: Course, CouresRepository, CourseController (POST /courses 메서드 포함), CourseService (createCourse 메서드 포함), CreateCourseRequest DTO
src/.../vet/VetRepository.java findById(Integer id) 메서드 추가 (readOnly 트랜잭션, DataAccessException 발생 가능)
src/.../schema.sql 새 테이블 추가: courses 테이블(컬럼: id, name, description, difficulty, vet_id) 및 인덱스 courses_vet_id 생성

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant CC as CourseController
    participant CS as CourseService
    participant VR as VetRepository
    participant CR as CouresRepository

    C->>CC: POST /courses (CreateCourseRequest)
    CC->>CS: createCourse(name, description, difficulty, vetId)
    CS->>VR: findById(vetId)
    VR--)CS: Vet or Error
    CS->>CR: save(Course)
    CR--)CS: Saved Course
    CS-->>CC: Course 객체 반환
    CC-->>C: ResponseEntity (course id)
Loading

Poem

작은 토끼가 기뻐 노래해
새로운 코드 길에 발자국 남겨
설정과 클래스가 춤추는 이곳에서
한 줄 한 줄 꿈을 심어가네 🐰
오늘도 코드 숲을 달리며 미소 짓네!

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (16)
src/main/java/org/springframework/samples/petclinic/course/dto/CreateCourseRequest.java (2)

6-10: 요청 DTO에 유효성 검증이 필요합니다

현재 CreateCourseRequest 클래스에는 필드에 대한 유효성 검증이 없습니다. 강의명, 설명, 난이도 등에 대한 기본적인 유효성 검증을 추가하는 것이 좋습니다.

다음과 같이 Bean Validation 어노테이션을 추가하세요:

 @Getter()
 public class CreateCourseRequest {
+	@NotBlank(message = "강의명은 필수입니다")
+	@Size(max = 100, message = "강의명은 100자를 넘을 수 없습니다")
 	private String name;
 	
+	@Size(max = 500, message = "설명은 500자를 넘을 수 없습니다")
 	private String description;
 	
+	@NotBlank(message = "난이도는 필수입니다")
 	private String difficulty;
 	
+	@NotNull(message = "강사 ID는 필수입니다")
 	private Integer vetId;

위 수정을 적용하시려면 먼저 다음 import를 추가하세요:

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

5-5: Lombok 어노테이션 최적화

@Getter()와 같이 빈 괄호를 사용하지 않는 것이 관례입니다.

다음과 같이 수정하세요:

-@Getter()
+@Getter
src/main/java/org/springframework/samples/petclinic/course/Course.java (2)

22-25: 필드 유효성 검증을 추가하는 것이 좋습니다.

현재 문자열 필드들(name, description, difficulty)에 대한 유효성 검증이 없습니다. @NotBlank, @Size 등의 검증 애노테이션을 추가하여 데이터 무결성을 보장하는 것이 좋습니다.

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;

 @Getter
 @Setter
 @Entity
 @Table(name = "courses")
 public class Course extends BaseEntity {
+	@NotBlank(message = "이름은 필수 항목입니다")
+	@Size(max = 255, message = "이름은 255자를 초과할 수 없습니다")
 	private String name;
 	
+	@Size(max = 1000, message = "설명은 1000자를 초과할 수 없습니다")
 	private String description;
 	// 예: 초급, 중급, 고급
+	@NotBlank(message = "난이도는 필수 항목입니다")
 	private String difficulty;

24-25: 난이도 필드를 Enum으로 관리하는 것이 좋습니다.

주석에 명시된 것처럼 난이도는 정해진 값들('초급', '중급', '고급')만 사용할 것으로 보입니다. 이런 경우 String 대신 Enum을 사용하면 타입 안전성을 확보하고 코드의 가독성을 높일 수 있습니다.

-	// 예: 초급, 중급, 고급
-	private String difficulty;
+	@Enumerated(EnumType.STRING)
+	private CourseDifficulty difficulty;

그리고 새로운 Enum 클래스를 생성합니다:

package org.springframework.samples.petclinic.course;

public enum CourseDifficulty {
    초급, 중급, 고급
}
.coderabbit.yaml (4)

27-27: 행 끝의 공백을 제거해야 합니다.

YAMLlint 정적 분석 도구에서 지적한 대로, 이 줄의 끝에 있는 공백을 제거해야 합니다.

-        Review the API endpoints based on this guidelines.
+        Review the API endpoints based on this guidelines.
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 27-27: trailing spaces

(trailing-spaces)


33-33: 행 끝의 공백을 제거해야 합니다.

YAMLlint 정적 분석 도구에서 지적한 대로, 이 줄의 끝에 있는 공백을 제거해야 합니다.

-        4. Level 3: Hypermedia (HATEOAS) for advanced RESTful APIs.
+        4. Level 3: Hypermedia (HATEOAS) for advanced RESTful APIs.
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 33-33: trailing spaces

(trailing-spaces)


42-42: 행 끝의 공백을 제거해야 합니다.

YAMLlint 정적 분석 도구에서 지적한 대로, 이 줄의 끝에 있는 공백을 제거해야 합니다.

-          - Resource identifiers should be unique and only one per URI path.
+          - Resource identifiers should be unique and only one per URI path.
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 42-42: trailing spaces

(trailing-spaces)


51-51: 행 끝의 공백을 제거해야 합니다.

YAMLlint 정적 분석 도구에서 지적한 대로, 이 줄의 끝에 있는 공백을 제거해야 합니다.

-          - Complex actions can include verbs in the URI (/orders/{orderId}/cancel).
+          - Complex actions can include verbs in the URI (/orders/{orderId}/cancel).
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 51-51: trailing spaces

(trailing-spaces)

src/main/java/org/springframework/samples/petclinic/course/CourseController.java (5)

3-13: 불필요한 import 문이 있습니다.

사용하지 않는 import 문이 있습니다:

  • org.springframework.samples.petclinic.vet.Vet
  • org.springframework.samples.petclinic.vet.VetRepository
  • org.springframework.web.bind.annotation.GetMapping
  • org.springframework.web.bind.annotation.RequestParam

이러한 불필요한 import는 제거하는 것이 좋습니다.

 package org.springframework.samples.petclinic.course;

 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.samples.petclinic.course.dto.CreateCourseRequest;
-import org.springframework.samples.petclinic.vet.Vet;
-import org.springframework.samples.petclinic.vet.VetRepository;
-import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;

21-21: 불필요한 빈 줄이 있습니다.

불필요한 빈 줄은 제거하는 것이 좋습니다.

 public class CourseController {
 	private final CourseService courseService;

-
 	@PostMapping()

22-22: PostMapping에 명확한 경로를 지정하는 것이 좋습니다.

@PostMapping()보다는 명시적으로 @PostMapping("/")로 지정하는 것이 API 설계 관점에서 더 명확합니다.

-	@PostMapping()
+	@PostMapping("/")

24-24: System.out.println 대신 로깅 프레임워크를 사용해야 합니다.

System.out.println은 운영 환경에서 사용하기에 적합하지 않습니다. 로깅 프레임워크(예: SLF4J)를 사용하는 것이 좋습니다.

+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;

 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/courses")
 public class CourseController {
+	private static final Logger log = LoggerFactory.getLogger(CourseController.class);
 	private final CourseService courseService;

 	@PostMapping()
 	public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
-		System.out.println(request);
+		log.info("Course creation request received: {}", request);

23-29: 요청 본문에 대한 유효성 검증이 없습니다.

현재 컨트롤러 메서드에서는 요청 본문에 대한 유효성 검증이 없습니다. @Valid 애노테이션을 사용하여 요청 데이터의 유효성을 검증하는 것이 좋습니다.

+import jakarta.validation.Valid;

 	@PostMapping()
-	public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
+	public ResponseEntity<Integer> createCourse(@Valid @RequestBody CreateCourseRequest request) {

또한 CreateCourseRequest 클래스에 유효성 검증 애노테이션을 추가해야 합니다.

src/main/java/org/springframework/samples/petclinic/course/CourseService.java (3)

28-30: 더 구체적인 예외 처리가 필요합니다.

일반적인 RuntimeException보다 더 구체적인 예외를 사용하는 것이 좋습니다. 사용자 정의 예외 클래스를 만들거나 Spring의 EntityNotFoundException을 사용하는 것을 고려해보세요.

 		Vet instructor = vetRepository.findById(vetId)
-			.orElseThrow(() -> new RuntimeException("Vet not found"));
+			.orElseThrow(() -> new EntityNotFoundException("Vet not found with ID: " + vetId));

위 코드를 적용하려면 다음 import를 추가해야 합니다:

import javax.persistence.EntityNotFoundException;

또는 사용자 정의 예외를 만들어 사용할 수도 있습니다:

import org.springframework.samples.petclinic.course.exception.VetNotFoundException;

// ...
.orElseThrow(() -> new VetNotFoundException("Vet not found with ID: " + vetId));

31-39: 입력 검증과 로깅이 필요합니다.

현재 코드는 입력값에 대한 검증이 없습니다. 이름, 설명, 난이도가 null이거나 빈 문자열인 경우에 대한 처리가 필요합니다. 또한, 새 코스 생성 시 로깅을 추가하는 것이 좋습니다.

+		// 입력값 검증
+		if (name == null || name.trim().isEmpty()) {
+			throw new IllegalArgumentException("Course name cannot be empty");
+		}
+		
 		Course course = new Course();
 		course.setName(name);
 		course.setDescription(description);
 		course.setDifficulty(difficulty);
 		course.setInstructor(instructor);
 		courseRepository.save(course);
+		
+		log.info("Course created: {}", course.getName());
 
 		return course;

로깅을 위해 다음과 같은 import와 어노테이션을 추가하세요:

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
// ...

1-41: 서비스 메소드에 대한 단위 테스트가 필요합니다.

이 서비스 클래스에 대한 단위 테스트를 작성하면 코드의 신뢰성을 높일 수 있습니다. 특히 강사가 존재하지 않는 경우의 예외 처리와 코스 생성 성공 케이스를 테스트하는 것이 중요합니다.

단위 테스트 예제 코드를 생성해드릴까요?

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa53f9 and 4e4b4c7.

📒 Files selected for processing (9)
  • .coderabbit.yaml (1 hunks)
  • build.gradle (2 hunks)
  • src/main/java/org/springframework/samples/petclinic/course/CouresRepository.java (1 hunks)
  • src/main/java/org/springframework/samples/petclinic/course/Course.java (1 hunks)
  • src/main/java/org/springframework/samples/petclinic/course/CourseController.java (1 hunks)
  • src/main/java/org/springframework/samples/petclinic/course/CourseService.java (1 hunks)
  • src/main/java/org/springframework/samples/petclinic/course/dto/CreateCourseRequest.java (1 hunks)
  • src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java (2 hunks)
  • src/main/resources/db/h2/schema.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.sql`: As a team convention, we do not specify FK constraints. Please avoid suggesting adding FK constraints in code reviews. This file is a TypeScript file, but the SQL state...

**/*.sql: As a team convention, we do not specify FK constraints. Please avoid suggesting adding FK constraints in code reviews. This file is a TypeScript file, but the SQL statements that modify the table structure are written as strings. For columns used as primary keys (PK) or foreign keys (FK), you must follow the convention of specifying the column type as "bigint". If a table column name is "id" or ends with "_id", verify that the column type is "bigint". If the SQL is written with a type other than "bigint", issue a warning. This convention is mandatory, so please raise the severity level of the warning. Add the following characters to the beginning of the warning message so that users can easily notice it: 🚨🚨🚨

  • src/main/resources/db/h2/schema.sql
🧬 Code Definitions (2)
src/main/java/org/springframework/samples/petclinic/course/dto/CreateCourseRequest.java (1)
src/main/java/org/springframework/samples/petclinic/course/Course.java (1)
  • Getter (17-31)
src/main/java/org/springframework/samples/petclinic/course/Course.java (1)
src/main/java/org/springframework/samples/petclinic/course/dto/CreateCourseRequest.java (1)
  • Getter (5-31)
🪛 YAMLlint (1.35.1)
.coderabbit.yaml

[error] 27-27: trailing spaces

(trailing-spaces)


[error] 33-33: trailing spaces

(trailing-spaces)


[error] 42-42: trailing spaces

(trailing-spaces)


[error] 51-51: trailing spaces

(trailing-spaces)

🔇 Additional comments (16)
src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java (1)

41-46: 변경사항이 잘 구현되었습니다

findById 메서드가 적절하게 추가되었고, 읽기 전용 트랜잭션으로 올바르게 설정되었습니다. 메서드 반환 타입이 Optional<Vet>으로 설정되어 존재하지 않는 ID 처리도 적절합니다.

build.gradle (2)

38-38: Lombok 의존성 추가가 적절합니다

코드 간소화를 위한 Lombok 의존성이 적절하게 추가되었습니다.


63-63: Lombok 어노테이션 프로세서 추가가
적절합니다

Lombok 어노테이션 프로세싱을 위한 의존성이 올바르게 추가되었습니다.

src/main/java/org/springframework/samples/petclinic/course/Course.java (2)

17-21: 엔티티 클래스에 적절한 애노테이션이 사용되었습니다.

엔티티 클래스가 적절하게 정의되어 있으며, 테이블명도 명확하게 지정되어 있습니다.


27-29: 다대일 관계가 적절하게 설정되었습니다.

CourseVet 간의 다대일 관계가 적절하게 설정되어 있으며, 외래 키 컬럼 이름도 명확하게 지정되어 있습니다.

.coderabbit.yaml (8)

1-4: 기본 설정이 올바르게 구성되어 있습니다.

한국어 설정과 얼리 액세스 및 프리 티어 활성화가 올바르게 구성되어 있습니다.


6-14: 리뷰 설정이 적절하게 구성되어 있습니다.

리뷰 프로필, 요약 설정, 워크플로우 설정이 적절하게 구성되어 있습니다.


14-17: 경로 필터가 적절하게 구성되어 있습니다.

swagger.json 및 README.md 파일을 제외하는 필터가 올바르게 설정되어 있습니다.


17-23: SQL 파일 지침이 명확하게 정의되어 있습니다.

SQL 파일에 대한 리뷰 지침이 잘 정의되어 있습니다. PK 및 FK 컬럼 타입에 대한 규칙이 명확합니다.


24-56: 컨트롤러 API 엔드포인트 디자인 지침이 잘 정의되어 있습니다.

RESTful API 디자인에 대한 Richardson Maturity Model과 URI 규칙, 디자인 원칙 등이 상세하게 정의되어 있습니다.

🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 27-27: trailing spaces

(trailing-spaces)


[error] 33-33: trailing spaces

(trailing-spaces)


[error] 42-42: trailing spaces

(trailing-spaces)


[error] 51-51: trailing spaces

(trailing-spaces)


56-64: 자동 리뷰 설정이 적절하게 구성되어 있습니다.

자동 리뷰 및 증분 리뷰가 활성화되어 있고, 다양한 설정이 올바르게 구성되어 있습니다.


64-108: 정적 분석 도구 설정이 잘 구성되어 있습니다.

다양한 정적 분석 도구가 활성화되어 있으며, 필요한 설정이 적절하게 구성되어 있습니다.


109-120: 지식 베이스 설정이 적절하게 구성되어 있습니다.

학습, 이슈, Jira, Linear 등 지식 베이스 관련 설정이 잘 구성되어 있습니다.

src/main/resources/db/h2/schema.sql (1)

73-73: 인덱스가 적절하게 생성되었습니다.

courses_vet_id 인덱스가 적절하게 생성되어 있어 vet_id를 기준으로 한 쿼리 성능이 향상될 것입니다.

src/main/java/org/springframework/samples/petclinic/course/CourseController.java (1)

15-20: 컨트롤러 클래스 구조가 잘 설계되어 있습니다.

컨트롤러 클래스에 적절한 애노테이션이 사용되었으며, 생성자 주입 방식을 통해 의존성이 주입되고 있습니다.

src/main/java/org/springframework/samples/petclinic/course/CourseService.java (1)

1-10: 잘 정의된 패키지 구조와 필요한 임포트만 포함된 것을 확인했습니다.

패키지와 임포트 구조가 잘 정의되어 있습니다. 필요한 클래스만 임포트하고 있어 좋습니다.

Comment on lines +5 to +6
public interface CouresRepository extends JpaRepository<Course, Integer> {
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

인터페이스 이름에 오타가 있습니다

인터페이스 이름이 CouresRepository로 되어 있는데, 올바른 철자는 CourseRepository입니다. 's'가 빠져있습니다. 이 오타는 다른 클래스에서 이 레포지토리를 참조할 때 혼란을 줄 수 있습니다.

다음과 같이 수정하세요:

-public interface CouresRepository extends JpaRepository<Course, Integer> {
+public interface CourseRepository extends JpaRepository<Course, Integer> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public interface CouresRepository extends JpaRepository<Course, Integer> {
}
public interface CourseRepository extends JpaRepository<Course, Integer> {
}

Comment on lines +66 to +72
CREATE TABLE courses (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(255),
description VARCHAR(1000),
difficulty VARCHAR(255),
vet_id INTEGER
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🚨🚨🚨 ID 및 FK 컬럼 타입이 팀 컨벤션을 따르지 않습니다.

idvet_id 컬럼 타입이 팀 컨벤션에 따라 'bigint'가 아닌 'INTEGER'로 정의되어 있습니다. 이는 필수 컨벤션이므로 반드시 수정해야 합니다.

 CREATE TABLE courses (
-  id          INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+  id          BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
   name        VARCHAR(255),
   description VARCHAR(1000),
   difficulty  VARCHAR(255),
-  vet_id      INTEGER
+  vet_id      BIGINT
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE courses (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(255),
description VARCHAR(1000),
difficulty VARCHAR(255),
vet_id INTEGER
);
CREATE TABLE courses (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(255),
description VARCHAR(1000),
difficulty VARCHAR(255),
vet_id BIGINT
);

public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
System.out.println(request);

Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

createCourse 메서드 매개변수 오류가 있습니다.

courseService.createCourse 호출 시 세 번째 매개변수로 request.getDifficulty() 대신 request.getDescription()이 잘못 전달되고 있습니다.

-		Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());
+		Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDifficulty(), request.getVetId());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());
Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDifficulty(), request.getVetId());

Comment on lines +23 to +29
public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
System.out.println(request);

Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());

return ResponseEntity.ok(course.getId());
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리가 없습니다.

현재 메서드에는 예외 처리 로직이 없습니다. 예를 들어, 존재하지 않는 VetId로 인한 오류나 DB 관련 예외를 처리하는 로직이 필요합니다.

 	@PostMapping()
 	public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
 		System.out.println(request);
 
-		Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());
-
-		return ResponseEntity.ok(course.getId());
+		try {
+			Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDifficulty(), request.getVetId());
+			return ResponseEntity.ok(course.getId());
+		} catch (Exception e) {
+			log.error("Course creation failed: {}", e.getMessage(), e);
+			return ResponseEntity.badRequest().build();
+		}
 	}

더 나은 해결책은 예외 유형에 따라 적절한 응답을 반환하는 글로벌 예외 핸들러를 구현하는 것입니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
System.out.println(request);
Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDescription(), request.getVetId());
return ResponseEntity.ok(course.getId());
}
@PostMapping()
public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
System.out.println(request);
try {
Course course = courseService.createCourse(request.getName(), request.getDescription(), request.getDifficulty(), request.getVetId());
return ResponseEntity.ok(course.getId());
} catch (Exception e) {
log.error("Course creation failed: {}", e.getMessage(), e);
return ResponseEntity.badRequest().build();
}
}

Copy link
Copy Markdown
Collaborator Author

@yakpoong yakpoong Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API 에서 발생하는 에러를 Controller 마다 catch 절로 처리하지 않고, 공통 관심사로 묶어서 처리하고 싶습니다. 어떻게 하면 API 에서 발생하는 예외들을 공통 관심사의 로직으로 처리할 수 있을까요 ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yakpoong Spring에서는 @RestControllerAdvice@ExceptionHandler를 사용하여 API 예외를 중앙에서 처리할 수 있습니다. 구현 방법을 보여드리겠습니다:

  1. 먼저 커스텀 예외 클래스를 만듭니다:
package org.springframework.samples.petclinic.exception;

import lombok.Getter;

@Getter
public class ResourceNotFoundException extends RuntimeException {
    private final String resourceName;
    private final String fieldName;
    private final Object fieldValue;

    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }
}
  1. 다른 비즈니스 예외들도 추가합니다:
package org.springframework.samples.petclinic.exception;

public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}
  1. 글로벌 예외 처리기를 구현합니다:
package org.springframework.samples.petclinic.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        log.error("Resource not found exception: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.NOT_FOUND.value(),
            "Resource Not Found",
            ex.getMessage(),
            request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ErrorResponse> handleBadRequestException(
            BadRequestException ex, WebRequest request) {
        log.error("Bad request exception: {}", ex.getMessage());
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.BAD_REQUEST.value(),
            "Bad Request",
            ex.getMessage(),
            request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex, WebRequest request) {
        log.error("Unhandled exception: {}", ex.getMessage(), ex);
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Internal Server Error",
            ex.getMessage(),
            request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
  1. 에러 응답 DTO를 정의합니다:
package org.springframework.samples.petclinic.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
}
  1. 이제 Controller에서는 try-catch 없이 예외를 발생시키면 됩니다:
@PostMapping()
public ResponseEntity<Integer> createCourse(@RequestBody CreateCourseRequest request) {
    // 유효성 검사
    if (request.getName() == null || request.getName().trim().isEmpty()) {
        throw new BadRequestException("Course name cannot be empty");
    }
    
    // 서비스 호출
    Course course = courseService.createCourse(
        request.getName(), 
        request.getDescription(), 
        request.getDifficulty(), 
        request.getVetId()
    );
    
    return ResponseEntity.ok(course.getId());
}
  1. CourseService도 수정합니다:
public Course createCourse(String name, String description, String difficulty, Integer vetId) {
    // vetRepository.findById()가 빈 Optional을 반환할 경우 예외 발생
    Vet vet = vetRepository.findById(vetId)
        .orElseThrow(() -> new ResourceNotFoundException("Vet", "id", vetId));
    
    Course course = new Course();
    course.setName(name);
    course.setDescription(description);
    course.setDifficulty(difficulty);
    course.setInstructor(vet);
    
    return courseRepository.save(course);
}

이 방식을 사용하면 모든 컨트롤러에서 공통적으로 발생하는 예외 처리 로직을 중앙화할 수 있습니다.

Copy link
Copy Markdown
Collaborator Author

@yakpoong yakpoong Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai 예외 처리 로직 중앙화처리는 별도의 Issue를 통해 해결하고 싶습니다. 이슈를 생성해 주세요.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@yakpoong 예외 처리 로직 중앙화에 대한 Issue를 생성해드렸습니다. Issue에는 다음 내용이 포함되어 있습니다:

  1. 현재 문제점: 컨트롤러마다 개별적으로 예외 처리
  2. 구현 방안: @RestControllerAdvice@ExceptionHandler 활용
  3. 기대 효과: 코드 중복 제거, 일관된 오류 응답 등
  4. PR 코멘트 참조 링크

해당 Issue는 yakpoong님께 할당되었습니다. 추가적인 요구사항이 있으시면 말씀해주세요.


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for centralized exception handling
gh issue create \
  --title "예외 처리 로직 중앙화 구현" \
  --body "## 개요
현재 API에서 발생하는 예외들이 각 Controller에서 개별적으로 처리되고 있습니다. 이를 공통 관심사로 분리하여 중앙화된 예외 처리 메커니즘을 구현해야 합니다.

## 구현 방안
- `@RestControllerAdvice`와 `@ExceptionHandler`를 사용한 글로벌 예외 처리기 구현
- 비즈니스 로직에 맞는 커스텀 예외 클래스 정의 (예: ResourceNotFoundException, BadRequestException 등)
- 표준화된 에러 응답 DTO 정의
- Controller에서 try-catch 블록 제거 및 명시적 예외 발생 패턴 적용

## 기대 효과
- 코드 중복 제거
- 일관된 에러 응답 포맷
- Controller 코드의 가독성 향상
- 예외 처리 로직 변경 시 유지보수 용이성 증가

## 참조
- PR #1 코멘트: https://github.com/inflearn/spring-petclinic/pull/1#discussion_r2009014022" \
  --assignee "yakpoong" \
  --repo "inflearn/spring-petclinic"

Length of output: 893


@yakpoong 요청하신 대로 예외 처리 로직 중앙화에 대한 이슈를 성공적으로 생성했습니다! 이슈는 다음 링크에서 확인하실 수 있습니다: #2

이슈는 yakpoong님께 할당되었으며, PR #1의 코멘트를 참조 링크로 포함했습니다.

Comment on lines +12 to +17
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CourseService {
private final CouresRepository courseRepository;
private final VetRepository vetRepository;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Repository 이름에 오타가 있습니다.

코드에 CouresRepository라고 되어 있는데, 이는 CourseRepository의 오타로 보입니다. 이로 인해 애플리케이션 시작 시 Bean 주입 오류가 발생할 수 있습니다.

-	private final CouresRepository courseRepository;
+	private final CourseRepository courseRepository;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CourseService {
private final CouresRepository courseRepository;
private final VetRepository vetRepository;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CourseService {
private final CourseRepository courseRepository;
private final VetRepository vetRepository;

Comment on lines +20 to +27
@Transactional()
public Course createCourse(String name, String description, String difficulty, Integer vetId) {
Collection<Vet> all = vetRepository.findAll();

all.forEach(course -> {
System.out.println(course.getId());
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

디버깅 코드를 제거하세요.

프로덕션 코드에 System.out.println과 같은 디버깅 코드가 남아있습니다. 또한, 모든 Vet을 조회하는 코드는 실제 기능에 필요하지 않아 보입니다.

람다 표현식에서 파라미터 이름이 course로 되어 있지만, 실제로는 Vet 객체입니다. 이는 코드 가독성을 저하시킵니다.

-	@Transactional()
+	@Transactional
 	public Course createCourse(String name, String description, String difficulty, Integer vetId) {
-		Collection<Vet> all = vetRepository.findAll();
-
-		all.forEach(course -> {
-			System.out.println(course.getId());
-		});
-
 		Vet instructor = vetRepository.findById(vetId)
 			.orElseThrow(() -> new RuntimeException("Vet not found"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional()
public Course createCourse(String name, String description, String difficulty, Integer vetId) {
Collection<Vet> all = vetRepository.findAll();
all.forEach(course -> {
System.out.println(course.getId());
});
@Transactional
public Course createCourse(String name, String description, String difficulty, Integer vetId) {
Vet instructor = vetRepository.findById(vetId)
.orElseThrow(() -> new RuntimeException("Vet not found"));

@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn deleted a comment from coderabbitai bot Mar 26, 2025
@inflearn inflearn locked as resolved and limited conversation to collaborators Mar 26, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant