diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..4f39817f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,63 @@ +name: deploy + +on: + push: + branches: + - 'letskuku' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash + + + - name: Build with Gradle + run: ./gradlew bootJar + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + + - name : Build Docker Image & Push to Docker Hub + run: | + docker build . -t ${{ secrets.DOCKER_HUB_USERNAME }}/spring-daagn-market-18th + docker build ./proxy -t ${{ secrets.DOCKER_HUB_USERNAME }}/nginx + docker push ${{ secrets.DOCKER_HUB_USERNAME }}/spring-daagn-market-18th + docker push ${{ secrets.DOCKER_HUB_USERNAME }}/nginx + + + - name: executing remote ssh commands using password + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ubuntu + key: ${{ secrets.KEY }} + script: | + cd /home/ubuntu/ + sudo touch .env + echo "${{ secrets.ENV_VARS }}" | sudo tee .env > /dev/null + sudo touch docker-compose.yml + echo "${{ vars.DOCKER_COMPOSE }}" | sudo tee docker-compose.yml > /dev/null + sudo chmod 666 /var/run/docker.sock + sudo docker rm -f $(docker ps -qa) + sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/spring-daagn-market-18th + sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/nginx + docker-compose -f docker-compose.yml --env-file ./.env up -d + docker image prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba6491b4..69b556c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ HELP.md .gradle build/ -gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..536f0ae5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# open jdk java 17 버전 환경 +FROM openjdk:17 + +# JAR_FILE 변수 정의 +ARG JAR_FILE=/build/libs/*.jar + +# JAR 파일 메인 디렉토리에 복사 +COPY ${JAR_FILE} app.jar + +# 시스템 진입점 정의 +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 8115ae25..4ef516d8 100644 --- a/README.md +++ b/README.md @@ -1041,3 +1041,201 @@ public ResponseEntity deleteMember(@AuthenticationPrincipal CustomUserDeta ``` - AuthenticationPrincipal을 통해 인증된 사용자의 정보를 받아올 수 있다. ![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/616ec6b4-90c8-4f93-a3d8-225462fe5ccf) + +----- +# 📁CEOS 18th Backend Study - 5주차 미션 +### 1️⃣ 로컬에서 도커 실행해보기 +#### 📌 도커 이미지 빌드 +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/c815271f-7485-45eb-a087-d64d2750071e) +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/b70673e8-b400-4491-a1a5-2d4e027ead93) + +#### 📌 빌드한 이미지 실행 +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/4b0b540b-c770-40e1-9a14-791f5c8dfe0f) + +#### ✔ 이미지 실행 관련 오류 해결 +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/bf69cb83-e9f9-4774-951f-7e050ae7eb1d) + +➡ db connection 관련 오류 발생 + +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/3556a4ac-3f8b-46c7-90d0-bb04fa74704f) + +- 도커 실행 시, localhost는 도커 컨테이너 자기 자신을 가리키게 된다. +- localhost 대신 host.docker.internal 사용하면 해결 가능 + +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/5c47d4ed-bf03-490f-b841-c50fb74b73c8) + +#### 📌 docker-compose.yml 실행 +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/009311cd-a43a-449e-aea9-36a13caed222) +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/e6ec23e0-6646-4a44-8f10-72319ffe675f) + +#### ✔ docker-compose 실행 관련 오류 해결 +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/ba4ae615-6047-4059-b612-d812dcb49c8d) + +➡ db 관련 오류 발생 + +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/9473d4b2-7ec5-4417-8952-e268f435c605) + +- 이미 로컬에서 3306 포트를 사용하고 있기 때문에 3306:3306으로 포트를 설정하면 제대로 동작하지 않는다. +- 포트번호 변경하면 해결 가능 + +![image](https://github.com/letskuku/spring-daagn-market-18th/assets/90572599/eab1b032-a7f4-4725-a709-5250e172b452) + +### 2️⃣ API 추가 구현 및 리팩토링 + +#### 1. 원하는 도메인/기능을 골라 API를 추가해주세요 + +생성/수정/삭제 등 자유롭게 원하는 API를 구현해주시면 됩니다🤓🤓 + +#### 📌 Post API 추가 +```java +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/post") +public class PostController { + + private final PostService postService; + + @PostMapping + public ResponseEntity createPost(@RequestBody CreatePostRequest createPostRequest) { + + postService.createPost(createPostRequest); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getPost(postId)); + } + + @GetMapping + public ResponseEntity> getAllPosts() { + return ResponseEntity.ok(postService.getAllPosts()); + } + + @DeleteMapping("/{postId}") + public ResponseEntity deletePost(@PathVariable Long postId) { + + postService.deletePost(postId); + + return ResponseEntity.ok().build(); + } +} +``` + +#### 2. 지금까지 과제를 하면서 아쉬웠던 부분이나 더 고치고 싶은 부분을 리팩토링 해주세요 + +#### 📌 권한 관리 위한 enum Role 추가 및 관련 코드 수정 + +```java +@Getter +public enum Role { + + ROLE_ADMIN, + ROLE_USER +} +``` + +#### 📌 CustomUserDetails의 isEnabled 메서드 반환값 알맞게 수정 +``` java +@Override +public boolean isEnabled() { + return member.getActivated(); +} +``` + +#### 📌 예외 처리 방식 변경 + +❓ 이전 방식 (Member) + +- Member 객체를 찾지 못했을 때를 위한 MemberNotFoundException 생성 +```java +public class MemberNotFoundException extends RuntimeException { + public MemberNotFoundException() { + super("회원 정보가 존재하지 않습니다."); + } + public MemberNotFoundException(Long id) { + super("요청한 id 값 " + id + "에 해당하는 회원 정보가 존재하지 않습니다."); + } + + public MemberNotFoundException(String email) { + super("요청한 email " + email + "에 해당하는 회원 정보가 존재하지 않습니다."); + } +} +``` + +- Member 관련 예외를 처리하는 MemberExceptionController 생성하여 MemberNotFoundException 처리 +```java +@Slf4j +@RestControllerAdvice +public class MemberExceptionController { + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity catchMemberNotFoundException(MemberNotFoundException e) { + + log.error(e.getMessage()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } +} +``` + +➡ Member 관련 커스텀 예외를 새로 생성할 때마다 처리 위한 catch~ 메서드 생성해야 한다. + +❓ 새로운 방식 + +- Member 관련 예외 정보 가지는 MemberErrorCode enum 생성 +```java +@Getter +public enum MemberErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 회원을 찾을 수 없습니다."); + + private final HttpStatus errorCode; + private final String errorMessage; + + MemberErrorCode(HttpStatus errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} +``` + +- Member 관련 예외 만들어주는 MemberExcpetion 생성 +```java +@Getter +public class MemberException extends RuntimeException { + + private final HttpStatus errorCode; + + public MemberException(MemberErrorCode errorCode) { + super(errorCode.getErrorMessage()); + this.errorCode = errorCode.getErrorCode(); + } + + public MemberException(MemberErrorCode errorCode, Long id) { + super(errorCode.getErrorMessage() + " 요청받은 id : " + id); + this.errorCode = errorCode.getErrorCode(); + } + + public MemberException(MemberErrorCode errorCode, String email) { + super(errorCode.getErrorMessage() + " 요청받은 email : " + email); + this.errorCode = errorCode.getErrorCode(); + } +} +``` + +- 모든 예외 관리하는 ExceptionController 생성하여 MemberExcpetion 처리 +- log에 stackTrace 정보 찍히도록 두번째 인자로 MemberException 객체 포함 +```java +@Slf4j +@RestControllerAdvice +public class ExceptionController { + + @ExceptionHandler(MemberException.class) + public ResponseEntity catchMemberException(MemberException e) { + log.error(e.getMessage(), e); + return ResponseEntity.status(e.getErrorCode()).body(e.getMessage()); + } +} +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f63d78d4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3" + +services: + backend: + image: "letskuku/spring-daagn-market-18th" + env_file: + - .env + ports: + - "8080:8080" + restart: "always" + environment: + - TZ=Asia/Seoul + + nginx: + image: "letskuku/nginx" + ports: + - "80:80" + restart: "always" + depends_on: + - "backend" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..033e24c4 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 00000000..fa9c7791 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx +RUN rm -rf /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/proxy/nginx.conf b/proxy/nginx.conf new file mode 100644 index 00000000..5cc065de --- /dev/null +++ b/proxy/nginx.conf @@ -0,0 +1,12 @@ +server { + listen 80; + server_name *.compute.amazonaws.com; + + location / { + proxy_pass http://backend:8080; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} \ No newline at end of file diff --git a/src/main/java/com/ceos18/springboot/global/config/WebSecurityConfig.java b/src/main/java/com/ceos18/springboot/global/config/SecurityConfig.java similarity index 97% rename from src/main/java/com/ceos18/springboot/global/config/WebSecurityConfig.java rename to src/main/java/com/ceos18/springboot/global/config/SecurityConfig.java index dd338122..3e706e4b 100644 --- a/src/main/java/com/ceos18/springboot/global/config/WebSecurityConfig.java +++ b/src/main/java/com/ceos18/springboot/global/config/SecurityConfig.java @@ -18,7 +18,7 @@ @EnableWebSecurity @Configuration @RequiredArgsConstructor -public class WebSecurityConfig { +public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -37,7 +37,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests( requests -> requests.requestMatchers("/api/member/signup", "/api/member/login").permitAll() - .requestMatchers("/api/member/admin").hasRole("ADMIN") + .requestMatchers("/api/member/admin").hasRole("ROLE_ADMIN") .anyRequest().authenticated() ); diff --git a/src/main/java/com/ceos18/springboot/global/exception/ExceptionController.java b/src/main/java/com/ceos18/springboot/global/exception/ExceptionController.java new file mode 100644 index 00000000..05e456e6 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/global/exception/ExceptionController.java @@ -0,0 +1,32 @@ +package com.ceos18.springboot.global.exception; + +import com.ceos18.springboot.member.exception.MemberException; +import com.ceos18.springboot.post.exception.PostException; +import com.ceos18.springboot.town.exception.TownException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ExceptionController { + + @ExceptionHandler(MemberException.class) + public ResponseEntity catchMemberException(MemberException e) { + log.error(e.getMessage(), e); + return ResponseEntity.status(e.getErrorCode()).body(e.getMessage()); + } + + @ExceptionHandler(PostException.class) + public ResponseEntity catchPostException(PostException e) { + log.error(e.getMessage(), e); + return ResponseEntity.status(e.getErrorCode()).body(e.getMessage()); + } + + @ExceptionHandler(TownException.class) + public ResponseEntity catchTownException(TownException e) { + log.error(e.getMessage(), e); + return ResponseEntity.status(e.getErrorCode()).body(e.getMessage()); + } +} diff --git a/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetails.java b/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetails.java index 846f2595..9006fe18 100644 --- a/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetails.java +++ b/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetails.java @@ -21,7 +21,7 @@ public Member getMember() { public Collection getAuthorities() { return member.getRoles() .stream() - .map(SimpleGrantedAuthority::new) + .map(role -> new SimpleGrantedAuthority(role.toString())) .toList(); } @@ -52,6 +52,6 @@ public boolean isCredentialsNonExpired() { @Override public boolean isEnabled() { - return true; + return member.getActivated(); } } diff --git a/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetailsService.java b/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetailsService.java index 56432f0b..edab9f5e 100644 --- a/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetailsService.java +++ b/src/main/java/com/ceos18/springboot/global/jwt/CustomUserDetailsService.java @@ -1,7 +1,8 @@ package com.ceos18.springboot.global.jwt; import com.ceos18.springboot.member.domain.Member; -import com.ceos18.springboot.member.exception.MemberNotFoundException; +import com.ceos18.springboot.member.exception.MemberErrorCode; +import com.ceos18.springboot.member.exception.MemberException; import com.ceos18.springboot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; @@ -19,7 +20,7 @@ public class CustomUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Member member = memberRepository.findByEmailAndActivated(username, true) - .orElseThrow(() -> new MemberNotFoundException(username)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND, username)); return new CustomUserDetails(member); } diff --git a/src/main/java/com/ceos18/springboot/member/application/MemberServiceImpl.java b/src/main/java/com/ceos18/springboot/member/application/MemberServiceImpl.java index b0323613..36e7e62d 100644 --- a/src/main/java/com/ceos18/springboot/member/application/MemberServiceImpl.java +++ b/src/main/java/com/ceos18/springboot/member/application/MemberServiceImpl.java @@ -7,7 +7,8 @@ import com.ceos18.springboot.member.dto.request.SignupMemberRequest; import com.ceos18.springboot.member.dto.response.LoginMemberResponse; import com.ceos18.springboot.member.dto.response.MemberResponse; -import com.ceos18.springboot.member.exception.MemberNotFoundException; +import com.ceos18.springboot.member.exception.MemberErrorCode; +import com.ceos18.springboot.member.exception.MemberException; import com.ceos18.springboot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -55,7 +56,7 @@ public List getAllMembers() { List memberList = memberRepository.findAllByActivated(true); if (memberList.isEmpty()) { - throw new MemberNotFoundException(); + throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND); } List memberResponseList = memberList.stream() @@ -68,7 +69,7 @@ public List getAllMembers() { public MemberResponse getMember(Long id) { Member member = memberRepository.findByIdAndActivated(id, true) - .orElseThrow(() -> new MemberNotFoundException(id)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND, id)); return MemberResponse.fromEntity(member); } @@ -77,7 +78,7 @@ public MemberResponse getMember(Long id) { public void deleteMember(Long id) { Member member = memberRepository.findByIdAndActivated(id, true) - .orElseThrow(() -> new MemberNotFoundException(id)); + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND, id)); member.updateActivatedFalse(); } diff --git a/src/main/java/com/ceos18/springboot/member/domain/Member.java b/src/main/java/com/ceos18/springboot/member/domain/Member.java index f79929cb..9aafb3c9 100644 --- a/src/main/java/com/ceos18/springboot/member/domain/Member.java +++ b/src/main/java/com/ceos18/springboot/member/domain/Member.java @@ -39,7 +39,7 @@ public class Member extends BaseEntity { private String imageUrl; @ElementCollection(fetch = FetchType.EAGER) - private List roles; + private List roles; @Column(nullable = false) private Boolean activated; @@ -53,7 +53,7 @@ public Member(String password, String nickname, String phone, String email, Stri this.email = email; this.imageUrl = imageUrl; this.roles = new ArrayList<>() {{ - add("USER"); + add(Role.ROLE_USER); }}; this.activated = true; } diff --git a/src/main/java/com/ceos18/springboot/member/domain/Role.java b/src/main/java/com/ceos18/springboot/member/domain/Role.java new file mode 100644 index 00000000..27f7a5b7 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/member/domain/Role.java @@ -0,0 +1,10 @@ +package com.ceos18.springboot.member.domain; + +import lombok.Getter; + +@Getter +public enum Role { + + ROLE_ADMIN, + ROLE_USER +} diff --git a/src/main/java/com/ceos18/springboot/member/exception/MemberErrorCode.java b/src/main/java/com/ceos18/springboot/member/exception/MemberErrorCode.java new file mode 100644 index 00000000..fc37d459 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/member/exception/MemberErrorCode.java @@ -0,0 +1,18 @@ +package com.ceos18.springboot.member.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum MemberErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 회원을 찾을 수 없습니다."); + + private final HttpStatus errorCode; + private final String errorMessage; + + MemberErrorCode(HttpStatus errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/com/ceos18/springboot/member/exception/MemberException.java b/src/main/java/com/ceos18/springboot/member/exception/MemberException.java new file mode 100644 index 00000000..c91c6b9a --- /dev/null +++ b/src/main/java/com/ceos18/springboot/member/exception/MemberException.java @@ -0,0 +1,25 @@ +package com.ceos18.springboot.member.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class MemberException extends RuntimeException { + + private final HttpStatus errorCode; + + public MemberException(MemberErrorCode errorCode) { + super(errorCode.getErrorMessage()); + this.errorCode = errorCode.getErrorCode(); + } + + public MemberException(MemberErrorCode errorCode, Long id) { + super(errorCode.getErrorMessage() + " 요청받은 id : " + id); + this.errorCode = errorCode.getErrorCode(); + } + + public MemberException(MemberErrorCode errorCode, String email) { + super(errorCode.getErrorMessage() + " 요청받은 email : " + email); + this.errorCode = errorCode.getErrorCode(); + } +} diff --git a/src/main/java/com/ceos18/springboot/member/exception/MemberNotFoundException.java b/src/main/java/com/ceos18/springboot/member/exception/MemberNotFoundException.java deleted file mode 100644 index 260971b3..00000000 --- a/src/main/java/com/ceos18/springboot/member/exception/MemberNotFoundException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ceos18.springboot.member.exception; - -public class MemberNotFoundException extends RuntimeException { - - public MemberNotFoundException() { - super("회원 정보가 존재하지 않습니다."); - } - - public MemberNotFoundException(Long id) { - super("요청한 id 값 " + id + "에 해당하는 회원 정보가 존재하지 않습니다."); - } - - public MemberNotFoundException(String email) { - super("요청한 email " + email + "에 해당하는 회원 정보가 존재하지 않습니다."); - } -} diff --git a/src/main/java/com/ceos18/springboot/member/presentation/MemberExceptionController.java b/src/main/java/com/ceos18/springboot/member/presentation/MemberExceptionController.java deleted file mode 100644 index 021d9c7a..00000000 --- a/src/main/java/com/ceos18/springboot/member/presentation/MemberExceptionController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ceos18.springboot.member.presentation; - -import com.ceos18.springboot.member.exception.MemberNotFoundException; -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; - -@Slf4j -@RestControllerAdvice -public class MemberExceptionController { - - @ExceptionHandler(MemberNotFoundException.class) - public ResponseEntity catchMemberNotFoundException(MemberNotFoundException e) { - - log.error(e.getMessage()); - - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } -} diff --git a/src/main/java/com/ceos18/springboot/post/application/PostService.java b/src/main/java/com/ceos18/springboot/post/application/PostService.java new file mode 100644 index 00000000..0319954c --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/application/PostService.java @@ -0,0 +1,17 @@ +package com.ceos18.springboot.post.application; + +import com.ceos18.springboot.post.dto.request.CreatePostRequest; +import com.ceos18.springboot.post.dto.response.PostResponse; + +import java.util.List; + +public interface PostService { + + void createPost(CreatePostRequest createPostRequest); + + PostResponse getPost(Long id); + + List getAllPosts(); + + void deletePost(Long id); +} diff --git a/src/main/java/com/ceos18/springboot/post/application/PostServiceImpl.java b/src/main/java/com/ceos18/springboot/post/application/PostServiceImpl.java new file mode 100644 index 00000000..f8ae6957 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/application/PostServiceImpl.java @@ -0,0 +1,84 @@ +package com.ceos18.springboot.post.application; + +import com.ceos18.springboot.member.domain.Member; +import com.ceos18.springboot.member.exception.MemberErrorCode; +import com.ceos18.springboot.member.exception.MemberException; +import com.ceos18.springboot.member.repository.MemberRepository; +import com.ceos18.springboot.post.domain.Category; +import com.ceos18.springboot.post.domain.Post; +import com.ceos18.springboot.post.dto.request.CreatePostRequest; +import com.ceos18.springboot.post.dto.response.PostResponse; +import com.ceos18.springboot.post.exception.PostErrorCode; +import com.ceos18.springboot.post.exception.PostException; +import com.ceos18.springboot.post.repository.PostRepository; +import com.ceos18.springboot.town.domain.Town; +import com.ceos18.springboot.town.exception.TownErrorCode; +import com.ceos18.springboot.town.exception.TownException; +import com.ceos18.springboot.town.repository.TownRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PostServiceImpl implements PostService { + + private final PostRepository postRepository; + private final MemberRepository memberRepository; + private final TownRepository townRepository; + + @Override + public void createPost(CreatePostRequest createPostRequest) { + + Member member = memberRepository.findByIdAndActivated(createPostRequest.getMemberId(), true) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND, createPostRequest.getMemberId())); + + Town town; + if (townRepository.existsByName(createPostRequest.getTownName())) { + town = townRepository.findByName(createPostRequest.getTownName()) + .orElseThrow(() -> new TownException(TownErrorCode.TOWN_NOT_FOUND)); + } else { + town = Town.builder() + .name(createPostRequest.getTownName()) + .latitude(createPostRequest.getLatitude()) + .longitude(createPostRequest.getLongitude()) + .build(); + + townRepository.save(town); + } + + Category category = Category.getCategoryByName(createPostRequest.getCategory()); + + postRepository.save(createPostRequest.toEntity(member, town, category)); + } + + @Override + public PostResponse getPost(Long id) { + + Post post = postRepository.findByIdAndActivated(id, true) + .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND, id)); + + return PostResponse.fromEntity(post); + } + + @Override + public List getAllPosts() { + + List postResponses = postRepository.findAll() + .stream() + .map(PostResponse::fromEntity) + .toList(); + + return postResponses; + } + + @Override + public void deletePost(Long id) { + + Post post = postRepository.findByIdAndActivated(id, true) + .orElseThrow(() -> new PostException(PostErrorCode.POST_NOT_FOUND, id)); + + post.updateActivatedToFalse(); + } +} diff --git a/src/main/java/com/ceos18/springboot/post/domain/Category.java b/src/main/java/com/ceos18/springboot/post/domain/Category.java index 00619d0c..23878cfd 100644 --- a/src/main/java/com/ceos18/springboot/post/domain/Category.java +++ b/src/main/java/com/ceos18/springboot/post/domain/Category.java @@ -1,21 +1,21 @@ package com.ceos18.springboot.post.domain; -import com.ceos18.springboot.global.common.BaseEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; +import com.ceos18.springboot.post.exception.PostErrorCode; +import com.ceos18.springboot.post.exception.PostException; import lombok.Getter; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PROTECTED) +import java.util.Arrays; + @Getter -@Entity -public class Category extends BaseEntity { +public enum Category { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "category_id") - private Long id; + SHIRT, + PANTS, + SHOES; - @Column(nullable = false) - private String name; + public static Category getCategoryByName(String name) { + return Arrays.stream(Category.values()) + .filter(category -> category.name().equalsIgnoreCase(name)) + .findAny().orElseThrow(() -> new PostException(PostErrorCode.CATEGORY_NOT_FOUND)); + } } diff --git a/src/main/java/com/ceos18/springboot/post/domain/Post.java b/src/main/java/com/ceos18/springboot/post/domain/Post.java index d302f93d..06658aed 100644 --- a/src/main/java/com/ceos18/springboot/post/domain/Post.java +++ b/src/main/java/com/ceos18/springboot/post/domain/Post.java @@ -5,6 +5,7 @@ import com.ceos18.springboot.town.domain.Town; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -42,7 +43,28 @@ public class Post extends BaseEntity { @JoinColumn(name = "town_id") private Town town; - @OneToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "category_id") + @Enumerated(EnumType.STRING) + @Column(name = "category", nullable = false) private Category category; + + @Column(nullable = false) + private Boolean activated; + + @Builder + public Post(String title, String content, Long cost, Boolean sharing, Status status, + Member member, Town town, Category category) { + this.title = title; + this.content = content; + this.cost = cost; + this.sharing = sharing; + this.status = status; + this.member = member; + this.town = town; + this.category = category; + this.activated = true; + } + + public void updateActivatedToFalse() { + this.activated = false; + } } diff --git a/src/main/java/com/ceos18/springboot/post/domain/Status.java b/src/main/java/com/ceos18/springboot/post/domain/Status.java index 5c1745c6..6a9ceba5 100644 --- a/src/main/java/com/ceos18/springboot/post/domain/Status.java +++ b/src/main/java/com/ceos18/springboot/post/domain/Status.java @@ -7,7 +7,7 @@ public enum Status { SELLING("판매중"), RESERVED("예약중"), - SOLDOUT("거래완료"); + SOLD_OUT("거래완료"); private final String value; diff --git a/src/main/java/com/ceos18/springboot/post/dto/request/CreatePostRequest.java b/src/main/java/com/ceos18/springboot/post/dto/request/CreatePostRequest.java new file mode 100644 index 00000000..f3a93688 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/dto/request/CreatePostRequest.java @@ -0,0 +1,35 @@ +package com.ceos18.springboot.post.dto.request; + +import com.ceos18.springboot.member.domain.Member; +import com.ceos18.springboot.post.domain.Category; +import com.ceos18.springboot.post.domain.Post; +import com.ceos18.springboot.post.domain.Status; +import com.ceos18.springboot.town.domain.Town; +import lombok.Getter; + +@Getter +public class CreatePostRequest { + + private String title; + private String content; + private Long cost; + private Boolean sharing; + private Long memberId; + private String townName; + private Double latitude; + private Double longitude; + private String category; + + public Post toEntity(Member member, Town town, Category category) { + return Post.builder() + .title(title) + .content(content) + .cost(cost) + .sharing(sharing) + .status(Status.SELLING) + .member(member) + .town(town) + .category(category) + .build(); + } +} diff --git a/src/main/java/com/ceos18/springboot/post/dto/response/PostResponse.java b/src/main/java/com/ceos18/springboot/post/dto/response/PostResponse.java new file mode 100644 index 00000000..d5508a0c --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/dto/response/PostResponse.java @@ -0,0 +1,49 @@ +package com.ceos18.springboot.post.dto.response; + +import com.ceos18.springboot.post.domain.Category; +import com.ceos18.springboot.post.domain.Post; +import com.ceos18.springboot.post.domain.Status; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PostResponse { + + private final Long postId; + private final String title; + private final Long cost; + private final Boolean sharing; + private final Status status; + private final Long memberId; + private final String memberNickname; + private final String townName; + private final Category category; + + @Builder + public PostResponse(Long postId, String title, Long cost, Boolean sharing, Status status, Long memberId, + String memberNickname, String townName, Category category) { + this.postId = postId; + this.title = title; + this.cost = cost; + this.sharing = sharing; + this.status = status; + this.memberId = memberId; + this.memberNickname = memberNickname; + this.townName = townName; + this.category = category; + } + + public static PostResponse fromEntity(Post post) { + return PostResponse.builder() + .postId(post.getId()) + .title(post.getTitle()) + .cost(post.getCost()) + .sharing(post.getSharing()) + .status(post.getStatus()) + .memberId(post.getMember().getId()) + .memberNickname(post.getMember().getNickname()) + .townName(post.getTown().getName()) + .category(post.getCategory()) + .build(); + } +} diff --git a/src/main/java/com/ceos18/springboot/post/exception/PostErrorCode.java b/src/main/java/com/ceos18/springboot/post/exception/PostErrorCode.java new file mode 100644 index 00000000..e741327e --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/exception/PostErrorCode.java @@ -0,0 +1,19 @@ +package com.ceos18.springboot.post.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum PostErrorCode { + + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 Post를 찾을 수 없습니다."), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 Category를 찾을 수 없습니다."); + + private final HttpStatus errorCode; + private final String errorMessage; + + PostErrorCode(HttpStatus errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/com/ceos18/springboot/post/exception/PostException.java b/src/main/java/com/ceos18/springboot/post/exception/PostException.java new file mode 100644 index 00000000..0832fdeb --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/exception/PostException.java @@ -0,0 +1,20 @@ +package com.ceos18.springboot.post.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class PostException extends RuntimeException { + + private final HttpStatus errorCode; + + public PostException(PostErrorCode errorCode) { + super(errorCode.getErrorMessage()); + this.errorCode = errorCode.getErrorCode(); + } + + public PostException(PostErrorCode errorCode, Long id) { + super(errorCode.getErrorMessage() + " : " + id); + this.errorCode = errorCode.getErrorCode(); + } +} diff --git a/src/main/java/com/ceos18/springboot/post/presentation/PostController.java b/src/main/java/com/ceos18/springboot/post/presentation/PostController.java new file mode 100644 index 00000000..e13f7e4c --- /dev/null +++ b/src/main/java/com/ceos18/springboot/post/presentation/PostController.java @@ -0,0 +1,45 @@ +package com.ceos18.springboot.post.presentation; + +import com.ceos18.springboot.post.application.PostService; +import com.ceos18.springboot.post.dto.request.CreatePostRequest; +import com.ceos18.springboot.post.dto.response.PostResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/post") +public class PostController { + + private final PostService postService; + + @PostMapping + public ResponseEntity createPost(@RequestBody CreatePostRequest createPostRequest) { + + postService.createPost(createPostRequest); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getPost(postId)); + } + + @GetMapping + public ResponseEntity> getAllPosts() { + return ResponseEntity.ok(postService.getAllPosts()); + } + + @DeleteMapping("/{postId}") + public ResponseEntity deletePost(@PathVariable Long postId) { + + postService.deletePost(postId); + + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/ceos18/springboot/post/repository/PostRepository.java b/src/main/java/com/ceos18/springboot/post/repository/PostRepository.java index 10edc821..a0acd2e1 100644 --- a/src/main/java/com/ceos18/springboot/post/repository/PostRepository.java +++ b/src/main/java/com/ceos18/springboot/post/repository/PostRepository.java @@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface PostRepository extends JpaRepository { + + Optional findByIdAndActivated(Long id, Boolean activated); } diff --git a/src/main/java/com/ceos18/springboot/town/exception/TownErrorCode.java b/src/main/java/com/ceos18/springboot/town/exception/TownErrorCode.java new file mode 100644 index 00000000..94b5d535 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/town/exception/TownErrorCode.java @@ -0,0 +1,18 @@ +package com.ceos18.springboot.town.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum TownErrorCode { + + TOWN_NOT_FOUND(HttpStatus.NOT_FOUND, "해당하는 Town을 찾을 수 없습니다."); + + private final HttpStatus errorCode; + private final String errorMessage; + + TownErrorCode(HttpStatus errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/com/ceos18/springboot/town/exception/TownException.java b/src/main/java/com/ceos18/springboot/town/exception/TownException.java new file mode 100644 index 00000000..bc1650e3 --- /dev/null +++ b/src/main/java/com/ceos18/springboot/town/exception/TownException.java @@ -0,0 +1,15 @@ +package com.ceos18.springboot.town.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class TownException extends RuntimeException { + + private final HttpStatus errorCode; + + public TownException(TownErrorCode errorCode) { + super(errorCode.getErrorMessage()); + this.errorCode = errorCode.getErrorCode(); + } +} diff --git a/src/main/java/com/ceos18/springboot/town/repository/TownRepository.java b/src/main/java/com/ceos18/springboot/town/repository/TownRepository.java index 1cc38a14..aa962388 100644 --- a/src/main/java/com/ceos18/springboot/town/repository/TownRepository.java +++ b/src/main/java/com/ceos18/springboot/town/repository/TownRepository.java @@ -4,6 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface TownRepository extends JpaRepository { + + Boolean existsByName(String name); + + Optional findByName(String name); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9fa2f2a8..b62208ab 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,17 +1,24 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/daangn - username: ${DATABASE_USERNAME} - password: ${DATABASE_PASSWORD} + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} jpa: database: mysql database-platform: org.hibernate.dialect.MySQL8Dialect hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.MySQL8Dialect show_sql: true format_sql: true + +jwt: + secret: ${JWT_SECRET} + access-expiration-time: 1800000 # 30분 + refresh-expiration-time: 1209600000 # 2주 + access-token-name: ACCESS-TOKEN + refresh-token-name: REFRESH-TOKEN \ No newline at end of file