diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..ba9c991 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,57 @@ +name: Deploy to EC2 with Docker + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission to gradlew + run: chmod +x gradlew + working-directory: week8/hyunjin-crud-api + + - name: Build with Gradle + run: ./gradlew build --no-daemon + working-directory: week8/hyunjin-crud-api + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + run: | + docker buildx build \ + --platform linux/amd64 \ + -t ${{ secrets.DOCKER_USERNAME }}/hyunjin-crud-api:latest \ + --push . + working-directory: week8/hyunjin-crud-api + + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + docker pull ${{ secrets.DOCKER_USERNAME }}/hyunjin-crud-api:latest + docker stop spring-app || true + docker rm spring-app || true + docker run -d --name spring-app -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/hyunjin-crud-api:latest diff --git "a/week6/[6\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" "b/week6/[6\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" new file mode 100644 index 0000000..ad9aa78 --- /dev/null +++ "b/week6/[6\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" @@ -0,0 +1,250 @@ +### Spring MVC Lifecycle +(Request) => Filter -> DispatcherServlet -> HandlerMapping -> HandlerInterceptor -> Controller -> Service -> Repository -> ViewResolver + + +- **Filter** + - 웹 요청이 들어올 때 맨 처음 가로채서, 필요한 검사나 설정을 하고 난 후 다음 단계로 넘기는 역할 + - 인증/인가: 로그인 했는지 확인 후, 안했으면 리다이렉트 + - 로깅: 요청 URL, 요청자 IP 등을 로그로 남김 + - 인코딩 설정 + - 보안 검사: XSS나 SQL Injection에 대한 간단 필터링 + - SQL Injection: 사용자가 쿼리문 안에 악의적인 코드를 주입해 DB를 속이는 공격 + - CORS 설정 + + +- **DispatcherServlet** + - HandlerMapping에게 요청을 어떤 Controller가 처리해야 하는지를 물어봄 + - HandlerMapping이 알려준 Controller를 호출함 + +- **HandlerMapping** + - DispatcherServlet이 받은 요청에 대해 어떤 Controller가 처리할지를 결정함 + +- **HandlerInterceptor** + - Controller 앞에 있는 문지기 역할 + - 다음 타이밍에 개입 가능 + - preHandle(): 컨트롤러가 실행되기 직전 + - posthandle(): 컨트롤러 실행 직후, 뷰 렌더링 전 + - afterCompletion(): 뷰 렌더링까지 완료된 후 + + - 흐름 순서 + - 요청 -> Filter -> DispatcherServlet -> HandlerInterceptor (preHandle) -> Controller 실행 -> HandlerInterceptor (postHandle) -> View 렌더링 -> HandlerInterceptor (afterCompletion) -> 응답 반환 + +- **Controller** + - 요청과 매핑되는 곳 + - 어떤 로직으로 처리할 것인지 결정하고 그에 맞는 Service를 호출함 + +- **Service** + - 데이터 처리 및 가공을 위한 비즈니스 로직을 수행 + - 요청에 대한 실질적인 로직을 수행함 + - Repository를 통해 DB에 접근해 데이터의 CRUD를 처리함 + +- **Repository** + - DB에 접근하는 객체 + - DAO(Data Access Ovject)의 발전된 개념 + - DB 접근 뿐 아니라 도메인 중심 아키텍처에 어울리게 설계된 Spring 스타일의 DAO + - Service에서 DB에 접근할 수 있게 해 데이터의 CRUD를 도와줌 + +- **ViewResolver** + - Controller에서 준 뷰의 이름을 DispatcherServlet으로부터 넘겨받음 + - 해당 뷰를 렌더링하고 DispatcherServlet으로 리턴 + - DispatcherServlet에서는 해당 뷰 화면을 응답 + - View 없이 데이터만 전달하는 경우 ViewResolver는 불필요함 + + +### Dispatcher servlet의 역할 +1. 요청 수신 + - 클라이언트의 HTTP 요청을 Filter 다음으로 받음 +2. 요청 URL에 따라 HandlerMapping에게 어떤 Controller가 해당 요청을 처리할지 물어봄 +3. HandlerAdapter 호출 + - 해당 Controller를 실제로 실행할 수 있는 방법을 HandlerAdapter에게 위임해 호출함 +4. Controller 실행 +5. ViewResolver로 어떤 화면을 보여줄지 결정 +6. 클라이언트에게 최종 응답 + +### Bean이란? +스프링이 관리하는 객체(인스턴스) +- 개발자가 new 키워드로 직접 만드는 객체가 아니라 스프링이 자동으로 생성하고 관리하는 객체 + + +- 특징 + - 객체의 생명주기를 스프링이 책임짐 + - 필요할 때 자동 주입 가능 + - 전역적으로 공유되어 사용됨 + +``` +@Component // Bean으로 등록됨 +public class UserService { + ... +} + +``` + +``` +@Configuration +public class AppConfig { + @Bean + public UserService userService() { + return new UserService(); + } +} +``` + + +### Bean Lifecycle +스프링 컨테이너가 Bean을 생성/초기화/사용/소멸시키기까지의 전 과정 + +1. 객체 생성 + - 스프링이 객체 인스턴스를 만듦 +2. 의존성 주입 + - @Autowired, 생성자 주입 등을 통해 필요한 의존 객체를 주입 +3. 초기화 + - 초기 설정 작업 진행 +4. 사용 + - 실제로 해당 Bean을 다른 컴포넌트에서 사용 +5. 소멸 + - @PreDstrosy 등이 호출되어 정리 작업이 수행됨 + + +### Spring 어노테이션 10가지와 그에 대한 설명 +**@Component** +- 스프링이 Bean으로 등록할 수 있게 해주는 클래스 표시용 어노테이션 + +**@Service** +- 비즈니스 로직을 담는 서비스 계층 클래스에 붙임 + - 내부적으로는 @Component와 같지만 의미적으로 구분함 + +**@Repository** +- DAO 역할을 하는 클래스에 붙임 + - @Component와 같지만, 데이터 접근 예외를 스프링 예외로 변환해줌 + +**@Controller** +- 웹 요청을 처리하는 클래스에 붙임 +- 스프링 MVC에서 클라이언트 요청을 받는 진입점 + +**@RestController** +- @Controller + @ResponseBody +- JSON 형태로 데이터를 반환하는 API 작성 시 사용 + +**@Autowired** +- 필요한 Bean을 자동 주입 +- 생성자, 필드, 메서드에 붙일 수 있음 + +**@Qualifier** +- @Autowired와 함께 사용할 때, 여러 Bean 중 어떤 Bean을 주입할 지 이름으로 지정함 + +**@Value** +- 환경 변수에서 값을 읽어 주입할 때 사용 + +**@Configuration** +- 설정 클래스를 나타냄 +- 내부에서 @Bean을 사용해 수동으로 Bean을 등록하는 것이 가능 + +**@Bean** +- 메서드에 붙임 + - 직접 생성한 객체를 Bean으로 등록할 때 사용 +- @Configuration 클래스 내부에서 사용함 + + +### Spring 의존성 주입 방식 +- **생성자 주입(Constructor Injection)**: 생성자를 통해 의존성을 주입 (생성자에 @Autowired) + + ``` + @Component + public class OrderService { + + private final PaymentService paymentService; + + @Autowired // 생략 가능 (스프링 4.3 이상이면 1개 생성자는 자동으로 주입됨) + public OrderService(PaymentService paymentService) { + this.paymentService = paymentService; + } + } + ``` + - 장점 + - final로 불변성을 보장 + - mock 객체를 주입하는 것이 가능해져 테스트 용이 + - 컴파일 시간에 오류가 잡힘 + - 단점 + - 의존성이 많을 때 생성자의 매개변수가 길어짐 + + +- **필드 주입(Field Injection)**: @Autowired로 필드에 직접 주입 + + ``` + @Component + public class OrderService { + + @Autowired + private PaymentService paymentService; + } + ``` + - 장점 + - 코드가 짧아서 간단한 구조를 가짐 + - 러닝커브가 작음 + - 단점 + - private이기 때문에 mock으로 주입이 어려워 테스트에 어려움 + - final을 사용하지 못하기 때문에 불변성이 보장 X + - 스프링 컨테이너가 없이는 사용이 불가하기 때문에 다른 환경에서의 사용이 어려워짐 + + +- **세터 주입(Setter Injection)**: @Autowired가 붙은 setter 메서드로 주입 + + ``` + @Component + public class OrderService { + + private PaymentService paymentService; + + @Autowired + public void setPaymentService(PaymentService paymentService) { + this.paymentService = paymentService; + } + } + ``` + - 장점 + - 선택적으로 의존성을 주입하는 것이 가능 + - setter를 통해 Mock을 주입하는 것이 가능하기 때문에 테스트가 가능 + - 단점 + - 의존성이 필수인지 선택인지 명확하지 않음 + - 객체가 완전히 세팅되기 전까지는 불안정한 상태임 + + +- **일반 메서드 주입(Method Injection)**: 직접 만든 메서드에 의존성 주입 + + ``` + @Component + public class OrderService { + + private PaymentService paymentService; + + @Autowired + public void init(PaymentService paymentService) { + this.paymentService = paymentService; + } + } + ``` + - 장점 + - 설정 처리나 초기화 로직 등과 함께 의존성을 주입할 때 유용 + - 단점 + - 코드 가독성이 떨어짐 + + +### 생성자 주입 방식(중요) +필요한 의존 객체를 생성자를 통해 주입받는 방식 => 객체가 생성될 때 부터 의존성을 강제하고, 안정적으로 주입받는 것이 가능 + +- Lombok 라이브러리 활용 시 + +``` + @RequiredArgsConstructor + @Service + public class OrderService { + + private final UserRepository userRepository; + private final OrderRepository orderRepository; + + // 생성자가 자동 생성되고, 스프링이 알아서 주입해줌 + } +``` +-> final 필드만 생성자 파라미터로 포함됨 => 불변성이 보장됨 + +=> 스프링이 권장하는 가장 안전하고 테스트하기 쉬운 의존성 주입 방식 diff --git "a/week7/[7\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" "b/week7/[7\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" new file mode 100644 index 0000000..ff7b7b1 --- /dev/null +++ "b/week7/[7\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" @@ -0,0 +1,112 @@ +## JPA 개념 + +### JPA와 JDBC의 차이란? + +- JDBC(Java DB Connectivity): 자바에서 DB와 통신하기 위한 가장 기본적 api + - 역할: 자바 코드로 SQL을 작성하고 직접 실행하며 결과를 받아옴 + - 사용법 -> 직접 SQL문을 작성하고 Connection 등을 수동으로 개발자가 관리해야 함 + + +- JPA(Java Persistence API): 자바 객체를 DB에 저장하고 관리하게 해주는 추상화된 ORM 표준 api + - 역할: SQL을 직접 작성하지 않고, entity 클래스를 통해 데이터를 관리함 + - 사용법 -> @Entity, @Id, @column과 같은 어노테이션으로 DB 테이블과 객체를 연결함 + + +- 차이점 + - JDBC는 SQL 중심, JPA는 객체(entity) 중심 + - JDBC는 트랜잭션을 수동으로 직접 처리, JPA는 JPA가 트랜잭션을 자동으로 관리해줌 + - 코드 복잡도: JDBC -> 높음 / JPA -> 낮음 + - 유지보수: JDBC가 JPA보다 불편함 + + +### ORM이란 무엇이고 JPA가 ORM 프레임워크에서 어떤 역할을 하는지 + +- ORM(Object-Relational Mapping): 자바 객체(클래스)와 데이터베이스의 테이블을 자동으로 매핑해주는 기술 + - 직접 SQL을 쓰는 대신 자바 객체만 다루면 내부에서 알아서 SQL로 변환해 DB랑 통신해줌 + + +- JPA가 ORM에서 하는 역할 + - entity 매핑 (자바 클래스와 DB 테이블을 연결) + - CRUD 자동화 (SQL 없이 수행) + - 트랜잭션을 더 쉽게 처리 + + +- Hibernate: JPA를 구현한 실제 라이브러리 + +## 엔티티 +JPA에서 DB table에 매핑되는 자바 클래스 -> 하나의 entity는 DB의 한 테이블과 1:1로 매칭됨 + +- DB의 테이블 구조를 자바 객체로 표현해줌 +- DB row를 자바 인스턴스로 다룸 +- JPA가 entity를 통해 SQL을 자동으로 생성해 DB와 통신함 + +### 엔티티 필수 어노테이션의 종류는? + +- @Entity: 이 클래스가 JPA가 관리할 엔티티임을 선언해줌 +- @Id: 엔티티의 pk 지정 +- @GeneratedValue: pk의 자동 생성 전략 설정 +- @Column: 필드와 테이블의 컬럼을 매핑 + +### JPA에서 엔티티의 필수 조건은? + +1. @Entity 어노테이션 필수 +2. 기본 생성자(public / protected)가 있어야 함 +3. final 클래스/메서드 사용 금지 +4. 필드 또는 프로퍼티의 접근 방식을 일관되게 작성해야 함 +5. 식별자 == @Id가 반드시 있어야 함 + +### 엔티티의 생명주기란? + +- 비영속: 엔티티 객체는 생성되었지만 아직 영속성 컨텍스트에 저장되지 않은 상태 +- 영속: 영속성 컨텍스트에 저장되어 JPA가 관리하는 상태 +- 준영속: 영속성 컨텍스트에서 지운 상태 +- 삭제: 실제 DB 삭제를 요청한 상태 + +## 영속성 컨텍스트 + +- entity 객체들을 저장하고 관리하는 일종의 메모리 공간 +- JPA를 사용해 entity를 DB에 저장하기 전에 항상 영속성 컨텍스트에 먼저 저장해야 함 + - 영속성 컨텍스트에 entity를 저장한다고 해서 DB에 저장되는 것은 아님 + +- 필요성 + - 같은 트랜잭션 내에서 객체와 DB를 1:1로 연결해 중복 쿼리를 방지함 + - 변경 사항을 추적해 자동으로 DB에 반영할 수 있도록 도와줌 + +### 영속성 컨텍스트 상태의 종류는? + +- 트랜잭션 스코프: 트랜잭션 단위로 영속성 컨텍스트가 생성되고 종료됨 +- 확장 스코프: 트랜잭션을 넘어 지속적으로 유지됨 + +## 연간관계 +두 entity 간의 참조 관계 (RDB에서 테이블 간의 fk 관계) + +### 연간관계의 종류와 JPA에서의 표현법은? +1. 방향에 따른 분류 + - 단방향 연관관계 (a->b, b는 a를 알지 못함) + - 양방향 연관관계 (a와 b가 서로 참조가 가능함) + - 반드시 한 쪽을 연관관계의 주인으로 설정해야 함. (fk를 설정하는 쪽이 한 군데여야하기 때문에) + +2. 다중성에 따른 분류 + - 1:1 -> 한 개당 한 개 + ``` + @OneToOne + @JoinColumn(name="id") + ``` + + - 1:n -> 한 개당 여러 개 + ``` + @OneToMany (mappedBy="member") + ``` + + - n:1 -> 여러 개가 한 개를 참조 (가장 많이 쓰임) + ``` + @ManyToOne + @JoinColumn(name="id") + ``` + + - n:m -> 여러 개가 여러 개와 연결 (잘 안쓰임) + ``` + @ManyToMany + @JoinTable(name="student_sub", joinCOlumns=@JoinColumn(name="student_id"), inverseJoinColumns = @JoinColumn(name="sub_id")) + ``` + diff --git "a/week8/[8\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" "b/week8/[8\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" new file mode 100644 index 0000000..5db5c64 --- /dev/null +++ "b/week8/[8\354\243\274\354\260\250]\354\213\240\355\230\204\354\247\204.md" @@ -0,0 +1,131 @@ +### REST API +REST: REpresentational State Transfer + +- REST 구성 + - 자원: URI + - 행위: HTTP Method(GET, POST, PATCH, DELETE) + - 표현: Representation + - 어떤 자원의 특정 시점의 상태를 반영하고 있는 정보 + +-> 자원을 이름(=자원의 표현)으로 구분해 해당 자원의 상태를 주고 받는 모든 것 +=> 자원의 표현에 의한 상태 전달 + +- REST API: REST 규칙을 지킨 API + +### REST API 설계 주의점 (동사를 써도 되는 경우) +- URI는 정보의 자원을 표현해야 한다. + - `/createUser` (X) `/users` (O) +- 자원은 복수형을 사용한다. +- 계층적으로 구조표현을 한다. + - `/users/123/orders/456` +- 필터링, 페이지네이션 등은 쿼리 파라미터를 통한다. +- 에러처리에 명확한 HTTP 상태코드를 사용한다. +- 자원에 대한 행위는 HTTP Method로 표현한다. + - 행위는 URI에 포함하지 않는다. + + +- **동사를 써도 되는 경우** + - 특수한 행위나 행동 자체가 목적일 때 + - POST `/orders/456/cancel` + +### HTTP 특징 +- 클라이언트 서버 구조 + - 클라이언트는 서버에 Request를 보내고, Response를 대기한다. + - 서버가 Request에 대한 결과를 만들어 Response한다. + + -> 양쪽이 독립적으로 진행할 수 있다. + + +- stateless(무상태): 서버가 클라이언트의 상태를 보존하지 않음 + - 고객: 이 토마토 얼마에요? + - 점원: 100만원이에요 + - 고객: 토마토 2개 구매할게요 + - 점원: 토마토 2개는 200만원이에요 + + -> 요청할 때 필요한 데이터를 모두 포함해서 서버에게 요청하게 됨 + + - 특징 + - 갑자기 클라이언트의 요청이 증가해도 서버를 많이 투입할 수 있음 + - 응답 서버를 쉽게 바꿀 수 있음 + - 한계 + - 모든 것을 stateless로 설계할 수 있는 경우도 있고 없는 경우도 있음 + - stateless: 서비스 소개 화면 + - stateful: 로그인 + - 데이터를 너무 많이 보냄 + + +- Connectionless(비연결성) + - 자원에 대한 요청을 주고 받을 때만 연결을 유지해 최소한으로 사용하고, 클라이언트와 요청-응답 과정 후 연결을 끊음 + + -> 서버 자원을 효율적으로 사용할 수 있음 + + - 한계 + - TCP/IP 연결을 새로 맺어야 하기 때문에 3way handshake 시간이 추가됨 + -> HTTP 지속 연결(Persistent Connections)로 문제를 해결함 + + +- 단순하고 확장이 가능함 + +### HTTP 메소드 8가지 +- GET: 자원 조회 + - 서버 상태를 바꾸지 않음 + - 멱등성 가짐 (여러 번 호출해도 결과가 같음) +- POST: 주로 등록에 사용함 + - 비멱등성 가짐 (같은 요청을 여러 번 시도하면 자원의 중복 생성이 가능) +- PUT: 자원을 대체하고, 해당하는 자원이 없다면 새로 생성 +- PATCH: 자원을 부분적으로 변경함 +- DELETE: 자원 삭제 +- HEAD: GET 요청과 동일하지만 body를 제외한 헤더 정보만 응답해줌 +- OPTIONS: 요청 가능한 HTTP 메서드 목록을 조회 + - 서버가 허용하는 동작을 확인하기 위한 용도로 사용 +- TRACE: 클라이언트 -> 서버로 가는 요청을 그대로 반사해 확인 + - 경로를 추적해 네트워크 문제를 확인하는데, 실제 사용은 거의 없음 + +### DB + +#### 정규화 3단계 +1. 1NF(Normal Form) + - 각 컬럼은 더 이상 쪼갤 수 없는 원자값만 가져야 함 -> 하나의 셀에는 하나의 값만 존재해야 함 +2. 2NF + - 1NF를 만족하면서 기본 키의 부분 집합에 종속된 속성을 제거 + - ex) 과목명은 과목 코드에만 종속되어있고 학번과는 무관한데 하나의 테이블에 학번,과목코드,과목명이 함께 있다면 + (PK = 학번, 과목 코드) + -> 학번-과목 코드 테이블/과목코드-과목명 테이블로 분리 + +3. 3NF + - 2NF를 만족하면서 기본 키가 아닌 컬럼에 의해 결정되는 다른 컬럼을 제거 + - ex) 부서명은 부서 ID에 의해 결정되는데 한 테이블에 사원 ID, 부서 ID, 부서명이 있다면 + (PK = 사원 ID) + -> 사원 ID-부서 ID 테이블/부서 ID-부서명 테이블로 분리 + + +#### 1:1 +- A 테이블의 하나의 레코드는 B 테이블의 하나의 레코드와 정확히 연결됨 + + ex) 한 사람은 하나의 주민등록증만 가짐 + +#### 1:M +- A 테이블의 하나의 레코드는 B 테이블의 여러 레코드와 연결될 수 있음 + + ex) 한 고객이 여러 개의 주문을 할 수 있음 + +#### M:N +- A와 B 테이블 각각의 레코드가 서로 여러 개와 연결될 수 있음 + + ex) 학생은 여러 강의를 들을 수 있고, 한 강의에 여러 학생이 참여할 수 있음 + +#### PK, FK +- PK(Primary Key): 테이블 내에서 각 row를 유일하게 식별하는 컬럼 + - 특징 + - 유일성: 중복된 값이 없어야 함 + - null값이 될 수 없음 + - 하나의 테이블에 한 개만 존재할 수 있음 + - 여러 컬럼을 조합해서 만들 수도 있음 (복합 PK) + + +- FK(Foreign Key): 다른 테이블의 PK를 참조하는 컬럼 + - 테이블 간의 관계를 나타내기 위해서 사용 + - 특징 + - 참조 무결성 유지: 존재하지 않는 PK 값을 FK로 가질 수 없음 + - null이나 중복 가능 + - 하나의 테이블에 여러 개 존재할 수 있음 diff --git a/week8/hyunjin-crud-api/Dockerfile b/week8/hyunjin-crud-api/Dockerfile new file mode 100644 index 0000000..cf889e0 --- /dev/null +++ b/week8/hyunjin-crud-api/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:17-jdk-jammy +VOLUME /tmp +ARG JAR_FILE=build/libs/*jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/week8/hyunjin-crud-api/build.gradle b/week8/hyunjin-crud-api/build.gradle new file mode 100644 index 0000000..8ff9b70 --- /dev/null +++ b/week8/hyunjin-crud-api/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.5' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'practice' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + developmentOnly 'org.springframework.boot:spring-boot-docker-compose' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.jar b/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9bbc975 Binary files /dev/null and b/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.jar differ diff --git a/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.properties b/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/week8/hyunjin-crud-api/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/week8/hyunjin-crud-api/gradlew b/week8/hyunjin-crud-api/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/week8/hyunjin-crud-api/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/week8/hyunjin-crud-api/gradlew.bat b/week8/hyunjin-crud-api/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/week8/hyunjin-crud-api/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/week8/hyunjin-crud-api/settings.gradle b/week8/hyunjin-crud-api/settings.gradle new file mode 100644 index 0000000..496e64c --- /dev/null +++ b/week8/hyunjin-crud-api/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'hyunjin-crud-api' diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/HyunjinCrudApiApplication.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/HyunjinCrudApiApplication.java new file mode 100644 index 0000000..5c3488c --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/HyunjinCrudApiApplication.java @@ -0,0 +1,13 @@ +package practice.hyunjincrudapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HyunjinCrudApiApplication { + + public static void main(String[] args) { + SpringApplication.run(HyunjinCrudApiApplication.class, args); + } + +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/controller/CommentController.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/controller/CommentController.java new file mode 100644 index 0000000..5b39a52 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/controller/CommentController.java @@ -0,0 +1,37 @@ +package practice.hyunjincrudapi.comment.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import practice.hyunjincrudapi.comment.dto.request.CreateCommentRequest; +import practice.hyunjincrudapi.comment.dto.request.UpdateCommentRequest; +import practice.hyunjincrudapi.comment.dto.response.CommentResponse; +import practice.hyunjincrudapi.comment.service.CommentService; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class CommentController { + private final CommentService commentService; + + @PostMapping("/comments") + public void createComment(@RequestBody CreateCommentRequest request) { + commentService.createComment(request); + } + + @GetMapping("/posts/{postId}/comments") + public List getCommentsByPostId(@PathVariable Long postId) { + return commentService.getCommentsByPostId(postId); + } + + @PatchMapping("/comments/{id}") + public void updateComment(@PathVariable Long id, @RequestBody @Valid UpdateCommentRequest request) { + commentService.updateComment(id, request); + } + + @DeleteMapping("/comments/{id}") + public void deleteComment(@PathVariable Long id) { + commentService.deleteComment(id); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/CreateCommentRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/CreateCommentRequest.java new file mode 100644 index 0000000..99aa5e3 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/CreateCommentRequest.java @@ -0,0 +1,21 @@ +package practice.hyunjincrudapi.comment.dto.request; + +import lombok.Getter; +import practice.hyunjincrudapi.comment.entity.Comment; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.post.entity.Post; + +@Getter +public class CreateCommentRequest { + private String content; + private Long memberId; + private Long postId; + + public Comment toEntity(Member member, Post post) { + return Comment.builder() + .content(content) + .member(member) + .post(post) + .build(); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/UpdateCommentRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/UpdateCommentRequest.java new file mode 100644 index 0000000..090c228 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/request/UpdateCommentRequest.java @@ -0,0 +1,10 @@ +package practice.hyunjincrudapi.comment.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class UpdateCommentRequest { + @NotBlank(message = "댓글 내용은 비어 있을 수 없습니다.") + private String content; +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/response/CommentResponse.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/response/CommentResponse.java new file mode 100644 index 0000000..a07e66c --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/dto/response/CommentResponse.java @@ -0,0 +1,18 @@ +package practice.hyunjincrudapi.comment.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import practice.hyunjincrudapi.comment.entity.Comment; + +@Getter +@AllArgsConstructor +public class CommentResponse { + private Long id; + private String content; + private Long memberId; + private Long postId; + + public static CommentResponse from(Comment comment) { + return new CommentResponse(comment.getId(), comment.getContent(), comment.getMember().getId(), comment.getPost().getId()); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/entity/Comment.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/entity/Comment.java new file mode 100644 index 0000000..328b992 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/entity/Comment.java @@ -0,0 +1,41 @@ +package practice.hyunjincrudapi.comment.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.post.entity.Post; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Comment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="comment_id") + private Long id; + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="post_id", nullable = false) + private Post post; + + @Builder + public Comment(String content, Member member, Post post) { + this.content = content; + this.member = member; + this.post = post; + } + + public void updateContent(String content) { + if(content!=null){ + this.content = content; + } + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/repository/JpaCommentRepository.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/repository/JpaCommentRepository.java new file mode 100644 index 0000000..4c5ce3b --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/repository/JpaCommentRepository.java @@ -0,0 +1,19 @@ +package practice.hyunjincrudapi.comment.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import practice.hyunjincrudapi.comment.entity.Comment; + +import java.util.List; + + +public interface JpaCommentRepository extends JpaRepository { + @Query(""" + SELECT c FROM Comment c + JOIN FETCH c.member + JOIN FETCH c.post + WHERE c.post.id = :postId + """) + List findByPostId(@Param("postId")Long postId); +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/service/CommentService.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/service/CommentService.java new file mode 100644 index 0000000..1161dd9 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/comment/service/CommentService.java @@ -0,0 +1,60 @@ +package practice.hyunjincrudapi.comment.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import practice.hyunjincrudapi.comment.dto.request.CreateCommentRequest; +import practice.hyunjincrudapi.comment.dto.request.UpdateCommentRequest; +import practice.hyunjincrudapi.comment.dto.response.CommentResponse; +import practice.hyunjincrudapi.comment.entity.Comment; +import practice.hyunjincrudapi.comment.repository.JpaCommentRepository; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.member.repository.JpaMemberRepository; +import practice.hyunjincrudapi.post.entity.Post; +import practice.hyunjincrudapi.post.repository.JpaPostRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class CommentService { + private final JpaCommentRepository commentRepository; + private final JpaMemberRepository memberRepository; + private final JpaPostRepository postRepository; + + public void createComment(CreateCommentRequest request) { + Member member = memberRepository + .findById(request.getMemberId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + Post post = postRepository.findById(request.getPostId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시글입니다.")); + + Comment comment = request.toEntity(member, post); + + commentRepository.save(comment); + } + + public List getCommentsByPostId(Long postId){ + return commentRepository.findByPostId(postId).stream() + .map(CommentResponse::from) + .toList(); + } + + public void updateComment(Long commentId, UpdateCommentRequest request) { + Comment comment = commentRepository + .findById(commentId) + .orElseThrow(()-> new IllegalArgumentException(("존재하지 않는 댓글입니다."))); + + comment.updateContent(request.getContent()); + } + + public void deleteComment(Long commentId){ + Comment comment = commentRepository + .findById(commentId) + .orElseThrow(()-> new IllegalArgumentException(("존재하지 않는 댓글입니다."))); + + commentRepository.delete(comment); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/controller/MemberController.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/controller/MemberController.java new file mode 100644 index 0000000..c941662 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/controller/MemberController.java @@ -0,0 +1,35 @@ +package practice.hyunjincrudapi.member.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import practice.hyunjincrudapi.member.dto.request.CreateMemberRequest; +import practice.hyunjincrudapi.member.dto.request.UpdateMemberRequest; +import practice.hyunjincrudapi.member.dto.response.MemberResponse; +import practice.hyunjincrudapi.member.service.MemberService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members1") +public class MemberController { + private final MemberService memberService; + + @PostMapping + public void signup(@RequestBody CreateMemberRequest createMemberRequest) { + memberService.signup(createMemberRequest); + } + + @GetMapping("/{id}") + public MemberResponse getMember(@PathVariable Long id) { + return memberService.getMember(id); + } + + @PutMapping("/{id}") + public void updateMember(@PathVariable Long id, @RequestBody UpdateMemberRequest updateMemberRequest) { + memberService.updateMember(id, updateMemberRequest); + } + + @DeleteMapping("/{id}") + public void deleteMember(@PathVariable Long id) { + memberService.deleteMember(id); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/CreateMemberRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/CreateMemberRequest.java new file mode 100644 index 0000000..26e8052 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/CreateMemberRequest.java @@ -0,0 +1,19 @@ +package practice.hyunjincrudapi.member.dto.request; + +import lombok.Getter; +import practice.hyunjincrudapi.member.entity.Member; +@Getter +public class CreateMemberRequest { + private String name; + private String password; + private String email; + + public Member toEntity(){ //toEntity 메서드를 통해 새로운 객체를 생성하게 됨 + return Member + .builder() + .name(name) + .password(password) + .email(email) + .build(); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/UpdateMemberRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/UpdateMemberRequest.java new file mode 100644 index 0000000..fcf8d9c --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/request/UpdateMemberRequest.java @@ -0,0 +1,9 @@ +package practice.hyunjincrudapi.member.dto.request; + +import lombok.Getter; + +@Getter +public class UpdateMemberRequest { + private String name; + private String email; +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/response/MemberResponse.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/response/MemberResponse.java new file mode 100644 index 0000000..45ba47f --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/dto/response/MemberResponse.java @@ -0,0 +1,17 @@ +package practice.hyunjincrudapi.member.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import practice.hyunjincrudapi.member.entity.Member; + +@Getter +@AllArgsConstructor +public class MemberResponse { + private Long id; + private String name; + private String email; + + public static MemberResponse from(Member member) { + return new MemberResponse(member.getId(), member.getName(), member.getEmail()); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/entity/Member.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/entity/Member.java new file mode 100644 index 0000000..3a0806e --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/entity/Member.java @@ -0,0 +1,49 @@ +package practice.hyunjincrudapi.member.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import practice.hyunjincrudapi.comment.entity.Comment; +import practice.hyunjincrudapi.post.entity.Post; + +import java.util.ArrayList; +import java.util.List; + + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + private String name; + private String password; + @Column(unique = true, nullable = false) //unique=true -> unique한 값으로 지정 + private String email; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List posts = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List comments = new ArrayList<>(); + + @Builder + public Member(String name, String password, String email) { + this.name = name; + this.password = password; + this.email = email; + } + + public void update(String name, String email){ + if(name!=null){ + this.name = name; + } + if(email!=null){ + this.email = email; + } + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/repository/JpaMemberRepository.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/repository/JpaMemberRepository.java new file mode 100644 index 0000000..838e367 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/repository/JpaMemberRepository.java @@ -0,0 +1,8 @@ +package practice.hyunjincrudapi.member.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import practice.hyunjincrudapi.member.entity.Member; + +public interface JpaMemberRepository extends JpaRepository { + +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/service/MemberService.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/service/MemberService.java new file mode 100644 index 0000000..c5887d4 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/member/service/MemberService.java @@ -0,0 +1,47 @@ +package practice.hyunjincrudapi.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import practice.hyunjincrudapi.member.dto.request.CreateMemberRequest; +import practice.hyunjincrudapi.member.dto.request.UpdateMemberRequest; +import practice.hyunjincrudapi.member.dto.response.MemberResponse; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.member.repository.JpaMemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final JpaMemberRepository jpaMemberRepository; + + public void signup(CreateMemberRequest createMemberRequest) { + Member member = createMemberRequest.toEntity(); + jpaMemberRepository.save(member); //member에 대한 정보가 db에 저장됨 + } + + public MemberResponse getMember(Long id){ + Member member = jpaMemberRepository + .findById(id) + .orElseThrow(()-> new IllegalArgumentException(("존재하지 않는 멤버입니다."))); + + return MemberResponse.from(member); + } + + public void updateMember(Long id, UpdateMemberRequest updateMemberRequest) { + Member member= jpaMemberRepository + .findById(id) + .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + member.update(updateMemberRequest.getName(), updateMemberRequest.getEmail()); + } + + public void deleteMember(Long id) { + Member member = jpaMemberRepository + .findById(id) + .orElseThrow(()-> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + jpaMemberRepository.delete(member); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/controller/PostController.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/controller/PostController.java new file mode 100644 index 0000000..fbc2a20 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/controller/PostController.java @@ -0,0 +1,43 @@ +package practice.hyunjincrudapi.post.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import practice.hyunjincrudapi.post.dto.request.CreatePostRequest; +import practice.hyunjincrudapi.post.dto.request.UpdatePostRequest; +import practice.hyunjincrudapi.post.dto.response.PostResponse; +import practice.hyunjincrudapi.post.service.PostService; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +public class PostController { + private final PostService postService; + + @PostMapping + public void uploadPost(@RequestBody CreatePostRequest createPostRequest) { + postService.createPost(createPostRequest); + } + + @GetMapping + public List getAllPosts(){ + return postService.getAllPosts(); + } + + @GetMapping("/{id}") + public PostResponse getPostById(@PathVariable Long id) { + return postService.getPostById(id); + } + + @PutMapping("/{id}") + public void updatePost(@PathVariable Long id, @RequestBody @Valid UpdatePostRequest updatePostRequest) { + postService.updatePost(id, updatePostRequest); + } + + @DeleteMapping("/{id}") + public void deletePost(@PathVariable Long id) { + postService.deletePost(id); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/CreatePostRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/CreatePostRequest.java new file mode 100644 index 0000000..90927b5 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/CreatePostRequest.java @@ -0,0 +1,20 @@ +package practice.hyunjincrudapi.post.dto.request; + +import lombok.Getter; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.post.entity.Post; + +@Getter +public class CreatePostRequest { + private String title; + private String content; + private Long memberId; + + public Post toEntity(Member member) { + return Post.builder() + .title(title) + .content(content) + .member(member) + .build(); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/UpdatePostRequest.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/UpdatePostRequest.java new file mode 100644 index 0000000..b25d8b1 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/request/UpdatePostRequest.java @@ -0,0 +1,12 @@ +package practice.hyunjincrudapi.post.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class UpdatePostRequest { + @NotBlank(message = "제목을 입력해주세요.") + private String title; + @NotBlank(message = "내용을 입력해주세요.") + private String content; +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/response/PostResponse.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/response/PostResponse.java new file mode 100644 index 0000000..17d6486 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/dto/response/PostResponse.java @@ -0,0 +1,18 @@ +package practice.hyunjincrudapi.post.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import practice.hyunjincrudapi.post.entity.Post; + +@Getter +@AllArgsConstructor +public class PostResponse { + private Long id; + private String title; + private String content; + private Long memberId; + + public static PostResponse from(Post post) { + return new PostResponse(post.getId(), post.getTitle(), post.getContent(), post.getMember().getId()); + } +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/entity/Post.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/entity/Post.java new file mode 100644 index 0000000..c7cc453 --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/entity/Post.java @@ -0,0 +1,49 @@ +package practice.hyunjincrudapi.post.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import practice.hyunjincrudapi.comment.entity.Comment; +import practice.hyunjincrudapi.member.entity.Member; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="post_id") + private Long id; + private String title; + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + @Builder + public Post(String title, String content, Member member) { + this.title = title; + this.content = content; + this.member = member; + } + + public void updatePost(String title, String content) { + if(title!=null){ + this.title = title; + } + + if(content!=null){ + this.content = content; + } + } + +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/repository/JpaPostRepository.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/repository/JpaPostRepository.java new file mode 100644 index 0000000..076910a --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/repository/JpaPostRepository.java @@ -0,0 +1,8 @@ +package practice.hyunjincrudapi.post.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import practice.hyunjincrudapi.post.entity.Post; + +public interface JpaPostRepository extends JpaRepository { + +} diff --git a/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/service/PostService.java b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/service/PostService.java new file mode 100644 index 0000000..39974af --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/java/practice/hyunjincrudapi/post/service/PostService.java @@ -0,0 +1,60 @@ +package practice.hyunjincrudapi.post.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import practice.hyunjincrudapi.member.entity.Member; +import practice.hyunjincrudapi.member.repository.JpaMemberRepository; +import practice.hyunjincrudapi.post.dto.request.CreatePostRequest; +import practice.hyunjincrudapi.post.dto.request.UpdatePostRequest; +import practice.hyunjincrudapi.post.dto.response.PostResponse; +import practice.hyunjincrudapi.post.entity.Post; +import practice.hyunjincrudapi.post.repository.JpaPostRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostService { + private final JpaPostRepository postRepository; + private final JpaMemberRepository memberRepository; + + public void createPost(CreatePostRequest request) { + Member member = memberRepository.findById(request.getMemberId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + Post post = request.toEntity(member); + postRepository.save(post); + } + + public List getAllPosts(){ + return postRepository.findAll().stream() + .map(PostResponse::from) + .collect(Collectors.toList()); + } + + public PostResponse getPostById(Long id) { + return postRepository.findById(id) + .map(PostResponse::from) + .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다.")); + } + + public void updatePost(Long id, UpdatePostRequest request) { + Post post = postRepository + .findById(id) + .orElseThrow(()-> new IllegalArgumentException(("해당 게시글이 존재하지 않습니다."))); + + post.updatePost(request.getTitle(), request.getContent()); + } + + public void deletePost(Long id) { + Post post = postRepository + .findById(id) + .orElseThrow(()-> new IllegalArgumentException(("해당 게시글이 존재하지 않습니다."))); + + postRepository.delete(post); + } +} + diff --git a/week8/hyunjin-crud-api/src/main/resources/application.yml b/week8/hyunjin-crud-api/src/main/resources/application.yml new file mode 100644 index 0000000..1ca5b9c --- /dev/null +++ b/week8/hyunjin-crud-api/src/main/resources/application.yml @@ -0,0 +1,36 @@ +spring: + application: + name: name-crud-api2 + jpa: + open-in-view: true + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + highlight_sql: true + datasource: + url: jdbc:mysql://cow-database.chmg4e88k39w.ap-northeast-2.rds.amazonaws.com/cow?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + driver-class-name: com.mysql.cj.jdbc.Driver + username: admin + password: tls2530. + + docker: + compose: + file: compose.yaml + enabled: true + lifecycle-management: none + start: + command: up + stop: + command: down + timeout: 1m + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace