├── backend ├── settings.gradle ├── lombok.config ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── main │ │ ├── resources │ │ │ ├── images │ │ │ │ └── test.png │ │ │ ├── application-credentials.yml.template │ │ │ ├── application-aws.yml.template │ │ │ ├── application-email.yml.template │ │ │ ├── application-db.yml.template │ │ │ ├── application.yml │ │ │ ├── static.docs │ │ │ │ └── api.adoc │ │ │ ├── application-auth.yml.template │ │ │ └── log4j2.xml │ │ └── java │ │ │ └── com │ │ │ └── hjjang │ │ │ └── backend │ │ │ ├── domain │ │ │ ├── post │ │ │ │ ├── service │ │ │ │ │ ├── PostService.java │ │ │ │ │ └── PostServiceImpl.java │ │ │ │ ├── exception │ │ │ │ │ └── PostNotFoundException.java │ │ │ │ ├── domain │ │ │ │ │ ├── repository │ │ │ │ │ │ └── PostRepository.java │ │ │ │ │ └── entity │ │ │ │ │ │ ├── PostState.java │ │ │ │ │ │ ├── PostDefaultValue.java │ │ │ │ │ │ └── Post.java │ │ │ │ ├── dto │ │ │ │ │ ├── PostRequestDto.java │ │ │ │ │ ├── PostResponseDto.java │ │ │ │ │ └── PostMapper.java │ │ │ │ └── controller │ │ │ │ │ └── PostController.java │ │ │ ├── user │ │ │ │ ├── entity │ │ │ │ │ ├── Agreement.java │ │ │ │ │ ├── ProviderType.java │ │ │ │ │ ├── AnonymousEmptyUser.java │ │ │ │ │ ├── RoleType.java │ │ │ │ │ ├── UserRefreshToken.java │ │ │ │ │ └── User.java │ │ │ │ ├── exception │ │ │ │ │ ├── UserDoesNotExistException.java │ │ │ │ │ ├── UserNotFoundException.java │ │ │ │ │ └── UserNotMatchException.java │ │ │ │ ├── repository │ │ │ │ │ ├── UserRepository.java │ │ │ │ │ └── UserRefreshTokenRepository.java │ │ │ │ ├── dto │ │ │ │ │ └── UserProfileDTO.java │ │ │ │ ├── controller │ │ │ │ │ └── UserController.java │ │ │ │ └── service │ │ │ │ │ └── UserProfileService.java │ │ │ ├── mail │ │ │ │ ├── dto │ │ │ │ │ ├── MailResponse.java │ │ │ │ │ ├── MailRequest.java │ │ │ │ │ └── Mail.java │ │ │ │ ├── exception │ │ │ │ │ ├── InvalidMailException.java │ │ │ │ │ ├── UnauthorizedException.java │ │ │ │ │ └── handler │ │ │ │ │ │ └── MailExceptionHandler.java │ │ │ │ ├── domain │ │ │ │ │ ├── MailRegex.java │ │ │ │ │ └── MailMessage.java │ │ │ │ ├── service │ │ │ │ │ ├── NoticeMailService.java │ │ │ │ │ └── MailService.java │ │ │ │ └── controller │ │ │ │ │ └── MailController.java │ │ │ ├── image │ │ │ │ ├── domain │ │ │ │ │ ├── repository │ │ │ │ │ │ └── ImageRepository.java │ │ │ │ │ └── entity │ │ │ │ │ │ └── Image.java │ │ │ │ ├── service │ │ │ │ │ ├── Path.java │ │ │ │ │ ├── LocalImageUploader.java │ │ │ │ │ ├── ImageUploader.java │ │ │ │ │ └── ImageService.java │ │ │ │ ├── exception │ │ │ │ │ └── ImageNotFoundException.java │ │ │ │ ├── dto │ │ │ │ │ └── ImagePathResponse.java │ │ │ │ └── controller │ │ │ │ │ └── ImageController.java │ │ │ ├── trade │ │ │ │ ├── domain │ │ │ │ │ ├── repositroy │ │ │ │ │ │ └── TradeRepository.java │ │ │ │ │ └── entity │ │ │ │ │ │ ├── TradeState.java │ │ │ │ │ │ └── Trade.java │ │ │ │ ├── dto │ │ │ │ │ ├── TradeRequestDto.java │ │ │ │ │ ├── TradeResponseDto.java │ │ │ │ │ └── TradeMapper.java │ │ │ │ ├── exception │ │ │ │ │ └── TradeNotFoundException.java │ │ │ │ └── service │ │ │ │ │ └── TradeService.java │ │ │ ├── category │ │ │ │ ├── domain │ │ │ │ │ ├── repository │ │ │ │ │ │ └── CategoryRepository.java │ │ │ │ │ └── entity │ │ │ │ │ │ └── Category.java │ │ │ │ ├── exception │ │ │ │ │ └── CategoryNotFoundException.java │ │ │ │ ├── service │ │ │ │ │ ├── CategoryService.java │ │ │ │ │ └── CategoryServiceImpl.java │ │ │ │ ├── dto │ │ │ │ │ ├── CategoryResponse.java │ │ │ │ │ └── CategoryRequest.java │ │ │ │ └── controller │ │ │ │ │ └── CategoryController.java │ │ │ ├── search │ │ │ │ ├── service │ │ │ │ │ ├── SearchService.java │ │ │ │ │ └── SearchServiceImpl.java │ │ │ │ ├── repository │ │ │ │ │ └── SearchRepository.java │ │ │ │ └── controller │ │ │ │ │ └── SearchController.java │ │ │ └── university │ │ │ │ ├── repository │ │ │ │ └── UniversityRepository.java │ │ │ │ ├── service │ │ │ │ └── UniversityService.java │ │ │ │ └── entity │ │ │ │ └── University.java │ │ │ ├── global │ │ │ ├── dto │ │ │ │ ├── ApiResponseHeader.java │ │ │ │ └── ApiResponse.java │ │ │ ├── util │ │ │ │ ├── HttpStatusResponseEntity.java │ │ │ │ ├── HeaderUtil.java │ │ │ │ ├── UserUtil.java │ │ │ │ └── CookieUtil.java │ │ │ ├── security │ │ │ │ ├── exception │ │ │ │ │ ├── TokenValidFailedException.java │ │ │ │ │ └── RestAuthenticationEntryPoint.java │ │ │ │ ├── parser │ │ │ │ │ ├── ParsingUserContextFactory.java │ │ │ │ │ ├── ParsingUserContext.java │ │ │ │ │ └── KakaoParsingParsingUserContext.java │ │ │ │ ├── AuthProviderConfig.java │ │ │ │ ├── handler │ │ │ │ │ ├── TokenAccessDeniedHandler.java │ │ │ │ │ └── OAuth2AuthenticationFailureHandler.java │ │ │ │ ├── filter │ │ │ │ │ └── TokenAuthenticationFilter.java │ │ │ │ ├── token │ │ │ │ │ ├── AuthTokenProvider.java │ │ │ │ │ └── AuthToken.java │ │ │ │ ├── service │ │ │ │ │ └── CustomOAuth2UserService.java │ │ │ │ ├── repository │ │ │ │ │ └── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ └── principal │ │ │ │ │ └── UserPrincipal.java │ │ │ ├── execption │ │ │ │ ├── handler │ │ │ │ │ └── GlobalExceptionHandler.java │ │ │ │ └── BusinessException.java │ │ │ ├── domain │ │ │ │ └── BaseTimeEntity.java │ │ │ ├── response │ │ │ │ ├── code │ │ │ │ │ ├── SuccessCode.java │ │ │ │ │ └── ErrorCode.java │ │ │ │ └── response │ │ │ │ │ ├── SuccessResponse.java │ │ │ │ │ └── ErrorResponse.java │ │ │ └── config │ │ │ │ ├── ApplicationStartupTask.java │ │ │ │ ├── security │ │ │ │ └── properties │ │ │ │ │ └── AuthProperties.java │ │ │ │ └── SwaggerConfig.java │ │ │ ├── BackendApplication.java │ │ │ └── infra │ │ │ └── aws │ │ │ ├── config │ │ │ └── AmazonS3Config.java │ │ │ └── service │ │ │ └── S3ImageUploader.java │ └── test │ │ ├── java │ │ └── com │ │ │ └── hjjang │ │ │ └── backend │ │ │ ├── BackendApplicationTests.java │ │ │ ├── global │ │ │ └── security │ │ │ │ ├── WithMockCustomUser.java │ │ │ │ ├── CustomSecurityExtension.java │ │ │ │ └── WithMockUserSecurityContextFactory.java │ │ │ ├── domain │ │ │ ├── user │ │ │ │ ├── controller │ │ │ │ │ └── docs │ │ │ │ │ │ └── UserRestDocument.java │ │ │ │ ├── repository │ │ │ │ │ └── UserRepositoryTest.java │ │ │ │ └── service │ │ │ │ │ └── UserProfileServiceTest.java │ │ │ └── post │ │ │ │ ├── domain │ │ │ │ ├── entity │ │ │ │ │ └── PostTest.java │ │ │ │ └── repository │ │ │ │ │ └── PostRepositoryTest.java │ │ │ │ └── dto │ │ │ │ └── PostMapperTest.java │ │ │ └── email │ │ │ └── MailServiceTest.java │ │ └── resources │ │ └── log4j2-test.xml ├── Dockerfile ├── .gitignore ├── gradlew.bat ├── HELP.md └── build.gradle ├── .gitignore ├── frontend ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── src │ ├── images │ │ └── kakao_login_large_wide.png │ ├── page │ │ ├── MyPage.js │ │ ├── SignUpPage.js │ │ ├── ItemDetailPage.js │ │ ├── ItemListPage.js │ │ ├── RouterPage.js │ │ └── LoginPage.js │ ├── App.js │ ├── setupTests.js │ ├── App.test.js │ ├── auth │ │ ├── OAuth.js │ │ └── OAuth2Redirecthandler.js │ ├── index.css │ ├── reportWebVitals.js │ ├── index.js │ ├── component │ │ └── NavBar.js │ ├── App.css │ └── logo.svg ├── Dockerfile ├── frontend.iml ├── package.json ├── .gitignore └── README.md ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── docker-compose.yml └── readme.md /backend/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'backend' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | 3 | .idea 4 | 5 | naver-intellij-formatter.xml -------------------------------------------------------------------------------- /backend/lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/backend/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /backend/src/main/resources/images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/backend/src/main/resources/images/test.png -------------------------------------------------------------------------------- /backend/src/main/resources/application-credentials.yml.template: -------------------------------------------------------------------------------- 1 | cloud: 2 | aws: 3 | credentials: 4 | accessKey: 5 | secretKey: -------------------------------------------------------------------------------- /frontend/src/images/kakao_login_large_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h-jjang/bauction/HEAD/frontend/src/images/kakao_login_large_wide.png -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/service/PostService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.service; 2 | 3 | public interface PostService { 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/Agreement.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | public enum Agreement { 4 | DISAGREE, AGREE 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-aws.yml.template: -------------------------------------------------------------------------------- 1 | cloud: 2 | aws: 3 | s3: 4 | bucket: 5 | region: 6 | static: ap-northeast-2 7 | stack: 8 | auto: false -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.17.4 2 | 3 | #작업 폴더를 만들고 npm설치 4 | WORKDIR /app 5 | 6 | RUN npm install --silent 7 | RUN npm install react-scripts@4 -g --silent 8 | EXPOSE 3000 9 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /frontend/src/page/MyPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyPage = () => { 4 | 5 | return ( 6 |
7 |

MyPage

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/exception/PostNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.exception; 2 | 3 | public class PostNotFoundException extends RuntimeException{ 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/page/SignUpPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const SignUpPage = () => { 4 | 5 | return ( 6 |
7 |

Sign Up Page

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /frontend/src/page/ItemDetailPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ItemDetailPage = () => { 4 | 5 | return ( 6 |
7 |

ItemDetailPage

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /frontend/src/page/ItemListPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ItemListPage = () => { 4 | 5 | return ( 6 |
7 |

ItemListPage

8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/ProviderType.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum ProviderType { 7 | KAKAO, 8 | LOCAL 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/exception/UserDoesNotExistException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.exception; 2 | 3 | public class UserDoesNotExistException { 4 | public UserDoesNotExistException() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import {RouterPage} from "./page/RouterPage"; 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /backend/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-email.yml.template: -------------------------------------------------------------------------------- 1 | spring: 2 | mail: 3 | host: 4 | port: 5 | username: 6 | password: 7 | properties: 8 | mail: 9 | smtp: 10 | auth: true 11 | starttls: 12 | enable: true -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/dto/MailResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class MailResponse { 9 | 10 | private Mail mail; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/AnonymousEmptyUser.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | public class AnonymousEmptyUser extends User { 4 | public AnonymousEmptyUser() { 5 | super(null, null, null, null, null, null, null, null, null, false, false); 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/BackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class BackendApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/domain/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.repository; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | 7 | public interface PostRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/domain/repository/ImageRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.domain.repository; 2 | 3 | import com.hjjang.backend.domain.image.domain.entity.Image; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface ImageRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/domain/repositroy/TradeRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.domain.repositroy; 2 | 3 | import com.hjjang.backend.domain.trade.domain.entity.Trade; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface TradeRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/dto/ApiResponseHeader.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Setter 8 | @Getter 9 | @AllArgsConstructor 10 | public class ApiResponseHeader { 11 | private int code; 12 | private String message; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/domain/repository/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.domain.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import com.hjjang.backend.domain.category.domain.entity.Category; 6 | 7 | public interface CategoryRepository extends JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/util/HttpStatusResponseEntity.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.util; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | public class HttpStatusResponseEntity { 7 | public static final ResponseEntity RESPONSE_OK = ResponseEntity.status(HttpStatus.OK).build(); 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/auth/OAuth.js: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = "accessToken"; 2 | export const API_BASE_URL = `${process.env.REACT_APP_HOST}`; 3 | export const OAUTH2_REDIRECT_URI = `${API_BASE_URL}/oauth2/redirect`; 4 | export const SERVER_BASE_URL = `${process.env.REACT_APP_SERVER_HOST}`; 5 | export const KAKAO_AUTH_URL = `${SERVER_BASE_URL}/oauth2/authorization/kakao?redirect_uri=${OAUTH2_REDIRECT_URI}`; -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/dto/MailRequest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | public class MailRequest { 11 | 12 | private String mail; 13 | 14 | private String code; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/domain/entity/PostState.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum PostState { 9 | SALE("판매중"), 10 | RESERVED("예약중"), 11 | SOLD("판매완료"), 12 | ; 13 | 14 | final String state; 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: 기능 개발 제안 4 | title: "[FEAT]" 5 | labels: "enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## ✨ 제안 기능 11 | 기능에 대해 간략하게 설명해주세요! 12 | 13 | ## 🎇 제안 배경 14 | 이 기능을 추가하게 되는 배경에 대해 적어주세요! 15 | ex) 중고 거래에 필요한 채팅 기능이 필요해서 도입한다! 16 | 17 |
18 | 19 | ## 📢 기능 설명 20 | 개발할 기능에 대해 설명해주세요. 21 | 22 |
23 | 24 | ### 📕 래퍼런스 25 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/service/Path.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.service; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum Path { 7 | imageSavePath(System.getProperty("user.dir") + "/backend/src/main/resources/images/"); 8 | 9 | private final String path; 10 | Path(String path) { 11 | this.path = path; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /frontend/frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/BackendApplication.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class BackendApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(BackendApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/domain/entity/TradeState.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.domain.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum TradeState { 9 | PENDING("대기"), 10 | RESERVE("예약"), 11 | CANCEL("취소"), 12 | APPROVE("판매완료"), 13 | ; 14 | 15 | private final String state; 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/exception/InvalidMailException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.exception; 2 | 3 | import com.hjjang.backend.global.execption.BusinessException; 4 | import com.hjjang.backend.global.response.code.ErrorCode; 5 | 6 | public class InvalidMailException extends BusinessException { 7 | 8 | public InvalidMailException() { 9 | super(ErrorCode.INVALID_MAIL); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/exception/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.exception; 2 | 3 | import com.hjjang.backend.global.execption.BusinessException; 4 | import com.hjjang.backend.global.response.code.ErrorCode; 5 | 6 | public class UnauthorizedException extends BusinessException { 7 | 8 | public UnauthorizedException(ErrorCode errorCode) { 9 | super(errorCode); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/exception/TokenValidFailedException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.exception; 2 | 3 | public class TokenValidFailedException extends RuntimeException { 4 | 5 | public TokenValidFailedException() { 6 | super("Failed to generate Token."); 7 | } 8 | 9 | private TokenValidFailedException(String message) { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Bug Report 설명 4 | title: "[BUG]" 5 | labels: "bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 🤔 버그 내용 11 | 버그 내용 및 버그 발생 상황을 알려주세요! 12 | ex) 어떤 api를 호출하였더니 오류가 생겼어요. 13 |
14 | 15 | ## 🚩 버그 발견 위치 16 | 버그 발견 예상 패키지 위치를 알려주세요! 17 | ex) 18 | 19 |
20 | 21 | ## ⚠ 에러 캡쳐 22 | 주요 에러 stack trace를 캡쳐해주세요! 23 | 24 |
25 | 26 | 27 | ## 🙂 etc 28 | 추가적으로 하고 싶은 말! (Optional) 29 | 30 |
31 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/dto/PostRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.dto; 2 | 3 | import lombok.*; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | @Data 8 | @Builder 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | public class PostRequestDto { 12 | @NotNull 13 | private String title; 14 | 15 | @NotNull 16 | private String content; 17 | 18 | @NotNull 19 | private int price; 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.repository; 2 | 3 | import com.hjjang.backend.domain.user.entity.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findUserById(Long id); 10 | 11 | Optional findUserByProviderId(String providerId); 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/exception/CategoryNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.exception; 2 | 3 | public class CategoryNotFoundException extends RuntimeException { 4 | public CategoryNotFoundException() { 5 | super(); 6 | } 7 | 8 | public CategoryNotFoundException(String message) { 9 | super(message); 10 | } 11 | 12 | public CategoryNotFoundException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/exception/ImageNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.exception; 2 | 3 | public class ImageNotFoundException extends RuntimeException { 4 | 5 | public ImageNotFoundException() { 6 | super(); 7 | } 8 | 9 | public ImageNotFoundException(String message) { 10 | super(message); 11 | } 12 | 13 | public ImageNotFoundException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-db.yml.template: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: 4 | url: 5 | username: 6 | password: 7 | 8 | jpa: 9 | database: 10 | show-sql: 11 | hibernate: 12 | ddl-auto: 13 | properties: 14 | hibernate: 15 | format_sql: true 16 | generate-ddl: true 17 | 18 | flyway: 19 | enabled: false 20 | url: 21 | user: 22 | password: 23 | baseline-on-migrate: true 24 | ignore-ignored-migrations: true 25 | clean-on-validation-error: true -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/repository/UserRefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.repository; 2 | 3 | import com.hjjang.backend.domain.user.entity.UserRefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface UserRefreshTokenRepository extends JpaRepository { 7 | UserRefreshToken findByProviderId(String providerId); 8 | 9 | UserRefreshToken findByProviderIdAndRefreshToken(String providerId, String refreshToken); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/domain/MailRegex.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum MailRegex { 9 | UNIVERSITY("UNIVERSITY_MAIL_REGEX", "^[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.+a+c+\\.+k+r+$"), 10 | GS_UNIVERSITY("GS_UNIVERSITY_MAIL_REGEX", "^[a-zA-Z0-9]+@+g+s+\\.+[a-zA-Z0-9]+\\.+a+c+\\.+k+r+$"), 11 | MAIL_PARSE("MAIL_PARSE_REGEX", "[@.]"); 12 | 13 | private final String name; 14 | private final String regex; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/search/service/SearchService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.search.service; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import com.hjjang.backend.domain.user.entity.User; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | public interface SearchService { 9 | 10 | Page findAll(String filter, Pageable pageable, User user); 11 | 12 | Page findByKeyword(String keyword, String filter, Pageable pageable, User user); 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/dto/TradeRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | @Data 9 | public class TradeRequestDto { 10 | 11 | @NotNull 12 | @JsonProperty("post_id") 13 | private Long postId; 14 | 15 | @NotNull 16 | @JsonProperty("buyer_id") 17 | private Long buyerId; 18 | 19 | @NotNull 20 | @JsonProperty("seller_id") 21 | private Long sellerId; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/university/repository/UniversityRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.university.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import com.hjjang.backend.domain.university.entity.University; 9 | 10 | @Repository 11 | public interface UniversityRepository extends JpaRepository { 12 | Optional findByName(String name); 13 | 14 | Boolean existsByName(String name); 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/dto/UserProfileDTO.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class UserProfileDTO { 13 | 14 | private String userNickname; 15 | 16 | private String userImageUrl; 17 | 18 | private String userEmail; 19 | 20 | private Long userMannerTemperature; 21 | 22 | private String userUnivName; 23 | } 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: pr 설명 4 | title: "[PR]" 5 | labels: "" 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | ## 추가한 기능 설명 12 | 13 |
14 | 15 | 16 | ## check list 17 | - [ ] issue number를 브랜치 앞에 추가 하였는가?
18 | - [ ] 그외의 [pr 규칙](https://github.com/h-jjang/bauction/wiki/%5BProject-Rules%5D#pr-%EA%B7%9C%EC%B9%99)을 잘 지켰는가? 19 | - [ ] 모든 단위 테스트를 돌려보고 기존에 작동하던 테스트에 영향이 없는 것을 확인했는가? 20 | 설명 21 | - [ ] naver code style formatting(단축키)을 했는가? 22 | - [ ] [우테코 pr 규칙](https://github.com/woowacourse/woowacourse-docs/blob/master/cleancode/pr_checklist.md)을 준수하였는가? -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/dto/TradeResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | @Data 8 | @Builder 9 | public class TradeResponseDto { 10 | Long id; 11 | 12 | @JsonProperty("post_id") 13 | private Long postId; 14 | 15 | @JsonProperty("buyer_id") 16 | private Long buyerId; 17 | 18 | @JsonProperty("seller_id") 19 | private Long sellerId; 20 | 21 | @JsonProperty("state") 22 | private String state; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/dto/ImagePathResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.dto; 2 | 3 | import com.hjjang.backend.domain.image.domain.entity.Image; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | @Builder 8 | @Getter 9 | public class ImagePathResponse { 10 | 11 | private Long id; 12 | private String path; 13 | 14 | public static ImagePathResponse of(Image image) { 15 | return ImagePathResponse.builder() 16 | .id(image.getId()) 17 | .path(image.getPath()) 18 | .build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/execption/handler/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.execption.handler; 2 | 3 | import org.springframework.web.bind.annotation.RestControllerAdvice; 4 | 5 | @RestControllerAdvice 6 | public class GlobalExceptionHandler { 7 | 8 | // 500 에러 9 | // @ExceptionHandler 10 | // protected ResponseEntity handleException(Exception e) { 11 | // final ErrorResponse response = ErrorResponse.of(INTERNAL_SERVER_ERROR); 12 | // return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); 13 | // } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.service; 2 | 3 | import com.hjjang.backend.domain.category.domain.entity.Category; 4 | import com.hjjang.backend.domain.category.dto.CategoryRequest; 5 | 6 | public interface CategoryService { 7 | 8 | public Category findAllByCategory(); 9 | 10 | public Category findCategoryById(Long categoryId); 11 | 12 | public void createNewCategory(CategoryRequest categoryRequest); 13 | 14 | public void updateCategory(Category category, CategoryRequest categoryRequest); 15 | 16 | public void removeCategory(Category category); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/parser/ParsingUserContextFactory.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.parser; 2 | 3 | import com.hjjang.backend.domain.user.entity.ProviderType; 4 | 5 | import java.util.Map; 6 | 7 | public class ParsingUserContextFactory { 8 | public static ParsingUserContext getParsingParsingUserContext(ProviderType providerType, 9 | Map attributes) { 10 | if (providerType == ProviderType.KAKAO) { 11 | return new KakaoParsingParsingUserContext(attributes); 12 | } 13 | throw new IllegalArgumentException("Invalid Provider Type."); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:7.4.1-jdk11 as cache 2 | RUN mkdir -p /home/gradle/cache_home 3 | ENV GRADLE_USER_HOME /home/gradle/cache_home 4 | COPY build.gradle /home/gradle/java-code/ 5 | WORKDIR /home/gradle/java-code 6 | RUN gradle clean build -i --stacktrace 7 | 8 | FROM gradle:7.4.1-jdk11 as builder 9 | COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle 10 | COPY . /usr/src/java-code/ 11 | WORKDIR /usr/src/java-code 12 | RUN gradle bootJar -i --stacktrace 13 | 14 | FROM openjdk:11-jre-slim 15 | EXPOSE 8080 16 | USER root 17 | WORKDIR /usr/src/java-app 18 | COPY --from=builder /usr/src/java-code/build/libs/*.jar ./app.jar 19 | ENTRYPOINT ["java", "-jar", "app.jar"] -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/parser/ParsingUserContext.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.parser; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class ParsingUserContext { 6 | protected Map attributes; 7 | 8 | public ParsingUserContext(Map attributes) { 9 | this.attributes = attributes; 10 | } 11 | 12 | public Map getAttributes() { 13 | return attributes; 14 | } 15 | 16 | public abstract String getId(); 17 | 18 | public abstract String getName(); 19 | 20 | public abstract String getEmail(); 21 | 22 | public abstract String getImageUrl(); 23 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/domain/entity/Image.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.domain.entity; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @AllArgsConstructor 10 | @Entity 11 | public class Image { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Long id; 15 | 16 | private String path; 17 | 18 | private Boolean removed = false; 19 | 20 | @Builder 21 | public Image(String path) { 22 | this.path = path; 23 | } 24 | 25 | public void removeImage() { 26 | this.removed = true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/university/service/UniversityService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.university.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import com.hjjang.backend.domain.university.entity.University; 6 | import com.hjjang.backend.domain.university.repository.UniversityRepository; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class UniversityService { 13 | private final UniversityRepository universityRepository; 14 | 15 | public University findUniversityByName(String name) { 16 | return universityRepository.findByName(name).orElseThrow(RuntimeException::new); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: "backend" 4 | profiles: 5 | include: 6 | - db 7 | - auth 8 | - email 9 | - aws 10 | - credentials 11 | 12 | redis: 13 | host: bauction-redis 14 | port: 6379 15 | 16 | #----- for swagger UI 17 | # mvc: 18 | # pathmatch: 19 | # matching-strategy: ant_path_matcher 20 | #----- for auto create table and insert data 21 | # sql: 22 | # init: 23 | # schema-locations: classpath:scheme.sql 24 | # data-locations: classpath:data.sql 25 | # mode: always 26 | 27 | servlet: 28 | multipart: 29 | max-file-size: 50MB 30 | max-request-size: 50MB 31 | 32 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/dto/CategoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.dto; 2 | 3 | import com.hjjang.backend.domain.category.domain.entity.Category; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Builder 11 | @Getter 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class CategoryResponse { 15 | 16 | private Long id; 17 | 18 | private String name; 19 | 20 | public static CategoryResponse of(Category category) { 21 | return CategoryResponse.builder() 22 | .id(category.getId()) 23 | .name(category.getName()) 24 | .build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/dto/PostResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Builder 8 | @Getter 9 | @Setter 10 | public class PostResponseDto { 11 | 12 | private Long id; 13 | 14 | private Long university_id; 15 | 16 | private String title; 17 | 18 | private String content; 19 | 20 | private Integer item_price; 21 | 22 | private Integer views; 23 | 24 | private Integer interest_number; 25 | 26 | private Integer chat_number; 27 | 28 | private String is_sale_completion; 29 | 30 | private boolean removed; 31 | 32 | private String time; 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/RoleType.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | import java.util.Arrays; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | public enum RoleType { 11 | USER("ROLE_USER", "일반 사용자 권한"), 12 | ADMIN("ROLE_ADMIN", "관리자 권한"), 13 | GUEST("GUEST", "게스트 권한"); 14 | 15 | private final String code; 16 | private final String displayName; 17 | 18 | public static RoleType of(String code) { 19 | return Arrays.stream(RoleType.values()) 20 | .filter(r -> r.getCode().equals(code)) 21 | .findAny() 22 | .orElse(GUEST); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/dto/CategoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.dto; 2 | 3 | import javax.validation.constraints.NotEmpty; 4 | 5 | import com.hjjang.backend.domain.category.domain.entity.Category; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Builder 14 | @Getter 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor 17 | public class CategoryRequest { 18 | 19 | @NotEmpty 20 | private String name; 21 | 22 | public Category toEntity() { 23 | return Category.builder() 24 | .name(name) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/domain/MailMessage.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum MailMessage { 9 | AUTH_TITLE("[Bauction] 인증번호를 발송했습니다."), 10 | AUTH_MESSAGE("[본인확인 인증 메일]\n" 11 | + "이메일 인증을 진행해주세요.\n" 12 | + "아래 메일 인증 번호를 입력하여 회원가입을 완료하세요.\n"), 13 | KEYWORD_NOTICE_TITLE("[Bauction] 키워드 알림입니다."), 14 | KEYWORD_NOTICE_MESSAGE("[키워드 알림 메일]\n" 15 | + "다음과 같은 키워드에 대한 게시글 알림입니다.\n"), 16 | TRADE_NOTICE_TITLE("[Bauction] 거래 현황 변동 알림입니다."), 17 | TRADE_NOTICE_MESSAGE("[거래 현황 변동 알림]\n" 18 | + "다음 상품의 거래에 대한 현황이 변동되었습니다.\n") 19 | ; 20 | 21 | private final String content; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/component/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Link} from "react-router-dom"; 3 | 4 | export const NavBar = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/domain/BaseTimeEntity.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.domain; 2 | 3 | import lombok.Getter; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | 8 | import javax.persistence.EntityListeners; 9 | import javax.persistence.MappedSuperclass; 10 | import java.time.LocalDateTime; 11 | 12 | @Getter 13 | @EntityListeners(value = {AuditingEntityListener.class}) 14 | @MappedSuperclass 15 | public abstract class BaseTimeEntity { 16 | @CreatedDate 17 | private LocalDateTime createdDate; 18 | 19 | @LastModifiedDate 20 | private LocalDateTime updatedDate; 21 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/util/HeaderUtil.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.util; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | public class HeaderUtil { 6 | 7 | private final static String HEADER_AUTHORIZATION = "Authorization"; 8 | private final static String TOKEN_PREFIX = "Bearer "; 9 | 10 | public static String getAccessToken(HttpServletRequest request) { 11 | String headerValue = request.getHeader(HEADER_AUTHORIZATION); 12 | 13 | if (headerValue == null) { 14 | return null; 15 | } 16 | 17 | if (headerValue.startsWith(TOKEN_PREFIX)) { 18 | return headerValue.substring(TOKEN_PREFIX.length()); 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/AuthProviderConfig.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security; 2 | 3 | import com.hjjang.backend.global.config.security.properties.AuthProperties; 4 | import com.hjjang.backend.global.security.token.AuthTokenProvider; 5 | 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | @RequiredArgsConstructor 13 | public class AuthProviderConfig { 14 | 15 | private final AuthProperties authProperties; 16 | 17 | @Bean 18 | public AuthTokenProvider jwtProvider() { 19 | return new AuthTokenProvider(authProperties.getJwtProperties().getSecretKey()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/dto/Mail.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.dto; 2 | 3 | import java.util.Random; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Getter 12 | @Setter 13 | public class Mail { 14 | 15 | private String code; 16 | private String address; 17 | private String university; 18 | private Boolean isAuth; 19 | 20 | private static final int CODE_SIZE = 6; 21 | 22 | public String createRandomCode() { 23 | Random random = new Random(); 24 | StringBuilder buffer = new StringBuilder(); 25 | while (buffer.length() < CODE_SIZE) { 26 | buffer.append(random.nextInt(10)); 27 | } 28 | return buffer.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/main/resources/static.docs/api.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ./build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 2 9 | :sectlinks: 10 | :docinfo: shared-head 11 | 12 | = REST API Document 13 | 14 | [[introduction]] 15 | == Introduction 16 | 17 | ✨BAuction (Book + Auction)✨ 18 | 19 | 중고책 실시간 경매 서비스 20 | 21 | [[common]] 22 | == Common 23 | 24 | === Domain 25 | 26 | |=== 27 | | 환경 | Domain 28 | | 로컬 서버 29 | | `http://localhost:3000` 30 | |=== 31 | 32 | === Exception 33 | 34 | |=== 35 | | 상태 코드 | 설명 36 | 37 | | 400 38 | | `잘못된 데이터` 39 | 40 | | 401 41 | | `권한 없음` 42 | |=== 43 | == User API 44 | operation::v1/users/profile[snippets='http-request,request-fields,http-response,response-fields'] 45 | |=== 46 | |=== 47 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/domain/entity/PostDefaultValue.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.entity; 2 | 3 | import com.hjjang.backend.domain.university.entity.University; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class PostDefaultValue { 12 | public static final University DEFAULT_UNIVERSITY = new University(1L, "empty", "empty"); 13 | public static final int DEFAULT_VIEWS = 0; 14 | public static final int DEFAULT_INTEREST_NUMBER = 0; 15 | public static final int DEFAULT_CHAT_NUMBER = 0; 16 | public static final PostState DEFAULT_IS_SALE_COMPLETION = PostState.SALE; 17 | public static final boolean DEFAULT_REMOVED = false; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/exception/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.exception; 2 | 3 | import com.hjjang.backend.global.execption.BusinessException; 4 | import com.hjjang.backend.global.response.code.ErrorCode; 5 | import com.hjjang.backend.global.response.response.ErrorResponse; 6 | 7 | import java.util.List; 8 | 9 | public class UserNotFoundException extends BusinessException { 10 | public UserNotFoundException(String message, ErrorCode errorCode) { 11 | super(message, errorCode); 12 | } 13 | 14 | public UserNotFoundException(ErrorCode errorCode) { 15 | super(errorCode); 16 | } 17 | 18 | public UserNotFoundException(ErrorCode errorCode, List errors) { 19 | super(errorCode, errors); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/exception/UserNotMatchException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.exception; 2 | 3 | import com.hjjang.backend.global.execption.BusinessException; 4 | import com.hjjang.backend.global.response.code.ErrorCode; 5 | import com.hjjang.backend.global.response.response.ErrorResponse; 6 | 7 | import java.util.List; 8 | 9 | public class UserNotMatchException extends BusinessException { 10 | public UserNotMatchException(String message, ErrorCode errorCode) { 11 | super(message, errorCode); 12 | } 13 | 14 | public UserNotMatchException(ErrorCode errorCode) { 15 | super(errorCode); 16 | } 17 | 18 | public UserNotMatchException(ErrorCode errorCode, List errors) { 19 | super(errorCode, errors); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/exception/TradeNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.exception; 2 | 3 | import com.hjjang.backend.global.execption.BusinessException; 4 | import com.hjjang.backend.global.response.code.ErrorCode; 5 | import com.hjjang.backend.global.response.response.ErrorResponse; 6 | 7 | import java.util.List; 8 | 9 | public class TradeNotFoundException extends BusinessException { 10 | public TradeNotFoundException(String message, ErrorCode errorCode) { 11 | super(message, errorCode); 12 | } 13 | 14 | public TradeNotFoundException(ErrorCode errorCode) { 15 | super(errorCode); 16 | } 17 | 18 | public TradeNotFoundException(ErrorCode errorCode, List errors) { 19 | super(errorCode, errors); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/response/code/SuccessCode.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.response.code; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum SuccessCode { 9 | 10 | //Mail 11 | MAIL_SEND_SUCCESS("M001","코드 생성하여 메일 전송."), 12 | MAIL_CHECK_SUCCESS("M002", "메일 인증 확인."), 13 | 14 | //User 15 | USER_PROFILE_SUCCESS( "U001", "프로필 조회 완료."), 16 | 17 | //Trade 18 | TRADE_CREATE_SUCCESS("T001", "거래기록 생성 완료"), 19 | TRADE_FIND_SUCCESS("T002", "거래기록 조회 완료"), 20 | TRADE_UPDATE_SUCCESS("T003", "거래기록 갱신 완료"), 21 | TRADE_DELETE_SUCCESS("T004", "거래기록 삭제 완료"), 22 | 23 | //Post 24 | POST_CREATE_SUCCESS( "P001", "게시글 등록 완료.") 25 | 26 | ; 27 | private final String code; 28 | private final String message; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/service/LocalImageUploader.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | 11 | @Slf4j 12 | @RequiredArgsConstructor 13 | @Service 14 | public class LocalImageUploader extends ImageUploader{ 15 | 16 | @Override 17 | public String upload(MultipartFile multipartFile) throws IOException { 18 | File uploadFile = 19 | this.localUpload(multipartFile) // 파일 변환할 수 없으면 에러 20 | .orElseThrow( 21 | () -> new IllegalArgumentException("error: MultipartFile -> File convert fail")); 22 | return uploadFile.getName(); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/response/code/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.response.code; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum ErrorCode { 9 | 10 | //Mail 11 | INVALID_MAIL("M001", "대학교 메일 계정 오류"), 12 | INVALID_CODE("M002", "코드 입력 오류"), 13 | TIME_LIMIT("M003", "시간 초과"), 14 | 15 | //Global 16 | INTERNAL_SERVER_ERROR("G001", "내부 서버 오류"), 17 | NOT_ALLOWED_METHOD("G002", "허용 되지 않은 HTTP method"), 18 | INVALID_INPUT_VALUE("G003", "검증 되지 않은 입력"), 19 | 20 | //User 21 | MEMBER_NOT_FOUND("U001", "존재 하지 않는 사용자"), 22 | NO_AUTHORITY("U002", "권한이 없음"), 23 | 24 | //Trade 25 | TRADE_NOT_CREATED("T001", "거래기록 생성 실패"), 26 | TRADE_NOT_FOUND("T002", "존재 하지 않는 거래기록"), 27 | TRADE_NOT_UPDATED("T003", "거래기록 갱신 실패") 28 | ; 29 | 30 | 31 | private final String code; 32 | private final String message; 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-auth.yml.template: -------------------------------------------------------------------------------- 1 | spring: 2 | # oauth 설정 3 | security: 4 | oauth2.client: 5 | registration: 6 | kakao: 7 | clientId: 8 | clientSecret: 9 | clientAuthenticationMethod: 10 | authorizationGrantType: 11 | redirectUri: 12 | scope: 13 | - profile_nickname 14 | - profile_image 15 | - account_email 16 | clientName: 17 | provider: 18 | kakao: 19 | authorizationUri: 20 | tokenUri: 21 | userInfoUri: 22 | userNameAttribute: 23 | 24 | auth-properties: 25 | token-properties: 26 | token-secret-key: 27 | token-expire-date: 28 | refresh-token-expiry: 29 | jwt-properties: 30 | secret-key: 31 | cors-properties: 32 | allowed-origins: 33 | allowed-methods: 34 | allowed-headers: 35 | max-age: 36 | oAuth2Properties: 37 | redirectUris: 38 | - 39 | - -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | .kakao_btn{ 41 | background-image: url("./images/kakao_login_large_wide.png"); 42 | background-repeat: no-repeat; 43 | background-size : cover; 44 | margin: 10px auto; 45 | /* padding: -10px; */ 46 | color: transparent; 47 | width: 300px; 48 | height: 45px; 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/global/security/WithMockCustomUser.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security; 2 | 3 | import com.hjjang.backend.domain.user.entity.Agreement; 4 | import com.hjjang.backend.domain.user.entity.RoleType; 5 | import org.springframework.security.test.context.support.WithSecurityContext; 6 | 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @WithSecurityContext(factory = WithMockUserSecurityContextFactory.class) 12 | public @interface WithMockCustomUser { 13 | 14 | long id() default 1L; 15 | 16 | String email() default "kevinkim@email.com"; 17 | 18 | String imageUrl() default "이미지입니다아아아"; 19 | 20 | String nickName() default "김겨여여연"; 21 | 22 | RoleType roles() default RoleType.USER; 23 | 24 | String providerId() default "kakao123456"; 25 | 26 | long mannerTemperature() default 36L; 27 | 28 | Agreement isPushAgree() default Agreement.AGREE; 29 | 30 | long univId() default 1L; 31 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.3", 7 | "@testing-library/react": "^12.1.4", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^0.26.1", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-router-dom": "^5.3.0", 13 | "react-scripts": "5.0.0", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/execption/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.execption; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import com.hjjang.backend.global.response.code.ErrorCode; 7 | import com.hjjang.backend.global.response.response.ErrorResponse; 8 | 9 | import lombok.Getter; 10 | 11 | @Getter 12 | public class BusinessException extends RuntimeException { 13 | 14 | private final ErrorCode errorCode; 15 | private List errors = new ArrayList<>(); 16 | 17 | public BusinessException(String message, ErrorCode errorCode) { 18 | super(message); 19 | this.errorCode = errorCode; 20 | } 21 | 22 | public BusinessException(ErrorCode errorCode) { 23 | super(errorCode.getMessage()); 24 | this.errorCode = errorCode; 25 | } 26 | 27 | public BusinessException(ErrorCode errorCode, List errors) { 28 | super(errorCode.getMessage()); 29 | this.errors = errors; 30 | this.errorCode = errorCode; 31 | } 32 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/response/response/SuccessResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.response.response; 2 | 3 | import com.hjjang.backend.global.response.code.SuccessCode; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public class SuccessResponse { 9 | 10 | @Schema(description = "Business code") 11 | private final String code; 12 | @Schema(description = "response message") 13 | private final String message; 14 | @Schema(description = "response data") 15 | private final Object data; 16 | 17 | public static SuccessResponse of(SuccessCode successCode, Object data) { 18 | return new SuccessResponse(successCode, data); 19 | } 20 | 21 | public static SuccessResponse of(SuccessCode successCode) { 22 | return new SuccessResponse(successCode, ""); 23 | } 24 | 25 | public SuccessResponse(SuccessCode successCode, Object data) { 26 | this.code = successCode.getCode(); 27 | this.message = successCode.getMessage(); 28 | this.data = data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/university/entity/University.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.university.entity; 2 | 3 | import static javax.persistence.GenerationType.IDENTITY; 4 | import static lombok.AccessLevel.PROTECTED; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.Id; 10 | import javax.persistence.Table; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Getter 17 | @AllArgsConstructor 18 | @Entity 19 | @NoArgsConstructor(access = PROTECTED) 20 | @Table(name = "university") 21 | public class University { 22 | 23 | @Id 24 | @GeneratedValue(strategy = IDENTITY) 25 | @Column(name = "id", nullable = false) 26 | private Long id; 27 | 28 | @Column(name = "name", nullable = false) 29 | private String name; 30 | 31 | @Column(name = "mail", nullable = false) 32 | private String mail; 33 | 34 | @Builder 35 | public University(String name, String mail) { 36 | this.name = name; 37 | this.mail = mail; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/search/repository/SearchRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.search.repository; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | 9 | import com.hjjang.backend.domain.post.domain.entity.Post; 10 | import com.hjjang.backend.domain.university.entity.University; 11 | 12 | public interface SearchRepository extends JpaRepository { 13 | 14 | List findByIsSaleCompletion(String IsSaleCompletion, Pageable pageable); 15 | 16 | Page findAllByIsSaleCompletion(String IsSaleCompletion, Pageable pageable); 17 | 18 | List findByTitleContaining(String keyword, Pageable pageable); 19 | 20 | Page findByUniversity(University university, Pageable pageable); 21 | 22 | List findByIsSaleCompletionAndUniversity(String IsSaleCompletion, University university, Pageable pageable); 23 | Page findAllByIsSaleCompletionAndUniversity(String IsSaleCompletion, University university, Pageable pageable); 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/exception/RestAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.exception; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.security.core.AuthenticationException; 5 | import org.springframework.security.web.AuthenticationEntryPoint; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | @Slf4j 13 | public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { 14 | @Override 15 | public void commence( 16 | HttpServletRequest request, 17 | HttpServletResponse response, 18 | AuthenticationException authException 19 | ) throws IOException, ServletException { 20 | authException.printStackTrace(); 21 | log.info("Responding with unauthorized error. Message := {}", authException.getMessage()); 22 | response.sendError( 23 | HttpServletResponse.SC_UNAUTHORIZED, 24 | authException.getLocalizedMessage() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/handler/TokenAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.handler; 2 | 3 | import java.io.IOException; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | 7 | import org.springframework.security.access.AccessDeniedException; 8 | import org.springframework.security.web.access.AccessDeniedHandler; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerExceptionResolver; 11 | import lombok.RequiredArgsConstructor; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | public class TokenAccessDeniedHandler implements AccessDeniedHandler { 16 | 17 | private final HandlerExceptionResolver handlerExceptionResolver; 18 | 19 | @Override 20 | public void handle(HttpServletRequest request, HttpServletResponse response, 21 | AccessDeniedException accessDeniedException) throws IOException { 22 | //response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); 23 | handlerExceptionResolver.resolveException(request, response, null, accessDeniedException); 24 | } 25 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/config/ApplicationStartupTask.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.config; 2 | 3 | import org.springframework.boot.context.event.ApplicationReadyEvent; 4 | import org.springframework.context.ApplicationListener; 5 | import org.springframework.stereotype.Component; 6 | 7 | import com.hjjang.backend.domain.university.entity.University; 8 | import com.hjjang.backend.domain.university.repository.UniversityRepository; 9 | 10 | import lombok.RequiredArgsConstructor; 11 | 12 | @Component 13 | @RequiredArgsConstructor 14 | public class ApplicationStartupTask implements ApplicationListener { 15 | private final UniversityRepository universityRepository; 16 | @Override 17 | public void onApplicationEvent(ApplicationReadyEvent event) { 18 | if (!universityRepository.existsByName("empty")){ 19 | universityRepository.save(new University("empty", "empty")); 20 | } 21 | if (!universityRepository.existsByName("tukorea")){ 22 | universityRepository.save(new University("tukorea", "tukorea.ac.kr")); 23 | } 24 | if (!universityRepository.existsByName("dankook")){ 25 | universityRepository.save(new University("dankook", "dankook.ac.kr")); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default-Setting 6 | %style{%d{yyyy/MM/dd HH:mm:ss,SSS}}{cyan} %highlight{[%-5p]}{FATAL=bg_red, ERROR=red, INFO=green, DEBUG=blue} [%C] %style{[%t]}{yellow}- %m%n - 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/infra/aws/config/AmazonS3Config.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.infra.aws.config; 2 | 3 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 4 | import com.amazonaws.auth.BasicAWSCredentials; 5 | import com.amazonaws.services.s3.AmazonS3Client; 6 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class AmazonS3Config { 13 | 14 | @Value("${cloud.aws.credentials.access-key}") 15 | private String accessKey; 16 | 17 | @Value("${cloud.aws.credentials.secret-key}") 18 | private String secretKey; 19 | 20 | @Value("${cloud.aws.region.static}") 21 | private String region; 22 | 23 | @Bean 24 | public AmazonS3Client amazonS3Client() { 25 | BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); 26 | return (AmazonS3Client) AmazonS3ClientBuilder.standard() 27 | .withRegion(region) 28 | .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) 29 | .build(); 30 | } 31 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/util/UserUtil.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.util; 2 | 3 | import com.hjjang.backend.domain.user.repository.UserRepository; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.security.core.context.SecurityContextHolder; 6 | import org.springframework.security.core.userdetails.User; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.persistence.EntityNotFoundException; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | public class UserUtil { 14 | 15 | private final UserRepository userRepository; 16 | 17 | public static String getLoginUserIdByToken() { 18 | User principal = (User) SecurityContextHolder.getContext() 19 | .getAuthentication() 20 | .getPrincipal(); 21 | 22 | return principal.getUsername(); 23 | } 24 | 25 | public com.hjjang.backend.domain.user.entity.User getLoginUserByToken() { 26 | 27 | User principal = (User) SecurityContextHolder.getContext() 28 | .getAuthentication() 29 | .getPrincipal(); 30 | return userRepository.findUserByProviderId(principal.getUsername()).orElseThrow(EntityNotFoundException::new); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/domain/entity/Category.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.domain.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | 9 | import com.hjjang.backend.domain.category.dto.CategoryRequest; 10 | 11 | import lombok.AccessLevel; 12 | import lombok.AllArgsConstructor; 13 | import lombok.Builder; 14 | import lombok.Getter; 15 | import lombok.NoArgsConstructor; 16 | 17 | @Getter 18 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 19 | @AllArgsConstructor 20 | @Entity 21 | public class Category { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "id") 26 | private Long id; 27 | 28 | @Column(name = "name") 29 | private String name; 30 | 31 | @Column(name = "removed") 32 | private Boolean removed = false; 33 | 34 | @Builder 35 | public Category(String name) { 36 | this.name = name; 37 | } 38 | 39 | public void updateCategory(CategoryRequest categoryRequest) { 40 | this.name = categoryRequest.getName(); 41 | } 42 | 43 | public void removeCategory() { 44 | this.removed = true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default-Setting 6 | %style{%d{yyyy/MM/dd HH:mm:ss,SSS}}{cyan} %highlight{[%-5p]}{FATAL=bg_red, ERROR=red, INFO=green, DEBUG=blue} [%C] %style{[%t]}{yellow}- %m%n - 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | ### Java template 39 | # Compiled class file 40 | *.class 41 | 42 | # Log file 43 | *.log 44 | 45 | # BlueJ files 46 | *.ctxt 47 | 48 | # Mobile Tools for Java (J2ME) 49 | .mtj.tmp/ 50 | 51 | # Package Files # 52 | *.jar 53 | *.war 54 | *.nar 55 | *.ear 56 | *.zip 57 | *.tar.gz 58 | *.rar 59 | 60 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 61 | hs_err_pid* 62 | 63 | /src/main/resources/application-aws.yml 64 | /src/main/resources/application-credentials.yml 65 | /src/main/resources/application-auth.yml 66 | /src/main/resources/application-db.yml 67 | /src/main/resources/application-email.yml 68 | /src/test/resources/application.yml 69 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.controller; 2 | 3 | import com.hjjang.backend.domain.user.dto.UserProfileDTO; 4 | import com.hjjang.backend.domain.user.service.UserProfileService; 5 | import com.hjjang.backend.global.response.code.SuccessCode; 6 | import com.hjjang.backend.global.response.response.SuccessResponse; 7 | import com.hjjang.backend.global.util.UserUtil; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | @RestController 15 | @RequestMapping("/api/v1/users") 16 | @RequiredArgsConstructor 17 | public class UserController { 18 | 19 | private final UserProfileService userProfileService; 20 | 21 | @GetMapping("/profile") 22 | public ResponseEntity getProfile() { 23 | String userId = UserUtil.getLoginUserIdByToken(); 24 | UserProfileDTO userProfile = userProfileService.getUserProfile(userId); 25 | return ResponseEntity.ok(SuccessResponse.of(SuccessCode.USER_PROFILE_SUCCESS, userProfile)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/page/RouterPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | BrowserRouter, 5 | Switch, 6 | Route, 7 | Redirect, 8 | } from "react-router-dom"; 9 | import OAuth2RedirectHandler from "../auth/OAuth2Redirecthandler"; 10 | 11 | import {ItemDetailPage} from "./ItemDetailPage"; 12 | import {MyPage} from "./MyPage"; 13 | import {ItemListPage} from "./ItemListPage"; 14 | import {NavBar} from "../component/NavBar"; 15 | import {LoginPage} from "./LoginPage"; 16 | import {SignUpPage} from "./SignUpPage"; 17 | 18 | export const RouterPage = () => { 19 | return ( 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/exception/handler/MailExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.exception.handler; 2 | 3 | import static com.hjjang.backend.global.response.code.ErrorCode.*; 4 | import static org.springframework.http.HttpStatus.*; 5 | 6 | import com.hjjang.backend.domain.mail.exception.InvalidMailException; 7 | import com.hjjang.backend.domain.mail.exception.UnauthorizedException; 8 | import com.hjjang.backend.global.response.code.ErrorCode; 9 | import com.hjjang.backend.global.response.response.ErrorResponse; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | 14 | @RestControllerAdvice 15 | public class MailExceptionHandler { 16 | 17 | @ExceptionHandler(value = InvalidMailException.class) 18 | public ResponseEntity handleMailException() { 19 | ErrorResponse response = ErrorResponse.of(INVALID_MAIL); 20 | return new ResponseEntity<>(response, BAD_REQUEST); 21 | } 22 | 23 | @ExceptionHandler(value = UnauthorizedException.class) 24 | public ResponseEntity handleUnauthorizedException(ErrorCode errorCode) { 25 | ErrorResponse response = ErrorResponse.of(errorCode); 26 | return new ResponseEntity<>(response, UNAUTHORIZED); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/auth/OAuth2Redirecthandler.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, useLocation } from "react-router-dom"; 3 | import { ACCESS_TOKEN } from "./OAuth"; 4 | 5 | const OAuth2RedirectHandler = () => { 6 | const location = useLocation(); 7 | 8 | const getUrlParameter = (name) => { 9 | const setName = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 10 | const regex = new RegExp("[\\?&]" + setName + "=([^&#]*)"); 11 | 12 | const results = regex.exec(location.search); 13 | return results === null 14 | ? "" 15 | : decodeURIComponent(results[1].replace(/\+/g, " ")); 16 | }; 17 | 18 | const token = getUrlParameter("token"); 19 | const error = getUrlParameter("error"); 20 | 21 | if (token) { 22 | localStorage.setItem(ACCESS_TOKEN, token); 23 | return ( 24 | 30 | ); 31 | } 32 | return ( 33 | 42 | ); 43 | }; 44 | 45 | export default OAuth2RedirectHandler; 46 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/UserRefreshToken.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | 8 | import javax.persistence.*; 9 | import javax.validation.constraints.NotNull; 10 | 11 | import static javax.persistence.GenerationType.IDENTITY; 12 | import static lombok.AccessLevel.PROTECTED; 13 | 14 | @AllArgsConstructor 15 | @EntityListeners(AuditingEntityListener.class) 16 | @Entity 17 | @NoArgsConstructor(access = PROTECTED) 18 | @Table(name = "user_refresh_token") 19 | public class UserRefreshToken { 20 | 21 | @Id @GeneratedValue(strategy = IDENTITY) 22 | @Column(name = "id", nullable = false) 23 | private Long id; 24 | 25 | @Column(name = "provider_id", nullable = false, length = 64) 26 | private String providerId; 27 | 28 | @Column(name = "refresh_token", nullable = false, length = 256) 29 | @NotNull 30 | private String refreshToken; 31 | 32 | @Builder 33 | public UserRefreshToken(String providerId, String refreshToken) { 34 | this.providerId = providerId; 35 | this.refreshToken = refreshToken; 36 | } 37 | 38 | public void reissueRefreshToken(String refreshToken) { 39 | this.refreshToken = refreshToken; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/service/NoticeMailService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.service; 2 | 3 | import static com.hjjang.backend.domain.mail.domain.MailMessage.*; 4 | import static com.hjjang.backend.global.response.code.ErrorCode.*; 5 | 6 | import com.hjjang.backend.domain.mail.exception.UnauthorizedException; 7 | import com.hjjang.backend.domain.post.domain.entity.Post; 8 | import com.hjjang.backend.domain.user.entity.User; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.mail.SimpleMailMessage; 11 | import org.springframework.mail.javamail.JavaMailSender; 12 | import org.springframework.stereotype.Service; 13 | 14 | @Service 15 | @RequiredArgsConstructor 16 | public class NoticeMailService { 17 | 18 | private final JavaMailSender javaMailSender; 19 | 20 | public void sendNotice(User user, Post post) { 21 | if (!user.getIsEmailVerification()) { 22 | throw new UnauthorizedException(NO_AUTHORITY); 23 | } 24 | SimpleMailMessage message = getSimpleMailMessage(user, post); 25 | javaMailSender.send(message); 26 | } 27 | 28 | private SimpleMailMessage getSimpleMailMessage(User user, Post post) { 29 | SimpleMailMessage message = new SimpleMailMessage(); 30 | message.setTo(user.getEmail()); 31 | message.setSubject(TRADE_NOTICE_TITLE.getContent()); 32 | message.setText(post.mailText(TRADE_NOTICE_MESSAGE.getContent())); 33 | return message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/dto/PostMapper.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.dto; 2 | 3 | import com.hjjang.backend.domain.user.entity.User; 4 | import org.springframework.stereotype.Service; 5 | 6 | import com.hjjang.backend.domain.post.domain.entity.Post; 7 | 8 | @Service 9 | public class PostMapper { 10 | 11 | public Post toEntity(PostRequestDto postRequestDto, User user) { 12 | return Post.builder() 13 | .user(user) 14 | .title(postRequestDto.getTitle()) 15 | .content(postRequestDto.getContent()) 16 | .itemPrice(postRequestDto.getPrice()) 17 | .build(); 18 | } 19 | 20 | public PostResponseDto fromEntity(Post post) { 21 | return PostResponseDto.builder() 22 | .id(post.getId()) 23 | .university_id(post.getUniversity().getId()) 24 | .title(post.getTitle()) 25 | .content(post.getContent()) 26 | .item_price(post.getItemPrice()) 27 | .views(post.getViews()) 28 | .interest_number(post.getInterestNumber()) 29 | .chat_number(post.getChatNumber()) 30 | .is_sale_completion(post.getIsSaleCompletion().getState()) 31 | .removed(post.isRemoved()) 32 | .time(post.getTime().toString()) 33 | .build(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/service/ImageUploader.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.util.Optional; 12 | import java.util.UUID; 13 | 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | @Service 17 | public class ImageUploader { 18 | 19 | public String upload(MultipartFile multipartFile) throws IOException { 20 | return null; 21 | } 22 | 23 | public String changeFileName(MultipartFile uploadFile) { 24 | return UUID.randomUUID() + ".png"; // 저장될 파일 이름 변환 25 | } 26 | 27 | // 로컬에 파일 업로드 하기 28 | public Optional localUpload(MultipartFile file) throws IOException { 29 | File convertFile = new File(Path.imageSavePath.getPath() + changeFileName(file)); 30 | if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능) 31 | try (FileOutputStream fos = 32 | new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함 33 | fos.write(file.getBytes()); 34 | } 35 | return Optional.of(convertFile); 36 | } 37 | return Optional.empty(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/service/ImageService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.service; 2 | 3 | import com.hjjang.backend.domain.image.domain.repository.ImageRepository; 4 | import com.hjjang.backend.domain.image.domain.entity.Image; 5 | import com.hjjang.backend.domain.image.exception.ImageNotFoundException; 6 | import com.hjjang.backend.infra.aws.service.S3ImageUploader; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import javax.transaction.Transactional; 12 | import java.io.IOException; 13 | import java.util.List; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | @Transactional 18 | public class ImageService { 19 | private final ImageRepository imageRepository; 20 | 21 | private final S3ImageUploader imageUploader; 22 | 23 | public List findAll() { 24 | return imageRepository.findAll(); 25 | } 26 | 27 | public Image findImageById(Long imageId) { 28 | return imageRepository.findById(imageId).orElseThrow(ImageNotFoundException::new); 29 | } 30 | 31 | public Image uploadNewImage(MultipartFile multipartFile) throws IOException { 32 | return imageRepository.save(new Image(imageUploader.upload(multipartFile))); 33 | } 34 | 35 | public void removeImage(Image image) { 36 | image.removeImage(); 37 | imageRepository.save(image); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/page/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {Link, Route} from "react-router-dom"; 3 | import axios from "axios"; 4 | import {KAKAO_AUTH_URL} from "../auth/OAuth"; 5 | import KAKAO_LOGO from "../images/kakao_login_large_wide.png" 6 | import styles from '../App.css'; 7 | 8 | export const LoginPage = () => { 9 | // 샘플 코드입니다. 10 | const [id, setId] = useState(""); 11 | const [pw, setPw] = useState(""); 12 | 13 | const changeId = (e) => { 14 | console.log(e.target.value); 15 | setId(e.target.value); 16 | }; 17 | 18 | const changePw = (e) => { 19 | console.log(e.target.value); 20 | setPw(e.target.value); 21 | }; 22 | 23 | const loginSubmit = () => { 24 | axios.post("http://localhost:8080/login", { 25 | id: id, 26 | pw: pw, 27 | }).then(res => { 28 | console.log(res); 29 | }) 30 | } 31 | 32 | 33 | return ( 34 |
35 |

Login Page

36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/service/PostServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.service; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.stereotype.Service; 6 | 7 | import com.hjjang.backend.domain.post.domain.entity.Post; 8 | import com.hjjang.backend.domain.post.domain.repository.PostRepository; 9 | import com.hjjang.backend.domain.post.dto.PostRequestDto; 10 | import com.hjjang.backend.domain.post.exception.PostNotFoundException; 11 | import com.hjjang.backend.domain.user.entity.User; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | 15 | @RequiredArgsConstructor 16 | @Service 17 | public class PostServiceImpl implements PostService { 18 | 19 | private final PostRepository postRepository; 20 | 21 | public Post save(Post post) { 22 | return postRepository.save(post); 23 | } 24 | 25 | public List findAll() { 26 | return postRepository.findAll(); 27 | } 28 | 29 | public Post findOneById(Long id) { 30 | return postRepository.findById(id).orElseThrow(PostNotFoundException::new); 31 | } 32 | 33 | public Post updateOneById(Long id, PostRequestDto postRequestDto, User user) { 34 | Post foundPost = findOneById(id); 35 | return save(foundPost.update(postRequestDto)); 36 | // if (user == foundPost.getUser()) return 37 | // throw new UserNotMatchException("사용자 정보가 일치하지 않습니다.", ErrorCode.NO_AUTHORITY); 38 | } 39 | 40 | public void deleteOneById(Long id) { 41 | findOneById(id).removePost(); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | frontend: 5 | container_name: bauction-frontend 6 | build: 7 | context: ./frontend 8 | dockerfile: Dockerfile 9 | ports: 10 | - "3000:3000" 11 | volumes: 12 | - ./frontend:/app 13 | networks: 14 | - network-tier 15 | 16 | redis: 17 | image: redis:latest 18 | command: redis-server --port 6379 19 | container_name: bauction-redis 20 | ports: 21 | - '6379:6379' 22 | 23 | backend: 24 | container_name: bauction-backend 25 | build: ./backend 26 | # restart: on-failure 27 | depends_on: 28 | - mysqldb 29 | ports: 30 | - '8080:8080' 31 | environment: 32 | spring.datasource.url: "jdbc:mysql://bauction-mysqldb:3306/test_db?useSSL=false&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&autoReconnect=true&createDatabaseIfNotExist=true" 33 | volumes: 34 | - ./backend:/backend 35 | networks: 36 | - network-tier 37 | tty: true 38 | 39 | mysqldb: 40 | image: mysql:5.7 41 | container_name: bauction-mysqldb 42 | env_file: 43 | - "./.env" 44 | command: 45 | [ 46 | "--character-set-server=utf8mb4", 47 | "--collation-server=utf8mb4_unicode_ci", 48 | ] 49 | volumes: 50 | - mysql-data:/var/lib/bauction 51 | ports: 52 | - "3306:3306" 53 | 54 | networks: 55 | - network-tier 56 | platform: linux/amd64 57 | 58 | networks: 59 | network-tier: 60 | external: true 61 | 62 | volumes: 63 | mysql-data: 64 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/filter/TokenAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.filter; 2 | 3 | import com.hjjang.backend.global.security.token.AuthToken; 4 | import com.hjjang.backend.global.security.token.AuthTokenProvider; 5 | import com.hjjang.backend.global.util.HeaderUtil; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import javax.servlet.FilterChain; 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | 18 | @Slf4j 19 | @RequiredArgsConstructor 20 | public class TokenAuthenticationFilter extends OncePerRequestFilter { 21 | private final AuthTokenProvider tokenProvider; 22 | 23 | @Override 24 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 25 | FilterChain filterChain) throws ServletException, IOException { 26 | 27 | String tokenStr = HeaderUtil.getAccessToken(request); 28 | AuthToken token = tokenProvider.convertAuthToken(tokenStr); 29 | 30 | if (token.validate()) { 31 | Authentication authentication = tokenProvider.getAuthentication(token); 32 | SecurityContextHolder.getContext().setAuthentication(authentication); 33 | } 34 | 35 | filterChain.doFilter(request, response); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/service/CategoryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | import com.hjjang.backend.domain.category.domain.entity.Category; 7 | import com.hjjang.backend.domain.category.domain.repository.CategoryRepository; 8 | import com.hjjang.backend.domain.category.dto.CategoryRequest; 9 | import com.hjjang.backend.domain.category.exception.CategoryNotFoundException; 10 | 11 | import lombok.RequiredArgsConstructor; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class CategoryServiceImpl implements CategoryService { 16 | 17 | private final CategoryRepository categoryRepository; 18 | 19 | @Override 20 | @Transactional 21 | public void createNewCategory(CategoryRequest categoryRequest) { 22 | Category category = categoryRequest.toEntity(); 23 | categoryRepository.save(category); 24 | } 25 | 26 | @Override 27 | public Category findAllByCategory() { 28 | return null; 29 | } 30 | 31 | @Override 32 | @Transactional(readOnly = true) 33 | public Category findCategoryById(Long categoryId) { 34 | return categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new); 35 | } 36 | 37 | @Override 38 | public void updateCategory(Category category, CategoryRequest categoryRequest) { 39 | category.updateCategory(categoryRequest); 40 | categoryRepository.save(category); 41 | } 42 | 43 | @Override 44 | public void removeCategory(Category category) { 45 | category.removeCategory(); 46 | categoryRepository.save(category); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/controller/MailController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.controller; 2 | 3 | import static com.hjjang.backend.global.response.code.SuccessCode.*; 4 | import static org.springframework.http.HttpStatus.*; 5 | 6 | import com.hjjang.backend.domain.mail.dto.MailRequest; 7 | import com.hjjang.backend.domain.mail.dto.MailResponse; 8 | import com.hjjang.backend.domain.mail.service.MailService; 9 | import com.hjjang.backend.global.response.response.SuccessResponse; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.RequestBody; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @RestController 18 | @RequiredArgsConstructor 19 | @RequestMapping("/api/mail") 20 | public class MailController { 21 | 22 | private final MailService mailService; 23 | 24 | @PostMapping("/send") 25 | public ResponseEntity send(@RequestBody MailRequest mailRequest) { 26 | String mail = mailRequest.getMail(); 27 | MailResponse mailResponse = mailService.sendMail(mail); 28 | SuccessResponse response = SuccessResponse.of(MAIL_SEND_SUCCESS, mailResponse); 29 | return new ResponseEntity<>(response, CREATED); 30 | } 31 | 32 | @PostMapping("/check") 33 | public ResponseEntity auth(@RequestBody MailRequest mailRequest) { 34 | MailResponse mailResponse = mailService.checkCode(mailRequest); 35 | return ResponseEntity.ok(SuccessResponse.of(MAIL_CHECK_SUCCESS)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/service/UserProfileService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.service; 2 | 3 | import com.hjjang.backend.domain.user.dto.UserProfileDTO; 4 | import com.hjjang.backend.domain.user.entity.User; 5 | import com.hjjang.backend.domain.user.exception.UserNotFoundException; 6 | import com.hjjang.backend.domain.user.repository.UserRepository; 7 | import com.hjjang.backend.global.response.code.ErrorCode; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.persistence.EntityNotFoundException; 12 | import java.util.Optional; 13 | import java.util.stream.Stream; 14 | 15 | @Service 16 | @RequiredArgsConstructor 17 | public class UserProfileService { 18 | private final UserRepository userRepository; 19 | 20 | public UserProfileDTO getUserProfile(String userId) { 21 | 22 | //todo add exception 23 | User user = userRepository.findUserByProviderId(userId).orElseThrow(EntityNotFoundException::new); 24 | 25 | //todo found univName logic 26 | return UserProfileDTO.builder() 27 | .userNickname(user.getNickName()) 28 | .userImageUrl(user.getImageUrl()) 29 | .userMannerTemperature(user.getMannerTemperature()) 30 | .userEmail(user.getEmail()) 31 | .userUnivName("산기대") 32 | .build(); 33 | } 34 | 35 | public User findById(Long id) { 36 | return Stream.of(id) 37 | .map(userRepository::findById) 38 | .filter(Optional::isPresent) 39 | .map(Optional::get) 40 | .findAny() 41 | .orElseThrow(() -> new UserNotFoundException(ErrorCode.MEMBER_NOT_FOUND)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/config/security/properties/AuthProperties.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.config.security.properties; 2 | 3 | import lombok.Data; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @Data 12 | @Configuration 13 | @ConfigurationProperties("auth-properties") 14 | public class AuthProperties { 15 | private TokenProperties tokenProperties = new TokenProperties(); 16 | private JwtProperties jwtProperties = new JwtProperties(); 17 | private CorsProperties corsProperties = new CorsProperties(); 18 | private OAuth2Properties oAuth2Properties = new OAuth2Properties(); 19 | @Data 20 | public static class TokenProperties { 21 | @Value("auth-properties.token-properties.token-secret-key") 22 | private String tokenSecretKey; 23 | private long tokenExpireDate; 24 | private long refreshTokenExpiry; 25 | } 26 | @Data 27 | public static class JwtProperties { 28 | @Value("auth-properties.jwt-properties.secret-key") 29 | private String secretKey; 30 | } 31 | @Data 32 | public static class OAuth2Properties { 33 | @Value("auth-properties.o-auth2-properties.redirect-uris[]") 34 | private List redirectUris = new ArrayList<>(); 35 | } 36 | @Data 37 | public static class CorsProperties { 38 | @Value("auth-properties.cors-properties.allowed-origins") 39 | private String allowedOrigins; 40 | private String allowedMethods; 41 | private String allowedHeaders; 42 | private Long maxAge; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/global/security/CustomSecurityExtension.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security; 2 | 3 | import com.hjjang.backend.domain.user.entity.Agreement; 4 | import com.hjjang.backend.domain.user.entity.RoleType; 5 | import com.hjjang.backend.domain.user.entity.User; 6 | import com.hjjang.backend.domain.user.repository.UserRefreshTokenRepository; 7 | import com.hjjang.backend.domain.user.repository.UserRepository; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.restdocs.RestDocumentationExtension; 12 | 13 | import java.util.Optional; 14 | 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.when; 17 | 18 | @ComponentScan(basePackages = "com.hjjang.backend.global.config.security") 19 | @ComponentScan(basePackages = "com.hjjang.backend.global.security") 20 | @ExtendWith(RestDocumentationExtension.class) 21 | public class CustomSecurityExtension { 22 | 23 | @MockBean 24 | protected UserRepository userRepository; 25 | 26 | @MockBean 27 | protected UserRefreshTokenRepository userRefreshTokenRepository; 28 | 29 | // 인증된 user setup 30 | public void userInfoSetUp() { 31 | User givenUser = User.builder() 32 | .email("kevinkim@email.com") 33 | .imageUrl("이미지입니다아아아") 34 | .isPushAgree(Agreement.AGREE) 35 | .mannerTemperature(36L) 36 | .nickName("김겨여여연") 37 | .providerId("kakao123456") 38 | .role(RoleType.USER) 39 | .univId(1L) 40 | .isEmailVerification(true) 41 | .build(); 42 | when(userRepository.findUserByProviderId(any())).thenReturn(Optional.of(givenUser)); 43 | } 44 | 45 | // todo 인증되지 않은 user setup 46 | 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/parser/KakaoParsingParsingUserContext.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.parser; 2 | 3 | import static com.hjjang.backend.global.security.parser.KakaoParsingParsingUserContext.KakaoInfoProperties.*; 4 | 5 | import java.util.Map; 6 | 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | 10 | public class KakaoParsingParsingUserContext extends ParsingUserContext { 11 | 12 | public KakaoParsingParsingUserContext(Map attributes) { 13 | super(attributes); 14 | } 15 | 16 | // kakao response json spec 17 | // https://developers.kakao.com/docs/latest/ko/kakaologin/common#additional-consent 18 | @Override 19 | public String getId() { 20 | return attributes.get(ID.getCode()).toString(); 21 | } 22 | 23 | @Override 24 | public String getName() { 25 | Map properties = (Map)attributes.get(PROPERTIES.getCode()); 26 | 27 | if (properties == null) { 28 | return null; 29 | } 30 | 31 | return (String)properties.get(NICKNAME.getCode()); 32 | } 33 | 34 | @Override 35 | public String getEmail() { 36 | return (String)attributes.get(EMAIL.getCode()); 37 | } 38 | 39 | @Override 40 | public String getImageUrl() { 41 | Map properties = (Map)attributes.get(PROPERTIES.getCode()); 42 | 43 | if (properties == null) { 44 | return null; 45 | } 46 | 47 | return (String)properties.get(PROPERTIES.getCode()); 48 | } 49 | 50 | @Getter 51 | @AllArgsConstructor 52 | enum KakaoInfoProperties { 53 | ID("id"), 54 | EMAIL("account_email"), 55 | NICKNAME("nickname"), 56 | PROFILE_IMAGE("thumbnail_image"), 57 | PROPERTIES("properties"); 58 | 59 | private final String code; 60 | } 61 | 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/handler/OAuth2AuthenticationFailureHandler.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.handler; 2 | 3 | import com.hjjang.backend.global.security.repository.OAuth2AuthorizationRequestBasedOnCookieRepository; 4 | import com.hjjang.backend.global.util.CookieUtil; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.util.UriComponentsBuilder; 10 | 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.Cookie; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | 17 | @Component 18 | @RequiredArgsConstructor 19 | public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { 20 | 21 | private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository; 22 | 23 | @Override 24 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, 25 | AuthenticationException exception) throws IOException, ServletException { 26 | String targetUrl = CookieUtil.getCookie(request, OAuth2AuthorizationRequestBasedOnCookieRepository.REDIRECT_URI_PARAM_COOKIE_NAME) 27 | .map(Cookie::getValue) 28 | .orElse(("/")); 29 | 30 | exception.printStackTrace(); 31 | 32 | targetUrl = UriComponentsBuilder.fromUriString(targetUrl) 33 | .queryParam("error", exception.getLocalizedMessage()) 34 | .build().toUriString(); 35 | 36 | authorizationRequestRepository.removeAuthorizationRequestCookies(request, response); 37 | 38 | getRedirectStrategy().sendRedirect(request, response, targetUrl); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/global/security/WithMockUserSecurityContextFactory.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security; 2 | 3 | import com.hjjang.backend.domain.user.entity.RoleType; 4 | import com.hjjang.backend.global.config.security.properties.AuthProperties; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.core.context.SecurityContext; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.security.core.userdetails.User; 12 | import org.springframework.security.test.context.support.WithSecurityContextFactory; 13 | 14 | import java.util.Collections; 15 | import java.util.Date; 16 | 17 | 18 | @RequiredArgsConstructor 19 | public class WithMockUserSecurityContextFactory implements WithSecurityContextFactory { 20 | 21 | private final AuthProperties authProperties; 22 | 23 | @Override 24 | public SecurityContext createSecurityContext(WithMockCustomUser customUser) { 25 | SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); 26 | 27 | Date now = new Date(); 28 | 29 | // 가짜 user 정보 설정 30 | User principal = new User( 31 | customUser.providerId(), 32 | "", 33 | Collections.singletonList(new SimpleGrantedAuthority(RoleType.USER.getCode())) 34 | ); 35 | 36 | // 가짜 security 인증 user 저장 37 | Authentication authentication = new UsernamePasswordAuthenticationToken( 38 | principal, 39 | "", 40 | principal.getAuthorities() 41 | ); 42 | securityContext.setAuthentication(authentication); 43 | return securityContext; 44 | } 45 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/dto/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @Getter 10 | @RequiredArgsConstructor 11 | public class ApiResponse { 12 | 13 | private final static int SUCCESS = 200; 14 | private final static int NOT_FOUND = 400; 15 | private final static int FAILED = 500; 16 | private final static String SUCCESS_MESSAGE = "SUCCESS"; 17 | private final static String NOT_FOUND_MESSAGE = "NOT FOUND"; 18 | private final static String FAILED_MESSAGE = "서버에서 오류가 발생하였습니다."; 19 | private final static String INVALID_ACCESS_TOKEN = "Invalid access token."; 20 | private final static String INVALID_REFRESH_TOKEN = "Invalid refresh token."; 21 | private final static String NOT_EXPIRED_TOKEN_YET = "Not expired token yet."; 22 | 23 | private final ApiResponseHeader header; 24 | private final Map body; 25 | 26 | public static ApiResponse success(String name, T body) { 27 | Map map = new HashMap<>(); 28 | map.put(name, body); 29 | 30 | return new ApiResponse(new ApiResponseHeader(SUCCESS, SUCCESS_MESSAGE), map); 31 | } 32 | 33 | public static ApiResponse fail() { 34 | return new ApiResponse(new ApiResponseHeader(FAILED, FAILED_MESSAGE), null); 35 | } 36 | 37 | public static ApiResponse invalidAccessToken() { 38 | return new ApiResponse(new ApiResponseHeader(FAILED, INVALID_ACCESS_TOKEN), null); 39 | } 40 | 41 | public static ApiResponse invalidRefreshToken() { 42 | return new ApiResponse(new ApiResponseHeader(FAILED, INVALID_REFRESH_TOKEN), null); 43 | } 44 | 45 | public static ApiResponse notExpiredTokenYet() { 46 | return new ApiResponse(new ApiResponseHeader(FAILED, NOT_EXPIRED_TOKEN_YET), null); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/user/controller/docs/UserRestDocument.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.controller.docs; 2 | 3 | import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; 4 | import org.springframework.restdocs.operation.preprocess.Preprocessors; 5 | import org.springframework.restdocs.payload.JsonFieldType; 6 | 7 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; 8 | import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; 9 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; 10 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 11 | import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; 12 | 13 | public class UserRestDocument { 14 | public static RestDocumentationResultHandler getProfile() { 15 | return document("v1/users/profile", 16 | Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), 17 | Preprocessors.preprocessResponse(Preprocessors.prettyPrint()), 18 | requestHeaders( 19 | headerWithName("Authorization").description("Bearer 토큰")), 20 | responseFields( 21 | fieldWithPath("code").type(JsonFieldType.STRING).description("Business code"), 22 | fieldWithPath("message").type(JsonFieldType.STRING).description("response message"), 23 | fieldWithPath("data.userNickname").type(JsonFieldType.STRING).description("user 닉네임"), 24 | fieldWithPath("data.userImageUrl").type(JsonFieldType.STRING).description("user 프로필 사진"), 25 | fieldWithPath("data.userEmail").type(JsonFieldType.STRING).description("user 이메일"), 26 | fieldWithPath("data.userMannerTemperature").type(JsonFieldType.NUMBER).description("user 매너 온도"), 27 | fieldWithPath("data.userUnivName").type(JsonFieldType.STRING).description("user 대학 이름") 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.config; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springdoc.core.GroupedOpenApi; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import io.swagger.v3.oas.models.Components; 10 | import io.swagger.v3.oas.models.ExternalDocumentation; 11 | import io.swagger.v3.oas.models.OpenAPI; 12 | import io.swagger.v3.oas.models.info.Info; 13 | import io.swagger.v3.oas.models.info.License; 14 | import io.swagger.v3.oas.models.security.SecurityRequirement; 15 | import io.swagger.v3.oas.models.security.SecurityScheme; 16 | 17 | @Configuration 18 | public class SwaggerConfig { 19 | SecurityScheme securityScheme = new SecurityScheme() 20 | .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") 21 | .in(SecurityScheme.In.HEADER).name("Authorization"); 22 | SecurityRequirement securityRequirement = new SecurityRequirement().addList("Bearer Token"); 23 | 24 | @Bean 25 | public GroupedOpenApi categoryApi() { 26 | return GroupedOpenApi.builder() 27 | .group("categories") 28 | .pathsToMatch("/categories/**") 29 | .build(); 30 | } 31 | 32 | @Bean 33 | public GroupedOpenApi mailApi() { 34 | return GroupedOpenApi.builder() 35 | .group("mail") 36 | .pathsToMatch("/api/mail/**") 37 | .build(); 38 | } 39 | 40 | @Bean 41 | public OpenAPI bauctionOpenAPI() { 42 | return new OpenAPI() 43 | .info(new Info().title("bauction API") 44 | .description("중고거래 서비스 bauction의 API 입니다.") 45 | .version("v0.0.1") 46 | .license(new License().name("Apache 2.0").url("http://springdoc.org"))) 47 | .externalDocs(new ExternalDocumentation() 48 | .description("github link") 49 | .url("https://github.com/h-jjang/bauction")) 50 | .components(new Components().addSecuritySchemes("Bearer Token", securityScheme)) 51 | .security(Arrays.asList(securityRequirement)); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/dto/TradeMapper.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.dto; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import com.hjjang.backend.domain.post.exception.PostNotFoundException; 5 | import com.hjjang.backend.domain.post.service.PostServiceImpl; 6 | import com.hjjang.backend.domain.trade.domain.entity.Trade; 7 | import com.hjjang.backend.domain.user.entity.User; 8 | import com.hjjang.backend.domain.user.service.UserProfileService; 9 | import com.hjjang.backend.global.execption.BusinessException; 10 | import com.hjjang.backend.global.response.code.ErrorCode; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.function.LongFunction; 15 | import java.util.stream.Stream; 16 | 17 | @Component 18 | @RequiredArgsConstructor 19 | public class TradeMapper { 20 | 21 | private final PostServiceImpl postService; 22 | private final UserProfileService userProfileService; 23 | 24 | public Trade toEntity(TradeRequestDto requestDto) { 25 | 26 | LongFunction findUser = userId -> Stream.of(userId) 27 | .map(userProfileService::findById) 28 | .findAny() 29 | .orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND)); 30 | 31 | LongFunction findPost = postId -> Stream.of(postId) 32 | .map(postService::findOneById) 33 | .findAny() 34 | .orElseThrow(PostNotFoundException::new); 35 | 36 | return Trade.builder() 37 | .post(findPost.apply(requestDto.getPostId())) 38 | .buyer(findUser.apply(requestDto.getBuyerId())) 39 | .seller(findUser.apply(requestDto.getSellerId())) 40 | .build(); 41 | } 42 | 43 | public TradeResponseDto fromEntity(Trade entity) { 44 | return TradeResponseDto.builder() 45 | .id(entity.getId()) 46 | .postId(entity.getPost().getId()) 47 | .buyerId(entity.getBuyer().getId()) 48 | .sellerId(entity.getSeller().getId()) 49 | .state(entity.getTradeState().getState()) 50 | .build(); 51 | } 52 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/util/CookieUtil.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.util; 2 | 3 | import java.util.Base64; 4 | import java.util.Optional; 5 | 6 | import javax.servlet.http.Cookie; 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | import org.springframework.util.SerializationUtils; 11 | 12 | public class CookieUtil { 13 | 14 | public static Optional getCookie(HttpServletRequest request, String name) { 15 | Cookie[] cookies = request.getCookies(); 16 | 17 | if (cookies != null && cookies.length > 0) { 18 | for (Cookie cookie : cookies) { 19 | if (name.equals(cookie.getName())) { 20 | return Optional.of(cookie); 21 | } 22 | } 23 | } 24 | return Optional.empty(); 25 | } 26 | 27 | public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { 28 | Cookie cookie = new Cookie(name, value); 29 | cookie.setPath("/"); 30 | cookie.setHttpOnly(true); 31 | cookie.setMaxAge(maxAge); 32 | 33 | response.addCookie(cookie); 34 | } 35 | 36 | public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { 37 | Cookie[] cookies = request.getCookies(); 38 | 39 | if (cookies != null && cookies.length > 0) { 40 | for (Cookie cookie : cookies) { 41 | if (name.equals(cookie.getName())) { 42 | cookie.setValue(""); 43 | cookie.setPath("/"); 44 | cookie.setMaxAge(0); 45 | response.addCookie(cookie); 46 | } 47 | } 48 | } 49 | } 50 | 51 | public static String serialize(Object obj) { 52 | return Base64.getUrlEncoder() 53 | .encodeToString(SerializationUtils.serialize(obj)); 54 | } 55 | 56 | public static T deserialize(Cookie cookie, Class cls) { 57 | return cls.cast( 58 | SerializationUtils.deserialize( 59 | Base64.getUrlDecoder().decode(cookie.getValue()) 60 | ) 61 | ); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/image/controller/ImageController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.image.controller; 2 | 3 | import com.hjjang.backend.domain.image.domain.entity.Image; 4 | import com.hjjang.backend.domain.image.dto.ImagePathResponse; 5 | import com.hjjang.backend.domain.image.service.ImageService; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | import org.springframework.web.multipart.MultipartFile; 11 | 12 | import java.io.IOException; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import static com.hjjang.backend.global.util.HttpStatusResponseEntity.RESPONSE_OK; 17 | 18 | @RestController 19 | @RequiredArgsConstructor 20 | @RequestMapping("/api/images") 21 | public class ImageController { 22 | 23 | private final ImageService imageService; 24 | 25 | @GetMapping() 26 | public ResponseEntity> findAllImage() { 27 | return ResponseEntity.ok(imageService 28 | .findAll() 29 | .stream() 30 | .map(ImagePathResponse::of) 31 | .collect(Collectors.toList()) 32 | ); 33 | } 34 | 35 | @PostMapping() 36 | public ResponseEntity uploadImage(@RequestParam("images") MultipartFile multipartFile) throws IOException { 37 | return ResponseEntity.ok(ImagePathResponse 38 | .of(imageService 39 | .uploadNewImage(multipartFile) 40 | ) 41 | ); 42 | } 43 | 44 | @GetMapping("/{imageId}") 45 | public ResponseEntity findImage(@PathVariable Long imageId) { 46 | return ResponseEntity.ok(ImagePathResponse 47 | .of(imageService 48 | .findImageById(imageId) 49 | ) 50 | ); 51 | } 52 | 53 | @DeleteMapping("/{imageId}") 54 | public ResponseEntity deleteImage(@PathVariable Long imageId) { 55 | Image image = imageService.findImageById(imageId); 56 | imageService.removeImage(image); 57 | 58 | return RESPONSE_OK; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/infra/aws/service/S3ImageUploader.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.infra.aws.service; 2 | 3 | import com.amazonaws.services.s3.AmazonS3Client; 4 | import com.amazonaws.services.s3.model.CannedAccessControlList; 5 | import com.amazonaws.services.s3.model.PutObjectRequest; 6 | import com.hjjang.backend.domain.image.service.ImageUploader; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.multipart.MultipartFile; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Service 19 | public class S3ImageUploader extends ImageUploader { 20 | 21 | private final AmazonS3Client amazonS3Client; 22 | 23 | @Value("${cloud.aws.s3.bucket}") 24 | public String bucket; // S3 버킷 이름 25 | 26 | @Override 27 | public String upload(MultipartFile multipartFile) throws IOException { 28 | File uploadFile = 29 | this.localUpload(multipartFile) // 파일 변환할 수 없으면 에러 30 | .orElseThrow( 31 | () -> new IllegalArgumentException("error: MultipartFile -> File convert fail")); 32 | 33 | return upload(uploadFile); 34 | } 35 | 36 | // S3로 파일 업로드하기 37 | private String upload(File uploadFile) { 38 | String uploadImageUrl = putS3(uploadFile, "static/" + uploadFile.getName()); // s3로 업로드 39 | removeNewFile(uploadFile); 40 | return uploadImageUrl; 41 | } 42 | 43 | // S3로 업로드 44 | private String putS3(File uploadFile, String fileName) { 45 | amazonS3Client.putObject( 46 | new PutObjectRequest(bucket, fileName, uploadFile) 47 | .withCannedAcl(CannedAccessControlList.PublicRead)); 48 | return amazonS3Client.getUrl(bucket, fileName).toString(); 49 | } 50 | 51 | // 로컬에 저장된 이미지 지우기 52 | private void removeNewFile(File targetFile) { 53 | if (targetFile.delete()) { 54 | log.info("File delete success"); 55 | return; 56 | } 57 | log.info("File delete fail"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/search/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.search.controller; 2 | 3 | import com.hjjang.backend.domain.post.dto.PostMapper; 4 | import com.hjjang.backend.domain.search.service.SearchServiceImpl; 5 | import com.hjjang.backend.global.dto.ApiResponse; 6 | import com.hjjang.backend.global.util.UserUtil; 7 | import java.util.stream.Collectors; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.data.domain.Sort.Direction; 11 | import org.springframework.data.web.PageableDefault; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @RequiredArgsConstructor 20 | @RequestMapping("/api/searches") 21 | public class SearchController { 22 | 23 | private final SearchServiceImpl searchService; 24 | private final PostMapper postMapper; 25 | private final UserUtil userUtil; 26 | 27 | @GetMapping("/all") 28 | public ResponseEntity searchPosts( 29 | @RequestParam(required = false) String filter, 30 | @PageableDefault(sort = "time", direction = Direction.DESC) Pageable pageable) { 31 | return ResponseEntity.ok( 32 | ApiResponse.success( 33 | "searchPosts", 34 | searchService.findAll(filter, pageable, userUtil.getLoginUserByToken()) 35 | .stream() 36 | .map(postMapper::fromEntity) 37 | .collect(Collectors.toList())) 38 | ); 39 | } 40 | 41 | @GetMapping 42 | public ResponseEntity searchPostsByKeyword( 43 | @RequestParam String keyword, 44 | @RequestParam(required = false) String filter, 45 | @PageableDefault(sort = "time", direction = Direction.DESC) Pageable pageable) { 46 | return ResponseEntity.ok( 47 | ApiResponse.success( 48 | "searchPostsByKeyword", 49 | searchService.findByKeyword(keyword, filter, pageable, userUtil.getLoginUserByToken()) 50 | .stream() 51 | .map(postMapper::fromEntity) 52 | .collect(Collectors.toList()) 53 | ) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/user/repository/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.repository; 2 | 3 | import com.hjjang.backend.domain.user.entity.Agreement; 4 | import com.hjjang.backend.domain.user.entity.RoleType; 5 | import com.hjjang.backend.domain.user.entity.User; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 11 | 12 | import javax.persistence.EntityExistsException; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertAll; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | @DataJpaTest 18 | class UserRepositoryTest { 19 | 20 | @Autowired 21 | private UserRepository userRepository; 22 | 23 | private User givenUser; 24 | private User newUser; 25 | 26 | @BeforeEach 27 | void setUp() { 28 | givenUser = User.builder() 29 | .email("tester@tukorea.ac.kr") 30 | .imageUrl("이미지 url") 31 | .isPushAgree(Agreement.AGREE) 32 | .mannerTemperature(36L) 33 | .nickName("kevinkim") 34 | .providerId("123412512") 35 | .role(RoleType.USER) 36 | .univId(null) 37 | .isEmailVerification(false) 38 | .build(); 39 | 40 | newUser = userRepository.save(givenUser); 41 | } 42 | 43 | @DisplayName("userId로 사용자 조회") 44 | @Test 45 | void findUserById_test() { 46 | System.out.println(newUser.getProviderId()); 47 | User foundUser = userRepository.findUserById(newUser.getId()).orElseThrow(EntityExistsException::new); 48 | 49 | assertAll( 50 | () -> assertEquals(foundUser, newUser) 51 | ); 52 | } 53 | 54 | @DisplayName("providerId로 사용자 조회") 55 | @Test 56 | void findUserByProviderId_test() { 57 | 58 | User foundUser = userRepository.findUserByProviderId(newUser.getProviderId()) 59 | .orElseThrow(EntityExistsException::new); 60 | 61 | assertAll( 62 | () -> assertEquals(foundUser, newUser) 63 | ); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/token/AuthTokenProvider.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.token; 2 | 3 | import com.hjjang.backend.global.security.exception.TokenValidFailedException; 4 | import io.jsonwebtoken.Claims; 5 | import io.jsonwebtoken.security.Keys; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 11 | import org.springframework.security.core.userdetails.User; 12 | 13 | import java.security.Key; 14 | import java.util.Arrays; 15 | import java.util.Collection; 16 | import java.util.Date; 17 | import java.util.stream.Collectors; 18 | 19 | @Slf4j 20 | public class AuthTokenProvider { 21 | 22 | private final Key key; 23 | private static final String AUTHORITIES_KEY = "role"; 24 | 25 | public AuthTokenProvider(String secret) { 26 | this.key = Keys.hmacShaKeyFor(secret.getBytes()); 27 | } 28 | 29 | public AuthToken createAuthToken(String id, Date expiry) { 30 | return new AuthToken(id, expiry, key); 31 | } 32 | 33 | public AuthToken createAuthToken(String id, String role, Date expiry) { 34 | return new AuthToken(id, role, expiry, key); 35 | } 36 | 37 | public AuthToken convertAuthToken(String token) { 38 | return new AuthToken(token, key); 39 | } 40 | 41 | public Authentication getAuthentication(AuthToken authToken) { 42 | 43 | if (authToken.validate()) { 44 | 45 | Claims claims = authToken.getTokenClaims(); 46 | Collection authorities = 47 | Arrays.stream(new String[] {claims.get(AUTHORITIES_KEY).toString()}) 48 | .map(SimpleGrantedAuthority::new) 49 | .collect(Collectors.toList()); 50 | 51 | log.debug("claims subject := [{}]", claims.getSubject()); 52 | User principal = new User(claims.getSubject(), "", authorities); 53 | 54 | return new UsernamePasswordAuthenticationToken(principal, authToken, authorities); 55 | } else { 56 | throw new TokenValidFailedException(); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/category/controller/CategoryController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.category.controller; 2 | 3 | import static com.hjjang.backend.global.util.HttpStatusResponseEntity.*; 4 | 5 | import javax.validation.Valid; 6 | 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.DeleteMapping; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.PutMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import com.hjjang.backend.domain.category.domain.entity.Category; 19 | import com.hjjang.backend.domain.category.dto.CategoryRequest; 20 | import com.hjjang.backend.domain.category.dto.CategoryResponse; 21 | import com.hjjang.backend.domain.category.service.CategoryService; 22 | 23 | import lombok.RequiredArgsConstructor; 24 | 25 | @RestController 26 | @RequiredArgsConstructor 27 | @RequestMapping("/categories") 28 | public class CategoryController { 29 | 30 | private final CategoryService categoryService; 31 | 32 | @PostMapping 33 | public ResponseEntity createCategory(@RequestBody @Valid CategoryRequest categoryRequest) { 34 | categoryService.createNewCategory(categoryRequest); 35 | 36 | return RESPONSE_OK; 37 | } 38 | 39 | @GetMapping("/{categoryId}") 40 | public ResponseEntity findCategory(@PathVariable Long categoryId) { 41 | return ResponseEntity.ok(CategoryResponse.of(categoryService.findCategoryById(categoryId))); 42 | } 43 | 44 | @PutMapping("/{categoryId}") 45 | public ResponseEntity updateCategory(@Valid @RequestBody CategoryRequest categoryRequest, 46 | @PathVariable Long categoryId) { 47 | Category category = categoryService.findCategoryById(categoryId); 48 | categoryService.updateCategory(category, categoryRequest); 49 | return RESPONSE_OK; 50 | } 51 | 52 | @DeleteMapping("/{categoryId}") 53 | public ResponseEntity deleteCategory(@PathVariable Long categoryId) { 54 | Category category = categoryService.findCategoryById(categoryId); 55 | 56 | categoryService.removeCategory(category); 57 | 58 | return RESPONSE_OK; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/domain/entity/Trade.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.domain.entity; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import com.hjjang.backend.domain.post.domain.entity.PostState; 5 | import com.hjjang.backend.domain.user.entity.User; 6 | import com.hjjang.backend.global.domain.BaseTimeEntity; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.RequiredArgsConstructor; 11 | 12 | import javax.persistence.*; 13 | 14 | @Entity 15 | @Getter 16 | @AllArgsConstructor 17 | @RequiredArgsConstructor 18 | public class Trade extends BaseTimeEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @ManyToOne(cascade = CascadeType.ALL) 25 | @JoinColumn(name = "post_id") 26 | private Post post; 27 | 28 | @ManyToOne(cascade = CascadeType.ALL) 29 | @JoinColumn(name = "buyer_id") 30 | private User buyer; 31 | 32 | @ManyToOne(cascade = CascadeType.ALL) 33 | @JoinColumn(name = "seller_id") 34 | private User seller; 35 | 36 | @Column(nullable = false) 37 | @Enumerated(EnumType.STRING) 38 | private TradeState tradeState; 39 | 40 | @Column(nullable = false) 41 | private boolean removed = false; 42 | 43 | @Builder 44 | public Trade(Post post, User buyer, User seller) { 45 | this.post = post; 46 | this.buyer = buyer; 47 | this.seller = seller; 48 | this.tradeState = TradeState.PENDING; 49 | } 50 | 51 | public Trade update(Trade entity) { 52 | this.post = entity.getPost(); 53 | this.buyer = entity.getBuyer(); 54 | this.seller = entity.getSeller(); 55 | return this; 56 | } 57 | 58 | public Trade remove() { 59 | this.removed = true; 60 | return this; 61 | } 62 | 63 | public Trade setTradeState(TradeState tradeState) { 64 | this.tradeState = tradeState; 65 | return this; 66 | } 67 | 68 | public Post setPostState() { 69 | if (tradeState.equals(TradeState.PENDING)) { 70 | post.setIsSaleCompletion(PostState.SALE); 71 | } 72 | if (tradeState.equals(TradeState.APPROVE)) { 73 | post.setIsSaleCompletion(PostState.SOLD); 74 | } 75 | if (tradeState.equals(TradeState.RESERVE)) { 76 | post.setIsSaleCompletion(PostState.RESERVED); 77 | } 78 | if (tradeState.equals(TradeState.CANCEL)) { 79 | post.setIsSaleCompletion(PostState.SALE); 80 | } 81 | return post; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/response/response/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.response.response; 2 | 3 | import com.hjjang.backend.global.response.code.ErrorCode; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.validation.BindingResult; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Getter 13 | @NoArgsConstructor 14 | public class ErrorResponse { 15 | 16 | private int status; 17 | private String code; 18 | private String message; 19 | private List errors; 20 | 21 | private ErrorResponse(final ErrorCode code, final List errors) { 22 | this.message = code.getMessage(); 23 | this.errors = errors; 24 | this.code = code.getCode(); 25 | } 26 | 27 | private ErrorResponse(final ErrorCode code) { 28 | this.message = code.getMessage(); 29 | this.code = code.getCode(); 30 | this.errors = new ArrayList<>(); 31 | } 32 | 33 | public static ErrorResponse of(final ErrorCode code) { 34 | return new ErrorResponse(code); 35 | } 36 | 37 | public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) { 38 | return new ErrorResponse(code, FieldError.of(bindingResult)); 39 | } 40 | 41 | @Getter 42 | @NoArgsConstructor 43 | public static class FieldError { 44 | private String field; 45 | private String value; 46 | private String reason; 47 | 48 | public FieldError(final String field, final String value, final String reason) { 49 | this.field = field; 50 | this.value = value; 51 | this.reason = reason; 52 | } 53 | 54 | public static List of(final String field, final String value, final String reason) { 55 | List fieldErrors = new ArrayList<>(); 56 | fieldErrors.add(new FieldError(field, value, reason)); 57 | return fieldErrors; 58 | } 59 | 60 | private static List of(final BindingResult bindingResult) { 61 | final List fieldErrors = bindingResult.getFieldErrors(); 62 | return fieldErrors.stream() 63 | .map(error -> new FieldError( 64 | error.getField(), 65 | error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(), 66 | error.getDefaultMessage())) 67 | .collect(Collectors.toList()); 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/post/domain/entity/PostTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.entity; 2 | 3 | import com.hjjang.backend.domain.post.dto.PostRequestDto; 4 | import com.hjjang.backend.domain.user.entity.Agreement; 5 | import com.hjjang.backend.domain.user.entity.RoleType; 6 | import com.hjjang.backend.domain.user.entity.User; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class PostTest { 14 | 15 | private Post post; 16 | 17 | private final String givenTitle = "test title"; 18 | private final String givenContent = "test content"; 19 | private final int givenItemPrice = 10000; 20 | 21 | @BeforeEach 22 | void setUp() { 23 | User givenUser = User.builder() 24 | .providerId("test Id") 25 | .email("user@email.ac.kr") 26 | .imageUrl("test path") 27 | .isPushAgree(Agreement.AGREE) 28 | .mannerTemperature(36L) 29 | .nickName("test NN") 30 | .role(RoleType.USER) 31 | .univId(1L) 32 | .build(); 33 | 34 | post = Post.builder() 35 | .user(givenUser) 36 | .title(givenTitle) 37 | .content(givenContent) 38 | .itemPrice(givenItemPrice) 39 | .build(); 40 | } 41 | 42 | @Test 43 | @DisplayName("Post 삭제 처리 테스트") 44 | void removePostTest() { 45 | // given 46 | 47 | // when 48 | post.removePost(); 49 | // then 50 | assertTrue(post.isRemoved()); 51 | } 52 | 53 | @Test 54 | @DisplayName("Post 갱신 테스트") 55 | void updateTest() { 56 | // given 57 | PostRequestDto givenPostRequestDto = PostRequestDto.builder() 58 | .title(givenTitle) 59 | .content(givenContent) 60 | .price(givenItemPrice) 61 | .build(); 62 | 63 | Post expectPost = Post.builder() 64 | .title(givenTitle) 65 | .content(givenContent) 66 | .itemPrice(givenItemPrice) 67 | .build(); 68 | 69 | // when 70 | Post actualPost = post.update(givenPostRequestDto); 71 | // then 72 | assertAll( 73 | () -> assertEquals(expectPost.getTitle(), actualPost.getTitle()), 74 | () -> assertEquals(expectPost.getContent(), actualPost.getContent()), 75 | () -> assertEquals(expectPost.getItemPrice(), actualPost.getItemPrice()) 76 | ); 77 | } 78 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/token/AuthToken.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.token; 2 | 3 | import io.jsonwebtoken.*; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.security.Key; 9 | import java.util.Date; 10 | 11 | @Slf4j 12 | @RequiredArgsConstructor 13 | public class AuthToken { 14 | 15 | @Getter 16 | private final String token; 17 | private final Key key; 18 | 19 | private static final String AUTHORITIES_KEY = "role"; 20 | 21 | AuthToken(String id, Date expiry, Key key) { 22 | this.key = key; 23 | this.token = createAuthToken(id, expiry); 24 | } 25 | 26 | AuthToken(String id, String role, Date expiry, Key key) { 27 | this.key = key; 28 | this.token = createAuthToken(id, role, expiry); 29 | } 30 | 31 | private String createAuthToken(String id, Date expiry) { 32 | return Jwts.builder() 33 | .setSubject(id) 34 | .signWith(key, SignatureAlgorithm.HS256) 35 | .setExpiration(expiry) 36 | .compact(); 37 | } 38 | 39 | private String createAuthToken(String id, String role, Date expiry) { 40 | return Jwts.builder() 41 | .setSubject(id) 42 | .claim(AUTHORITIES_KEY, role) 43 | .signWith(key, SignatureAlgorithm.HS256) 44 | .setExpiration(expiry) 45 | .compact(); 46 | } 47 | 48 | public boolean validate() { 49 | return this.getTokenClaims() != null; 50 | } 51 | 52 | public Claims getTokenClaims() { 53 | try { 54 | return Jwts.parserBuilder() 55 | .setSigningKey(key) 56 | .build() 57 | .parseClaimsJws(token) 58 | .getBody(); 59 | } catch (SecurityException e) { 60 | log.info("Invalid JWT signature."); 61 | } catch (MalformedJwtException e) { 62 | log.info("Invalid JWT token."); 63 | } catch (ExpiredJwtException e) { 64 | log.info("Expired JWT token."); 65 | } catch (UnsupportedJwtException e) { 66 | log.info("Unsupported JWT token."); 67 | } catch (IllegalArgumentException e) { 68 | log.info("JWT token compact of handler are invalid."); 69 | } 70 | return null; 71 | } 72 | 73 | public Claims getExpiredTokenClaims() { 74 | try { 75 | Jwts.parserBuilder() 76 | .setSigningKey(key) 77 | .build() 78 | .parseClaimsJws(token) 79 | .getBody(); 80 | } catch (ExpiredJwtException e) { 81 | log.info("Expired JWT token."); 82 | return e.getClaims(); 83 | } 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | ### Node template 25 | # Logs 26 | logs 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | lerna-debug.log* 32 | 33 | # Diagnostic reports (https://nodejs.org/api/report.html) 34 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 35 | 36 | # Runtime data 37 | pids 38 | *.pid 39 | *.seed 40 | *.pid.lock 41 | 42 | # Directory for instrumented libs generated by jscoverage/JSCover 43 | lib-cov 44 | 45 | # Coverage directory used by tools like istanbul 46 | coverage 47 | *.lcov 48 | 49 | # nyc test coverage 50 | .nyc_output 51 | 52 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 53 | .grunt 54 | 55 | # Bower dependency directory (https://bower.io/) 56 | bower_components 57 | 58 | # node-waf configuration 59 | .lock-wscript 60 | 61 | # Compiled binary addons (https://nodejs.org/api/addons.html) 62 | build/Release 63 | 64 | # Dependency directories 65 | node_modules/ 66 | jspm_packages/ 67 | 68 | # Snowpack dependency directory (https://snowpack.dev/) 69 | web_modules/ 70 | 71 | # TypeScript cache 72 | *.tsbuildinfo 73 | 74 | # Optional npm cache directory 75 | .npm 76 | 77 | # Optional eslint cache 78 | .eslintcache 79 | 80 | # Microbundle cache 81 | .rpt2_cache/ 82 | .rts2_cache_cjs/ 83 | .rts2_cache_es/ 84 | .rts2_cache_umd/ 85 | 86 | # Optional REPL history 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | *.tgz 91 | 92 | # Yarn Integrity file 93 | .yarn-integrity 94 | 95 | # dotenv environment variables file 96 | .env 97 | .env.test 98 | 99 | # parcel-bundler cache (https://parceljs.org/) 100 | .cache 101 | .parcel-cache 102 | 103 | # Next.js build output 104 | .next 105 | out 106 | 107 | # Nuxt.js build / generate output 108 | .nuxt 109 | dist 110 | 111 | # Gatsby files 112 | .cache/ 113 | # Comment in the public line in if your project uses Gatsby and not Next.js 114 | # https://nextjs.org/blog/next-9-1#public-directory-support 115 | # public 116 | 117 | # vuepress build output 118 | .vuepress/dist 119 | 120 | # Serverless directories 121 | .serverless/ 122 | 123 | # FuseBox cache 124 | .fusebox/ 125 | 126 | # DynamoDB Local files 127 | .dynamodb/ 128 | 129 | # TernJS port file 130 | .tern-port 131 | 132 | # Stores VSCode versions used for testing VSCode extensions 133 | .vscode-test 134 | 135 | # yarn v2 136 | .yarn/cache 137 | .yarn/unplugged 138 | .yarn/build-state.yml 139 | .yarn/install-state.gz 140 | .pnp.* 141 | 142 | -------------------------------------------------------------------------------- /backend/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/trade/service/TradeService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.trade.service; 2 | 3 | import com.hjjang.backend.domain.mail.service.NoticeMailService; 4 | import com.hjjang.backend.domain.post.domain.entity.Post; 5 | import com.hjjang.backend.domain.post.domain.repository.PostRepository; 6 | import com.hjjang.backend.domain.trade.domain.entity.Trade; 7 | import com.hjjang.backend.domain.trade.domain.entity.TradeState; 8 | import com.hjjang.backend.domain.trade.domain.repositroy.TradeRepository; 9 | import com.hjjang.backend.domain.trade.exception.TradeNotFoundException; 10 | import com.hjjang.backend.global.util.UserUtil; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.stream.Stream; 17 | 18 | import static com.hjjang.backend.global.response.code.ErrorCode.TRADE_NOT_UPDATED; 19 | 20 | @Service 21 | @RequiredArgsConstructor 22 | public class TradeService { 23 | 24 | private final TradeRepository repository; 25 | private final PostRepository postRepository; 26 | private final NoticeMailService mailService; 27 | private final UserUtil userUtil; 28 | 29 | public Trade save(Trade entity) { 30 | return Stream.of(entity) 31 | .map(repository::save) 32 | .findAny() 33 | .orElseThrow(() -> new TradeNotFoundException(null)); 34 | } 35 | 36 | public Trade findById(long id) { 37 | return Stream.of(id) 38 | .map(repository::findById) 39 | .filter(Optional::isPresent) 40 | .map(Optional::get) 41 | .findAny() 42 | .orElseThrow(() -> new TradeNotFoundException(null)); 43 | } 44 | 45 | public List findAll() { 46 | return repository.findAll(); 47 | } 48 | 49 | public Trade update(long id, Trade entity) { 50 | return Stream.of(id) 51 | .map(this::findById) 52 | .map(trade -> trade.update(entity)) 53 | .map(this::save) 54 | .findAny() 55 | .orElseThrow(() -> new TradeNotFoundException(TRADE_NOT_UPDATED)); 56 | } 57 | 58 | public void remove(long id) { 59 | Stream.of(id) 60 | .map(this::findById) 61 | .map(Trade::remove) 62 | .forEach(this::save); 63 | } 64 | 65 | public Trade changeState(long id, TradeState tradeState) { 66 | return Stream.of(id) 67 | .map(this::findById) 68 | .map(trade -> trade.setTradeState(tradeState)) 69 | .map(this::save) 70 | .peek(trade -> { 71 | Post post = trade.setPostState(); 72 | postRepository.save(post); 73 | mailService.sendNotice(userUtil.getLoginUserByToken(), post); 74 | }) 75 | .findAny() 76 | .orElseThrow(() -> new TradeNotFoundException(TRADE_NOT_UPDATED)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/user/service/UserProfileServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.service; 2 | 3 | import com.hjjang.backend.domain.user.dto.UserProfileDTO; 4 | import com.hjjang.backend.domain.user.entity.Agreement; 5 | import com.hjjang.backend.domain.user.entity.RoleType; 6 | import com.hjjang.backend.domain.user.entity.User; 7 | import com.hjjang.backend.domain.user.repository.UserRepository; 8 | import com.hjjang.backend.global.security.WithMockCustomUser; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | 16 | import java.util.Optional; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertAll; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.when; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | class UserProfileServiceTest { 25 | 26 | @InjectMocks 27 | private UserProfileService userProfileService; 28 | 29 | @Mock 30 | private UserRepository userRepository; 31 | 32 | @Test 33 | @WithMockCustomUser 34 | @DisplayName("유저 조회 api test") 35 | void getUserProfileTest() { 36 | // given 37 | User givenUser = User.builder() 38 | .email("tester@tukorea.ac.kr") 39 | .imageUrl("이미지 url") 40 | .isPushAgree(Agreement.AGREE) 41 | .mannerTemperature(36L) 42 | .nickName("kevinkim") 43 | .providerId("123412512") 44 | .role(RoleType.USER) 45 | .univId(null) 46 | .isEmailVerification(true) 47 | .build(); 48 | 49 | UserProfileDTO exceptUserProfileDTO = UserProfileDTO.builder() 50 | .userEmail("tester@tukorea.ac.kr") 51 | .userImageUrl("이미지 url") 52 | .userMannerTemperature(36L) 53 | .userNickname("kevinkim") 54 | .userUnivName("산기대") 55 | .build(); 56 | 57 | // when 58 | when(userRepository.findUserByProviderId(any())).thenReturn(Optional.of(givenUser)); 59 | UserProfileDTO actualUserProfileDTO = userProfileService.getUserProfile(givenUser.getProviderId()); 60 | 61 | // then 62 | assertAll( 63 | () -> assertEquals(exceptUserProfileDTO.getUserEmail(), actualUserProfileDTO.getUserEmail()), 64 | () -> assertEquals(exceptUserProfileDTO.getUserNickname(), actualUserProfileDTO.getUserNickname()), 65 | () -> assertEquals(exceptUserProfileDTO.getUserImageUrl(), actualUserProfileDTO.getUserImageUrl()), 66 | () -> assertEquals(exceptUserProfileDTO.getUserMannerTemperature(), actualUserProfileDTO.getUserMannerTemperature()), 67 | () -> assertEquals(exceptUserProfileDTO.getUserUnivName(), actualUserProfileDTO.getUserUnivName()) 68 | ); 69 | 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/service/CustomOAuth2UserService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.service; 2 | 3 | import com.hjjang.backend.domain.user.entity.Agreement; 4 | import com.hjjang.backend.domain.user.entity.RoleType; 5 | import com.hjjang.backend.domain.user.entity.User; 6 | import com.hjjang.backend.domain.user.repository.UserRepository; 7 | import com.hjjang.backend.global.security.parser.KakaoParsingParsingUserContext; 8 | import com.hjjang.backend.global.security.parser.ParsingUserContext; 9 | import com.hjjang.backend.global.security.principal.UserPrincipal; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 12 | import org.springframework.security.core.AuthenticationException; 13 | import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; 14 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 15 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 16 | import org.springframework.security.oauth2.core.user.OAuth2User; 17 | import org.springframework.stereotype.Service; 18 | 19 | import java.util.Optional; 20 | 21 | @Service 22 | @RequiredArgsConstructor 23 | public class CustomOAuth2UserService extends DefaultOAuth2UserService { 24 | 25 | private final UserRepository userRepository; 26 | 27 | @Override 28 | public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { 29 | OAuth2User user = super.loadUser(userRequest); 30 | 31 | try { 32 | return this.process(userRequest, user); 33 | } catch (AuthenticationException ex) { 34 | throw ex; 35 | } catch (Exception ex) { 36 | ex.printStackTrace(); 37 | throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause()); 38 | } 39 | } 40 | 41 | private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) { 42 | ParsingUserContext userInfo = new KakaoParsingParsingUserContext(user.getAttributes()); 43 | Optional optionalSavedUser = userRepository.findUserByProviderId(userInfo.getId()); 44 | User savedUser = null; 45 | if (optionalSavedUser.isPresent()) { 46 | savedUser = optionalSavedUser.get(); 47 | } 48 | 49 | if (savedUser == null) { 50 | savedUser = createUser(userInfo); 51 | } 52 | 53 | return UserPrincipal.create(savedUser, user.getAttributes()); 54 | } 55 | 56 | private User createUser(ParsingUserContext userInfo) { 57 | User user = User.builder() 58 | .providerId(userInfo.getId()) 59 | .nickName(userInfo.getName()) 60 | .email(userInfo.getEmail()) 61 | .isPushAgree(Agreement.DISAGREE) 62 | .mannerTemperature((long)36.5) 63 | .imageUrl(userInfo.getImageUrl()) 64 | .role(RoleType.USER) 65 | .isEmailVerification(false) 66 | .isBlocked(false) 67 | .build(); 68 | return userRepository.saveAndFlush(user); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/repository/OAuth2AuthorizationRequestBasedOnCookieRepository.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.repository; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; 7 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 8 | 9 | import com.hjjang.backend.global.util.CookieUtil; 10 | import com.nimbusds.oauth2.sdk.util.StringUtils; 11 | 12 | public class OAuth2AuthorizationRequestBasedOnCookieRepository 13 | implements AuthorizationRequestRepository { 14 | 15 | public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; 16 | public static final String REDIRECT_URI_PARAM_COOKIE_NAME = "redirect_uri"; 17 | public static final String REFRESH_TOKEN = "refresh_token"; 18 | private static final int cookieExpireSeconds = 180; 19 | 20 | @Override 21 | public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { 22 | return CookieUtil.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) 23 | .map(cookie -> CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class)) 24 | .orElse(null); 25 | } 26 | 27 | @Override 28 | public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, 29 | HttpServletResponse response) { 30 | if (authorizationRequest == null) { 31 | CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 32 | CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 33 | CookieUtil.deleteCookie(request, response, REFRESH_TOKEN); 34 | return; 35 | } 36 | 37 | CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, 38 | CookieUtil.serialize(authorizationRequest), cookieExpireSeconds); 39 | String redirectUriAfterLogin = request.getParameter(REDIRECT_URI_PARAM_COOKIE_NAME); 40 | if (StringUtils.isNotBlank(redirectUriAfterLogin)) { 41 | CookieUtil.addCookie(response, REDIRECT_URI_PARAM_COOKIE_NAME, redirectUriAfterLogin, cookieExpireSeconds); 42 | } 43 | } 44 | 45 | @Override 46 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { 47 | return this.loadAuthorizationRequest(request); 48 | } 49 | 50 | @Override 51 | public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, 52 | HttpServletResponse response) { 53 | return this.loadAuthorizationRequest(request); 54 | } 55 | 56 | public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { 57 | CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); 58 | CookieUtil.deleteCookie(request, response, REDIRECT_URI_PARAM_COOKIE_NAME); 59 | CookieUtil.deleteCookie(request, response, REFRESH_TOKEN); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/user/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.user.entity; 2 | 3 | import static javax.persistence.EnumType.*; 4 | import static javax.persistence.GenerationType.*; 5 | import static lombok.AccessLevel.*; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.Enumerated; 10 | import javax.persistence.FetchType; 11 | import javax.persistence.GeneratedValue; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.ManyToOne; 15 | import javax.persistence.Table; 16 | 17 | import com.hjjang.backend.domain.university.entity.University; 18 | import com.hjjang.backend.global.domain.BaseTimeEntity; 19 | 20 | import lombok.AllArgsConstructor; 21 | import lombok.Builder; 22 | import lombok.Getter; 23 | import lombok.NoArgsConstructor; 24 | 25 | @Getter 26 | @AllArgsConstructor 27 | @Entity 28 | @NoArgsConstructor(access = PROTECTED) 29 | @Table(name = "user") 30 | public class User extends BaseTimeEntity { 31 | 32 | @Id 33 | @GeneratedValue(strategy = IDENTITY) 34 | @Column(name = "id", nullable = false) 35 | private Long id; 36 | 37 | @Column(name = "provider_id", nullable = false, length = 128) 38 | private String providerId; 39 | 40 | @Column(name = "nickname", nullable = false, length = 30) 41 | private String nickName; 42 | 43 | @Column(name = "email", nullable = true, length = 50) 44 | private String email; 45 | 46 | @Column(name = "manner_temperature", nullable = false) 47 | private Long mannerTemperature; 48 | 49 | @Column(name = "image_url", length = 50) 50 | private String imageUrl; 51 | 52 | @Enumerated(STRING) 53 | @Column(name = "is_push_agree", nullable = false, length = 10) 54 | private Agreement isPushAgree; 55 | 56 | @ManyToOne(fetch = FetchType.LAZY) 57 | @JoinColumn(name = "university_id", nullable = true) 58 | private University university; 59 | 60 | @Column(name = "role", length = 20) 61 | @Enumerated(STRING) 62 | private RoleType role; 63 | 64 | @Column(name = "is_email_verification") 65 | private Boolean isEmailVerification; 66 | 67 | @Column(name = "is_blocked") 68 | private Boolean isBlocked; 69 | 70 | @Builder 71 | public User(String providerId, String nickName, String email, Long mannerTemperature, 72 | String imageUrl, Agreement isPushAgree, University university, RoleType role, Boolean isEmailVerification, 73 | Boolean isBlocked) { 74 | this.providerId = providerId; 75 | this.nickName = nickName; 76 | this.email = email; 77 | this.mannerTemperature = mannerTemperature; 78 | this.imageUrl = imageUrl; 79 | this.isPushAgree = isPushAgree; 80 | this.university = university; 81 | this.role = role; 82 | this.isEmailVerification = isEmailVerification; 83 | this.isBlocked = isBlocked; 84 | } 85 | 86 | @Builder 87 | public void setEmailAndUniversity(String email, University university) { 88 | this.email = email; 89 | this.isEmailVerification = true; 90 | this.university = university; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/global/security/principal/UserPrincipal.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.global.security.principal; 2 | 3 | import com.hjjang.backend.domain.user.entity.RoleType; 4 | import com.hjjang.backend.domain.user.entity.User; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.Setter; 9 | import org.springframework.security.core.GrantedAuthority; 10 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.oauth2.core.oidc.OidcIdToken; 13 | import org.springframework.security.oauth2.core.oidc.OidcUserInfo; 14 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; 15 | import org.springframework.security.oauth2.core.user.OAuth2User; 16 | 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | import java.util.Map; 20 | 21 | @Getter 22 | @AllArgsConstructor 23 | @RequiredArgsConstructor 24 | public class UserPrincipal implements OAuth2User, UserDetails, OidcUser { 25 | private final String userId; 26 | private final RoleType roleType; 27 | private final Collection authorities; 28 | 29 | @Setter 30 | private Map attributes; 31 | 32 | @Override 33 | public Map getAttributes() { 34 | return attributes; 35 | } 36 | 37 | @Override 38 | public Collection getAuthorities() { 39 | return authorities; 40 | } 41 | 42 | @Override 43 | public String getPassword() { 44 | return null; 45 | } 46 | 47 | @Override 48 | public String getName() { 49 | return userId; 50 | } 51 | 52 | @Override 53 | public String getUsername() { 54 | return userId; 55 | } 56 | 57 | @Override 58 | public boolean isAccountNonExpired() { 59 | return true; 60 | } 61 | 62 | @Override 63 | public boolean isAccountNonLocked() { 64 | return true; 65 | } 66 | 67 | @Override 68 | public boolean isCredentialsNonExpired() { 69 | return true; 70 | } 71 | 72 | @Override 73 | public boolean isEnabled() { 74 | return true; 75 | } 76 | 77 | @Override 78 | public Map getClaims() { 79 | return null; 80 | } 81 | 82 | @Override 83 | public OidcUserInfo getUserInfo() { 84 | return null; 85 | } 86 | 87 | @Override 88 | public OidcIdToken getIdToken() { 89 | return null; 90 | } 91 | 92 | public static UserPrincipal create(User user) { 93 | return new UserPrincipal( 94 | user.getProviderId(), 95 | RoleType.USER, 96 | Collections.singletonList(new SimpleGrantedAuthority(RoleType.USER.getCode()) 97 | )); 98 | } 99 | 100 | public static UserPrincipal create(User user, Map attributes) { 101 | UserPrincipal userPrincipal = create(user); 102 | userPrincipal.setAttributes(attributes); 103 | 104 | return userPrincipal; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 🥕 당근 마켓 클론 코딩 - BAuction 2 | 3 | MAU 1800만의 중고 거래 플랫폼 API를 재현해보기 위해 시작한 사이드 프로젝트입니다. 4 | 5 | 차별점으로 기존의 동네 단위의 서비스를 대학교 단위의 서비스로 바꿔보았습니다. 6 | 7 | ## Initialization 8 | 9 | ### clone repository 10 | 11 | ```bash 12 | $ git clone git@github.com:h-jjang/bauction.git 13 | ``` 14 | 15 | ### Docker build up 16 | 17 | ```bash 18 | # /baution 19 | $ docker-compose up --build 20 | ``` 21 | 22 | ## Project Goal 23 | 24 | 1. 당근 마켓의 MVP라 생각되는 부분에 대해서 서비스를 직접 분석하여 유사하게 구현 25 | 2. 대용량 트래픽에도 장애없이 동작할 수 있는 코드, 가독성이 좋고 유지보수 쉬운 코드에 대해 고민 26 | 3. 아래 4가지 항목 구현 27 | 28 | ### Spring Boot 29 | 30 | - 객체지향적 설계 31 | - SOLID 32 | - 의존성 주입 (Dependency Injection) 33 | - Thread-Safe 34 | 35 | ### DB + JPA 36 | 37 | - N + 1 Issue (LAZY Loading, Fetch Join) 38 | - Transaction 39 | 40 | ### JWT + Security + OAuth 41 | 42 | - 소셜 로그인 43 | 44 | ### STOMP, SMTP, Async 45 | 46 | - 실시간 채팅 47 | - 메일 비동기 전송 48 | 49 | ## Wire Frame 50 | 51 | ![Wire Frame](https://user-images.githubusercontent.com/73998876/184500678-17a20d69-9772-4a5c-ab7e-cbebdbfdeb52.png) 52 | 53 | ## Architecture 54 | 55 | ![Architecture](https://user-images.githubusercontent.com/73998876/184500716-8cbe4d0a-0e3d-4127-b957-de366cf4196b.png) 56 | 57 | Backend 58 | 59 | - Java 11 60 | - Gradle 7.4.1 61 | - Spring Boot 2.6.4 62 | - Spring Security + OAuth 2.0 63 | - Spring Data JPA + MySQL 5.7 64 | - Log4j 2 65 | - JUnit 5, Mockito, Jacoco, H2 66 | - Swagger, Spring REST Docs 67 | 68 | Frontend 69 | 70 | - React.js 71 | 72 | ETC 73 | 74 | - Kakao Login 75 | - Gmail 76 | - AWS S3 77 | - Docker 78 | - NGINX 79 | 80 | ## Process 81 | 82 | 83 | ### 상품 거래 84 | 85 | [등록] 86 | 87 | 1. 카테고리 등록 88 | 2. 게시글 등록 + 이미지 업로드 89 | - 메일 알림 전송 90 | - 거래 상태 등록 (대기) 91 | 92 | 93 | [검색] 94 | 95 | 1. 전체 게시글 검색 96 | 2. 키워드 검색 97 | 3. 키워드 + 필터 검색 98 | 99 | 100 | [구매] 101 | 102 | 대화 → 예약 → 구매확정 103 | 104 | 1. 대화 - STOMP 방식을 통한 실시간 채팅 105 | 2. 예약 - 거래 상태 변경 (예약) 106 | - 메일 알림 전송 107 | 3. 구매 확정 - 거래 상태 변경 (승인) 108 | - 메일 알림 전송 109 | 110 | 111 | [삭제] 112 | 113 | 1. 게시글 삭제 114 | 2. 이미지 삭제 115 | 3. 거래 상태 삭제 116 | 117 | 118 | ### 회원 119 | 120 | [소셜 로그인] 121 | 122 | 1. 카카오 로그인 123 | - 유저 토큰 발급 124 | 125 | [대학교 인증] 126 | 127 | 1. 메일 전송 128 | 2. 인증 코드 입력 129 | 130 | ## Members 131 | 132 | | 이름 | 개발분야 | 담당 | 소개페이지 | 133 | | ---------- | ---------------------------------- | -------------------------------------------- | -------------------------------------------------- | 134 | | 김영준 | BE, Team Lead | 게시글, 거래, 이미지 업로드 API, DB Design | [개인 리포로 이동](https://github.com/orgs/h-jjang/people/0BVer) | 135 | | 김기현 | BE | Security, OAuth, Chatting API, DB Design | [개인 리포로 이동](https://github.com/orgs/h-jjang/people/kim1387) | 136 | | 조윤근 | BE, DevOps | 검색, 메일 인증 API, Deploy, DB Design | [개인 리포로 이동](https://github.com/orgs/h-jjang/people/Yunkeun) | 137 | | 박근우 | BE | 카페고리 API, DB Design | [개인 리포로 이동](https://github.com/Gnu-Kenny) | 138 | 139 | 140 | # Tech Blog 141 | 142 | [BAuction 프로젝트 회고](https://medium.com/@dr0joon/bauction-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-d4b8711409c8) 143 | 144 | [SMTP와 비동기](https://medium.com/@choyun0415/spring-smtp%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-b567125433e3) 145 | 146 | [채팅 API 구현에 WebSocket + STOMP 도입 이유](https://medium.com/@qkr0677/%EC%B1%84%ED%8C%85-api-%EA%B5%AC%ED%98%84%EC%97%90-websocket-stomp-%EB%8F%84%EC%9E%85-%EC%9D%B4%EC%9C%A0-d3ed17776441) 147 | -------------------------------------------------------------------------------- /backend/HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | 5 | For further reference, please consider the following sections: 6 | 7 | * [Official Gradle documentation](https://docs.gradle.org) 8 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.6.5/gradle-plugin/reference/html/) 9 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.6.5/gradle-plugin/reference/html/#build-image) 10 | * [Spring Security](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-security) 11 | * [OAuth2 Client](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-security-oauth2-client) 12 | * [Flyway Migration](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#howto-execute-flyway-database-migrations-on-startup) 13 | * [Spring Batch](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#howto-batch-applications) 14 | * [Validation](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-validation) 15 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-developing-web-applications) 16 | * [Spring for Apache Kafka](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-kafka) 17 | * [Prometheus](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#production-ready-metrics-export-prometheus) 18 | * [Spring REST Docs](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/) 19 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-jpa-and-spring-data) 20 | * [WebSocket](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-websockets) 21 | * [Java Mail Sender](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-email) 22 | * [Spring Configuration Processor](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#configuration-metadata-annotation-processor) 23 | * [Spring Data Redis (Access+Driver)](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#boot-features-redis) 24 | * [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#using-boot-devtools) 25 | * [Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/2.6.5/reference/htmlsingle/#production-ready) 26 | 27 | ### Guides 28 | 29 | The following guides illustrate how to use some features concretely: 30 | 31 | * [Securing a Web Application](https://spring.io/guides/gs/securing-web/) 32 | * [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) 33 | * [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) 34 | * [Creating a Batch Service](https://spring.io/guides/gs/batch-processing/) 35 | * [Validation](https://spring.io/guides/gs/validating-form-input/) 36 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 37 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 38 | * [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) 39 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 40 | * [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) 41 | * [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/) 42 | * [Messaging with Redis](https://spring.io/guides/gs/messaging-redis/) 43 | * [Building a RESTful Web Service with Spring Boot Actuator](https://spring.io/guides/gs/actuator-service/) 44 | 45 | ### Additional Links 46 | 47 | These additional references should also help you: 48 | 49 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 50 | 51 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/email/MailServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.email; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import com.hjjang.backend.domain.mail.dto.Mail; 7 | import com.hjjang.backend.domain.mail.dto.MailRequest; 8 | import com.hjjang.backend.domain.mail.exception.InvalidMailException; 9 | import com.hjjang.backend.domain.mail.service.MailService; 10 | import java.lang.reflect.Method; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.mail.javamail.JavaMailSenderImpl; 16 | 17 | class MailServiceTest { 18 | 19 | private final MailService mailService = new MailService(mailSender()); 20 | 21 | @DisplayName("코드 인증 테스트") 22 | @Test 23 | void authEmail() { 24 | //given 25 | String emailAddress = "32174294@dankook.ac.kr"; 26 | String code = "123456"; 27 | Mail email = new Mail(code, emailAddress, "dankook", false); 28 | //when 29 | MailRequest mailRequest = new MailRequest(emailAddress, code); 30 | 31 | //then 32 | assertThat(email.getCode()).isEqualTo(mailRequest.getCode()); 33 | } 34 | 35 | @DisplayName("이메일에서 대학교 파싱 테스트") 36 | @Test 37 | void parseUniversityFromEmail() throws Exception { 38 | //reflection 39 | Method parseUniversity = MailService.class.getDeclaredMethod("parseUniversity", String.class); 40 | //given 41 | String email = "32174294@dankook.ac.kr"; 42 | //when 43 | parseUniversity.setAccessible(true); 44 | String university = (String) parseUniversity.invoke(mailService, email); 45 | //then 46 | assertThat(university).isEqualTo("dankook"); 47 | } 48 | 49 | @DisplayName("gs이메일에서 대학교 파싱 테스트") 50 | @Test 51 | void parseGsUniversityFromEmail() throws Exception { 52 | //reflection 53 | Method parseUniversity = MailService.class.getDeclaredMethod("parseUniversity", String.class); 54 | //given 55 | String email = "dr0joon@gs.anyang.ac.kr"; 56 | //when 57 | parseUniversity.setAccessible(true); 58 | String university = (String) parseUniversity.invoke(mailService, email); 59 | //then 60 | assertThat(university).isEqualTo("anyang"); 61 | } 62 | 63 | @DisplayName("메일 주소 예외처리 테스트 (실패 케이스)") 64 | @Test 65 | void checkInvalidEmail() { 66 | //given 67 | List falseEmails = new ArrayList<>(); 68 | falseEmails.add("32174294@naver.com"); 69 | falseEmails.add("32174294@gmail.com"); 70 | falseEmails.add("32174294_dankook.ac.kr"); 71 | falseEmails.add("@dankook.ac.kr"); 72 | falseEmails.add("32174294@ac.kr"); 73 | falseEmails.add("32174294@.ac.kr"); 74 | falseEmails.add("32174294@gsd.dankook.ac.kr"); 75 | //when 76 | //then 77 | // falseEmails.forEach(MailServiceTest::isException); 78 | } 79 | 80 | @DisplayName("메일 주소 예외처리 테스트 (성공 케이스)") 81 | @Test 82 | void checkValidEmail() { 83 | //given 84 | List trueEmails = new ArrayList<>(); 85 | trueEmails.add("32174294@dankook.ac.kr"); 86 | trueEmails.add("32174294@anyang.ac.kr"); 87 | trueEmails.add("32174294@kpu.ac.kr"); 88 | trueEmails.add("dr0joon@gs.anyang.ac.kr"); 89 | //when 90 | //then 91 | // trueEmails.forEach(InvalidMailException::checkPossibleMail); 92 | } 93 | 94 | private JavaMailSenderImpl mailSender() { 95 | JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); 96 | javaMailSender.setProtocol("SMTP"); 97 | // javaMailSender.setHost("127.0.0.1"); 98 | // javaMailSender.setPort(587); 99 | return javaMailSender; 100 | } 101 | 102 | // private static void isException(String email) { 103 | // //then, when 104 | // assertThrows(RuntimeException.class, () -> 105 | // 106 | // InvalidMailException.checkPossibleMail(email)); 107 | // } 108 | } 109 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/controller/PostController.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.controller; 2 | 3 | import static org.springframework.http.HttpStatus.*; 4 | import static org.springframework.http.ResponseEntity.*; 5 | 6 | import com.hjjang.backend.domain.mail.service.NoticeMailService; 7 | import java.util.stream.Collectors; 8 | 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.validation.annotation.Validated; 11 | import org.springframework.web.bind.annotation.DeleteMapping; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.PutMapping; 16 | import org.springframework.web.bind.annotation.RequestBody; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import com.hjjang.backend.domain.post.domain.entity.Post; 21 | import com.hjjang.backend.domain.post.dto.PostMapper; 22 | import com.hjjang.backend.domain.post.dto.PostRequestDto; 23 | import com.hjjang.backend.domain.post.dto.PostResponseDto; 24 | import com.hjjang.backend.domain.post.service.PostServiceImpl; 25 | import com.hjjang.backend.domain.user.entity.User; 26 | import com.hjjang.backend.global.dto.ApiResponse; 27 | import com.hjjang.backend.global.response.code.SuccessCode; 28 | import com.hjjang.backend.global.response.response.SuccessResponse; 29 | import com.hjjang.backend.global.util.UserUtil; 30 | 31 | import lombok.RequiredArgsConstructor; 32 | 33 | @RequiredArgsConstructor 34 | @RequestMapping("/api/posts") 35 | @RestController 36 | public class PostController { 37 | 38 | private final PostServiceImpl postService; 39 | private final NoticeMailService mailService; 40 | private final PostMapper postMapper; 41 | private final UserUtil userUtil; 42 | 43 | @PostMapping 44 | public ResponseEntity createItem(@Validated @RequestBody PostRequestDto postRequestDto) { 45 | User user = userUtil.getLoginUserByToken(); 46 | Post post = postService.save(postMapper.toEntity(postRequestDto, user)); 47 | PostResponseDto postResponseDto = postMapper.fromEntity(post); 48 | mailService.sendNotice(user, post); 49 | return ResponseEntity.ok(SuccessResponse.of(SuccessCode.POST_CREATE_SUCCESS, postResponseDto)); 50 | } 51 | 52 | @GetMapping 53 | public ResponseEntity findAllItem() { 54 | return ok( 55 | ApiResponse.success("findAllItem", postService 56 | .findAll() 57 | .stream() 58 | .map(postMapper::fromEntity) 59 | .collect(Collectors.toList()) 60 | ) 61 | ); 62 | } 63 | 64 | @GetMapping("/{id}") 65 | public ResponseEntity findOneItem(@PathVariable Long id) { 66 | return ok(ApiResponse.success("findOneItem", postMapper.fromEntity(postService.findOneById(id)))); 67 | } 68 | 69 | @PutMapping("/{id}") 70 | public ResponseEntity putOneItem(@PathVariable Long id, @Validated @RequestBody PostRequestDto postRequestDto) { 71 | return status(CREATED) 72 | .body(ApiResponse.success( 73 | "updatePutOneItem", 74 | postMapper.fromEntity( 75 | postService.updateOneById( 76 | id, postRequestDto, userUtil.getLoginUserByToken() 77 | ) 78 | ) 79 | ) 80 | ); 81 | } 82 | 83 | @DeleteMapping("/{id}") 84 | public ResponseEntity deleteOneItem(@PathVariable Long id) { 85 | postService.deleteOneById(id); 86 | return ok(ApiResponse.success("deleteOneItem", ACCEPTED)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/post/dto/PostMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.dto; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import com.hjjang.backend.domain.user.entity.Agreement; 5 | import com.hjjang.backend.domain.user.entity.RoleType; 6 | import com.hjjang.backend.domain.user.entity.User; 7 | import org.assertj.core.api.Assertions; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.springframework.test.context.junit.jupiter.SpringExtension; 14 | 15 | import java.time.LocalDateTime; 16 | 17 | import static com.hjjang.backend.domain.post.domain.entity.PostDefaultValue.*; 18 | import static org.junit.jupiter.api.Assertions.*; 19 | 20 | @ExtendWith(SpringExtension.class) 21 | class PostMapperTest { 22 | 23 | @InjectMocks 24 | private PostMapper postMapper; 25 | 26 | private Post expectPost; 27 | 28 | private User givenUser; 29 | 30 | private final String givenTitle = "test title"; 31 | private final String givenContent = "test content"; 32 | private final int givenItemPrice = 10000; 33 | 34 | @BeforeEach 35 | void setUp() { 36 | givenUser = User.builder() 37 | .providerId("test Id") 38 | .email("user@email.ac.kr") 39 | .imageUrl("test path") 40 | .isPushAgree(Agreement.AGREE) 41 | .mannerTemperature(36L) 42 | .nickName("test NN") 43 | .role(RoleType.USER) 44 | .univId(1L) 45 | .build(); 46 | 47 | expectPost = Post.builder() 48 | .user(givenUser) 49 | .title(givenTitle) 50 | .content(givenContent) 51 | .itemPrice(givenItemPrice) 52 | .build(); 53 | 54 | } 55 | 56 | @Test 57 | @DisplayName("PostRequestDto -> Post 변환 테스트") 58 | void toEntityTest() { 59 | // given 60 | PostRequestDto givenPostRequestDto = PostRequestDto.builder() 61 | .title(givenTitle) 62 | .content(givenContent) 63 | .price(givenItemPrice) 64 | .build(); 65 | // when 66 | Post actualPost = postMapper.toEntity(givenPostRequestDto, givenUser); 67 | // then 68 | assertAll( 69 | () -> assertEquals(expectPost.getUser(), actualPost.getUser()), 70 | () -> assertEquals(expectPost.getTitle(), actualPost.getTitle()), 71 | () -> assertEquals(expectPost.getContent(), actualPost.getContent()), 72 | () -> assertEquals(expectPost.getItemPrice(), actualPost.getItemPrice()) 73 | ); 74 | } 75 | 76 | @Test 77 | @DisplayName("Post -> PostResponseDto 변환 테스트") 78 | void fromEntityTest() { 79 | // given 80 | Post givenPost = new Post(1L, givenUser, givenTitle, givenContent, givenItemPrice, 81 | DEFAULT_VIEWS, DEFAULT_INTEREST_NUMBER, DEFAULT_CHAT_NUMBER, 82 | DEFAULT_IS_SALE_COMPLETION, DEFAULT_REMOVED, LocalDateTime.now()); 83 | 84 | PostResponseDto expectPostResponseDto = PostResponseDto.builder() 85 | .id(givenPost.getId()) 86 | .user_id(givenPost.getUser().getId()) 87 | .title(givenPost.getTitle()) 88 | .content(givenPost.getContent()) 89 | .item_price(givenPost.getItemPrice()) 90 | .views(givenPost.getViews()) 91 | .interest_number(givenPost.getInterestNumber()) 92 | .chat_number(givenPost.getChatNumber()) 93 | .is_sale_completion(givenPost.getIsSaleCompletion().getState()) 94 | .removed(givenPost.isRemoved()) 95 | .time(givenPost.getTime().toString()) 96 | .build(); 97 | // when 98 | PostResponseDto actualPostResponseDto = postMapper.fromEntity(givenPost); 99 | // then 100 | Assertions.assertThat(actualPostResponseDto).isEqualToComparingFieldByField(expectPostResponseDto); 101 | } 102 | } -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/post/domain/entity/Post.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.entity; 2 | 3 | import static com.hjjang.backend.domain.post.domain.entity.PostDefaultValue.*; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.EnumType; 10 | import javax.persistence.Enumerated; 11 | import javax.persistence.FetchType; 12 | import javax.persistence.GeneratedValue; 13 | import javax.persistence.GenerationType; 14 | import javax.persistence.Id; 15 | import javax.persistence.JoinColumn; 16 | import javax.persistence.ManyToOne; 17 | import javax.persistence.Table; 18 | 19 | import lombok.Setter; 20 | import org.hibernate.annotations.CreationTimestamp; 21 | 22 | import com.hjjang.backend.domain.post.dto.PostRequestDto; 23 | import com.hjjang.backend.domain.university.entity.University; 24 | import com.hjjang.backend.domain.user.entity.User; 25 | 26 | import lombok.AccessLevel; 27 | import lombok.AllArgsConstructor; 28 | import lombok.Builder; 29 | import lombok.Getter; 30 | import lombok.NoArgsConstructor; 31 | 32 | @Getter 33 | //@Builder 34 | @AllArgsConstructor 35 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 36 | //@DynamicInsert 37 | @Table(name = "post") 38 | @Entity 39 | public class Post { 40 | @Id 41 | @GeneratedValue(strategy = GenerationType.IDENTITY) 42 | private Long id; 43 | 44 | @ManyToOne(fetch = FetchType.LAZY) 45 | @JoinColumn(name = "user_id") 46 | private User user; 47 | 48 | @ManyToOne(fetch = FetchType.LAZY) 49 | @JoinColumn(name = "university_id", nullable = false) 50 | private University university = DEFAULT_UNIVERSITY; 51 | 52 | @Column(name = "title", nullable = false) 53 | private String title; 54 | 55 | @Column(name = "content", nullable = false) 56 | private String content; 57 | 58 | @Column(name = "item_price", nullable = false) 59 | private int itemPrice; 60 | 61 | // @ColumnDefault("0") 62 | @Column(name = "views", nullable = false) 63 | private int views = DEFAULT_VIEWS; 64 | 65 | // @ColumnDefault("0") 66 | @Column(name = "interest_number", nullable = false) 67 | private int interestNumber = DEFAULT_INTEREST_NUMBER; 68 | 69 | // @ColumnDefault("0") 70 | @Column(name = "chat_number", nullable = false) 71 | private int chatNumber = DEFAULT_CHAT_NUMBER; 72 | 73 | @Enumerated(EnumType.STRING) @Setter 74 | @Column(name = "is_sale_completion", nullable = false) 75 | private PostState isSaleCompletion = DEFAULT_IS_SALE_COMPLETION; 76 | 77 | // @ColumnDefault("false") 78 | @Column(name = "removed", nullable = false) 79 | private boolean removed = DEFAULT_REMOVED; 80 | 81 | @CreationTimestamp 82 | private LocalDateTime time; 83 | 84 | @Builder 85 | public Post(User user, String title, String content, int itemPrice) { 86 | this.user = user; 87 | this.university = user.getUniversity(); 88 | this.title = title; 89 | this.content = content; 90 | this.itemPrice = itemPrice; 91 | } 92 | 93 | public void removePost() { 94 | this.removed = true; 95 | } 96 | 97 | public Post update(PostRequestDto postRequestDto) { 98 | this.title = postRequestDto.getTitle(); 99 | this.content = postRequestDto.getContent(); 100 | this.itemPrice = postRequestDto.getPrice(); 101 | return this; 102 | } 103 | 104 | public String mailText(String content) { 105 | return content + "\n" 106 | + "거래 제목: " + title + "\n" 107 | + "상태: " + isSaleCompletion.getState(); 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return "Post{" + 113 | "id=" + id + 114 | ", user=" + user + 115 | ", university=" + university + 116 | ", title='" + title + '\'' + 117 | ", content='" + content + '\'' + 118 | ", itemPrice=" + itemPrice + 119 | ", views=" + views + 120 | ", interestNumber=" + interestNumber + 121 | ", chatNumber=" + chatNumber + 122 | ", isSaleCompletion=" + isSaleCompletion + 123 | ", removed=" + removed + 124 | ", time=" + time + 125 | '}'; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/mail/service/MailService.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.mail.service; 2 | 3 | import static com.hjjang.backend.global.response.code.ErrorCode.*; 4 | 5 | import java.util.List; 6 | import java.util.regex.Pattern; 7 | 8 | import org.springframework.mail.SimpleMailMessage; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.stereotype.Service; 11 | 12 | import com.hjjang.backend.domain.mail.domain.MailMessage; 13 | import com.hjjang.backend.domain.mail.domain.MailRegex; 14 | import com.hjjang.backend.domain.mail.dto.Mail; 15 | import com.hjjang.backend.domain.mail.dto.MailRequest; 16 | import com.hjjang.backend.domain.mail.dto.MailResponse; 17 | import com.hjjang.backend.domain.mail.exception.InvalidMailException; 18 | import com.hjjang.backend.domain.mail.exception.UnauthorizedException; 19 | import com.hjjang.backend.domain.university.entity.University; 20 | import com.hjjang.backend.domain.university.service.UniversityService; 21 | import com.hjjang.backend.domain.user.entity.User; 22 | import com.hjjang.backend.domain.user.repository.UserRepository; 23 | import com.hjjang.backend.global.util.UserUtil; 24 | 25 | import lombok.RequiredArgsConstructor; 26 | 27 | @Service 28 | @RequiredArgsConstructor 29 | public class MailService { 30 | 31 | private final UniversityService universityService; 32 | private final UserRepository userRepository; 33 | private final JavaMailSender javaMailSender; 34 | private final Mail mail = new Mail(); 35 | private final UserUtil userUtil; 36 | 37 | public MailResponse sendMail(String mailAddress) { 38 | checkPossibleMail(mailAddress); 39 | SimpleMailMessage message = new SimpleMailMessage(); 40 | setMessage(message, mailAddress); 41 | javaMailSender.send(message); 42 | return new MailResponse(mail); 43 | } 44 | 45 | public MailResponse checkCode(MailRequest mailRequest) { 46 | checkRequest(mailRequest, mail); 47 | mail.setIsAuth(true); 48 | University university = universityService.findUniversityByName(mail.getUniversity()); 49 | User user = userUtil.getLoginUserByToken(); 50 | user.setEmailAndUniversity(mail.getAddress(), university); 51 | userRepository.save(user); 52 | return new MailResponse(mail); 53 | } 54 | 55 | private String parseUniversity(String mailAddress) { 56 | MailRegex mailParse = MailRegex.MAIL_PARSE; 57 | String regex = mailParse.getRegex(); 58 | List splitMail = List.of(mailAddress.split(regex)); 59 | int universityIndex = splitMail.lastIndexOf("ac") - 1; 60 | return splitMail.get(universityIndex); 61 | } 62 | 63 | private void setMessage(SimpleMailMessage message, String mailAddress) { 64 | message.setTo(mailAddress); 65 | MailMessage title = MailMessage.AUTH_TITLE; 66 | message.setSubject(title.getContent()); 67 | saveMailInfo(mailAddress); 68 | MailMessage mailMessage = MailMessage.AUTH_MESSAGE; 69 | message.setText(mailMessage.getContent() + mail.getCode()); 70 | } 71 | 72 | private void saveMailInfo(String mailAddress) { 73 | String code = mail.createRandomCode(); 74 | mail.setCode(code); 75 | mail.setAddress(mailAddress); 76 | String university = parseUniversity(mailAddress); 77 | mail.setUniversity(university); 78 | mail.setIsAuth(false); 79 | } 80 | 81 | private void checkPossibleMail(String mailAddress) { 82 | if (!isUniversityMail(mailAddress)) { 83 | throw new InvalidMailException(); 84 | } 85 | } 86 | 87 | private void checkRequest(MailRequest mailRequest, Mail mail) { 88 | checkValidCode(mailRequest, mail.getCode()); 89 | checkValidRequestMail(mailRequest, mail.getAddress()); 90 | } 91 | 92 | private void checkValidCode(MailRequest mailRequest, String code) { 93 | if (!mailRequest.getCode().equals(code)) { 94 | throw new UnauthorizedException(INVALID_CODE); 95 | } 96 | } 97 | 98 | private void checkValidRequestMail(MailRequest mailRequest, String mailAddress) { 99 | if (!mailRequest.getMail().equals(mailAddress)) { 100 | throw new InvalidMailException(); 101 | } 102 | } 103 | 104 | private boolean isUniversityMail(String mailAddress) { 105 | MailRegex university = MailRegex.UNIVERSITY; 106 | MailRegex gsUniversity = MailRegex.GS_UNIVERSITY; 107 | if (Pattern.matches(university.getRegex(), mailAddress)) { 108 | return true; 109 | } 110 | return Pattern.matches(gsUniversity.getRegex(), mailAddress); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /backend/src/main/java/com/hjjang/backend/domain/search/service/SearchServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.search.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.stream.Collectors; 8 | 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.PageImpl; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.stereotype.Service; 13 | 14 | import com.hjjang.backend.domain.post.domain.entity.Post; 15 | import com.hjjang.backend.domain.search.repository.SearchRepository; 16 | import com.hjjang.backend.domain.university.entity.University; 17 | import com.hjjang.backend.domain.user.entity.User; 18 | 19 | import lombok.RequiredArgsConstructor; 20 | 21 | @Service 22 | @RequiredArgsConstructor 23 | public class SearchServiceImpl implements SearchService { 24 | 25 | private final SearchRepository searchRepository; 26 | 27 | public Page findAll(String filter, Pageable pageable, User user) { 28 | // TODO::이메일 인증된 유저로 대체 29 | if (true) { 30 | return searchByFilterInUniversity(filter, pageable, user.getUniversity()); 31 | } 32 | return searchByFilter(filter, pageable); 33 | } 34 | 35 | public Page findByKeyword(String keyword, String filter, Pageable pageable, User user) { 36 | // TODO::이메일 인증된 유저로 대체 37 | if (true) { 38 | return searchByFilterAndKeywordInUniversity(keyword, filter, user.getUniversity(), pageable); 39 | } 40 | return searchByFilterAndKeyword(keyword, filter, pageable); 41 | } 42 | 43 | private Page searchByFilterAndKeyword(String keyword, String filter, Pageable pageable) { 44 | List posts = new ArrayList<>(); 45 | List keywords = parseKeyword(keyword); 46 | keywords.forEach(word -> addSearchedPosts(pageable, posts, word)); 47 | if (filter == null) { 48 | return getPostPage(pageable, sortAndDistinctPosts(posts)); 49 | } 50 | posts.retainAll(searchRepository.findByIsSaleCompletion(filter, pageable)); 51 | return getPostPage(pageable, sortAndDistinctPosts(posts)); 52 | } 53 | 54 | private void addSearchedPosts(Pageable pageable, List posts, String word) { 55 | posts.addAll(searchRepository.findByTitleContaining(word, pageable)); 56 | } 57 | 58 | private Page searchByFilterAndKeywordInUniversity(String keyword, String filter, University university, Pageable pageable) { 59 | List keywords = parseKeyword(keyword); 60 | List posts = new ArrayList<>(); 61 | keywords.forEach(word -> 62 | getPostsInUniversity( 63 | university, 64 | searchRepository.findByTitleContaining(word, pageable), 65 | posts) 66 | ); 67 | if (filter == null) { 68 | return getPostPage(pageable, sortAndDistinctPosts(posts)); 69 | } 70 | posts.retainAll(searchRepository.findByIsSaleCompletionAndUniversity(filter, university, pageable)); 71 | return getPostPage(pageable, sortAndDistinctPosts(posts)); 72 | } 73 | 74 | private void getPostsInUniversity(University university, List searchedPosts, List searchedPostsInUniv) { 75 | searchedPosts.forEach(post -> { 76 | if (Objects.equals(post.getUniversity().getId(), university.getId())) { 77 | searchedPostsInUniv.add(post); 78 | } 79 | }); 80 | } 81 | 82 | private Page searchByFilterInUniversity(String filter, Pageable pageable, University university) { 83 | if (filter == null) { 84 | return searchRepository.findByUniversity(university, pageable); 85 | } 86 | return searchRepository.findAllByIsSaleCompletionAndUniversity(filter, university, pageable); 87 | } 88 | 89 | private Page searchByFilter(String filter, Pageable pageable) { 90 | if (filter == null) { 91 | searchRepository.findAll(pageable); 92 | } 93 | return searchRepository.findAllByIsSaleCompletion(filter, pageable); 94 | } 95 | 96 | private PageImpl getPostPage(Pageable pageable, List posts) { 97 | final int start = (int) pageable.getOffset(); 98 | final int end = Math.min((start + pageable.getPageSize()), posts.size()); 99 | return new PageImpl<>(posts.subList(start, end), pageable, posts.size()); 100 | } 101 | 102 | private List sortAndDistinctPosts(List posts) { 103 | List distinctPosts = posts.stream().distinct().collect(Collectors.toList()); 104 | return distinctPosts 105 | .stream() 106 | .sorted(Comparator.comparing(Post::getTime).reversed()) 107 | .collect(Collectors.toList()); 108 | } 109 | 110 | private List parseKeyword(String keyword) { 111 | return List.of(keyword.split(" ")); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /backend/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.6.4' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id "org.asciidoctor.jvm.convert" version "3.3.2" 5 | id 'java' 6 | id 'application' 7 | id 'jacoco' 8 | } 9 | 10 | group = 'com.hjjang' 11 | version = '0.0.1-SNAPSHOT' 12 | sourceCompatibility = '11' 13 | 14 | configurations { 15 | asciidoctorExtensions // dependencies 에서 적용한 것 추가 16 | compileOnly { 17 | extendsFrom annotationProcessor 18 | } 19 | 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | ext { 27 | set('snippetsDir', file("build/generated-snippets")) 28 | } 29 | 30 | dependencies { 31 | 32 | // 모니터링 관련 의존성 33 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 34 | runtimeOnly 'io.micrometer:micrometer-registry-prometheus' 35 | 36 | implementation 'org.springframework.boot:spring-boot-starter-batch' 37 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 38 | implementation 'org.springframework.boot:spring-boot-starter-data-redis' 39 | implementation 'org.springframework.boot:spring-boot-starter-mail' 40 | 41 | implementation 'org.springframework.boot:spring-boot-starter-validation' 42 | implementation 'org.springframework.boot:spring-boot-starter-web' 43 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 44 | implementation 'org.flywaydb:flyway-core' 45 | implementation 'org.springframework.kafka:spring-kafka' 46 | implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' 47 | 48 | // 롬복 관련 의존성 49 | compileOnly 'org.projectlombok:lombok' 50 | annotationProcessor 'org.projectlombok:lombok' 51 | 52 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 53 | 54 | runtimeOnly 'mysql:mysql-connector-java' 55 | annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 56 | testAnnotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 57 | //swagger-ui 58 | implementation 'org.springdoc:springdoc-openapi-ui:1.6.8' 59 | //jwt 관련 의존성 60 | implementation 'io.jsonwebtoken:jjwt-api:0.11.5' 61 | runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' 62 | runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' 63 | 64 | // oauth 관련 의존성 65 | implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' 66 | implementation 'org.springframework.boot:spring-boot-starter-security' 67 | 68 | implementation 'com.google.guava:guava:31.1-jre' 69 | 70 | // test를 위한 의존성 71 | testImplementation 'org.springframework.batch:spring-batch-test' 72 | testImplementation 'org.springframework.kafka:spring-kafka-test' 73 | testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' 74 | asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' 75 | testImplementation 'org.springframework.security:spring-security-test' 76 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 77 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' //junit 4 하위 호환을 위한 라이브러리 제거 78 | } 79 | testCompileOnly 'org.projectlombok:lombok' 80 | testAnnotationProcessor 'org.projectlombok:lombok' 81 | runtimeOnly 'com.h2database:h2' 82 | 83 | // 로깅 관련 의존성 84 | implementation "org.springframework.boot:spring-boot-starter-log4j2" 85 | modules { 86 | module("org.springframework.boot:spring-boot-starter-logging") { 87 | replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback") 88 | } 89 | } 90 | implementation 'org.apache.logging.log4j:log4j-web' 91 | 92 | } 93 | 94 | tasks.named('compileJava') { 95 | inputs.files(tasks.named('processResources')) 96 | } 97 | 98 | bootRun { 99 | mainClassName = 'com.hjjang.backend.BackendApplication' 100 | 101 | } 102 | 103 | // 작성된 문서를 서버에서 접근할 수 있도록 이동 104 | bootJar { 105 | mainClassName = 'com.hjjang.backend.BackendApplication' 106 | 107 | dependsOn asciidoctor 108 | from ("${asciidoctor.outputDir}/html5") { 109 | into 'static/docs' 110 | } 111 | } 112 | tasks.named('test') { 113 | outputs.dir snippetsDir 114 | useJUnitPlatform() 115 | } 116 | 117 | tasks.named('asciidoctor') { 118 | inputs.dir snippetsDir 119 | dependsOn test 120 | } 121 | 122 | jacocoTestReport { 123 | reports { 124 | html.enabled true 125 | xml.enabled false 126 | csv.enabled true 127 | } 128 | finalizedBy 'jacocoTestCoverageVerification' 129 | } 130 | 131 | jacocoTestCoverageVerification { 132 | violationRules { 133 | rule { 134 | enabled = true 135 | element = 'CLASS' 136 | 137 | limit { 138 | counter = 'METHOD' 139 | value = 'COVEREDRATIO' 140 | minimum = 0.00 141 | } 142 | 143 | limit { 144 | counter = 'INSTRUCTION' 145 | value = 'COVEREDRATIO' 146 | minimum = 0.00 147 | } 148 | excludes = ["*Dto*", "*.dto.*", "*.SvProjectApplication*", "*.config.*", "*.exceptions.*", "*.utils.*"] 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /backend/src/test/java/com/hjjang/backend/domain/post/domain/repository/PostRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.hjjang.backend.domain.post.domain.repository; 2 | 3 | import com.hjjang.backend.domain.post.domain.entity.Post; 4 | import com.hjjang.backend.domain.user.entity.Agreement; 5 | import com.hjjang.backend.domain.user.entity.RoleType; 6 | import com.hjjang.backend.domain.user.entity.User; 7 | import com.hjjang.backend.domain.user.repository.UserRepository; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 13 | import org.springframework.dao.DataIntegrityViolationException; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static com.hjjang.backend.domain.post.domain.entity.PostDefaultValue.*; 19 | import static org.junit.jupiter.api.Assertions.*; 20 | 21 | @DataJpaTest 22 | @DisplayName("Post Repository 테스트") 23 | class PostRepositoryTest { 24 | 25 | @Autowired 26 | private PostRepository postRepository; 27 | 28 | @Autowired 29 | private UserRepository userRepository; 30 | 31 | private Post expectPost1; 32 | 33 | private Post expectPost2; 34 | 35 | private Post givenPost1; 36 | private Post givenPost2; 37 | 38 | private final String givenTitle = "test title"; 39 | private final String givenContent = "test content"; 40 | private final int givenItemPrice = 10000; 41 | 42 | @BeforeEach 43 | void setUp() { 44 | 45 | User givenUser = User.builder() 46 | .providerId("test Id") 47 | .email("user@email.ac.kr") 48 | .imageUrl("test path") 49 | .isPushAgree(Agreement.AGREE) 50 | .mannerTemperature(36L) 51 | .nickName("test NN") 52 | .role(RoleType.USER) 53 | .univId(1L) 54 | .build(); 55 | 56 | User actualUser = userRepository.save(givenUser); 57 | 58 | givenPost1 = Post.builder() 59 | .user(actualUser) 60 | .title(givenTitle) 61 | .content(givenContent) 62 | .itemPrice(givenItemPrice) 63 | .build(); 64 | 65 | givenPost2 = Post.builder() 66 | .user(actualUser) 67 | .title(givenTitle) 68 | .content(givenContent) 69 | .itemPrice(givenItemPrice) 70 | .build(); 71 | 72 | expectPost1 = postRepository.save(givenPost1); 73 | expectPost2 = postRepository.save(givenPost2); 74 | } 75 | 76 | @Test 77 | @DisplayName("같은 정보의 Post가 저장되는지 테스트") 78 | void saveSameTest() { 79 | // given 80 | 81 | // when 82 | Post actualPost = postRepository.save(givenPost2); 83 | // then 84 | assertAll( 85 | () -> assertEquals(givenPost2.getUser(), actualPost.getUser()), 86 | () -> assertEquals(givenPost2.getTitle(), actualPost.getTitle()), 87 | () -> assertEquals(givenPost2.getContent(), actualPost.getContent()) 88 | ); 89 | } 90 | 91 | @Test 92 | @DisplayName("빈 Post를 저장시킬 때 검증 절차에의해 에러가 발생하는지 테스트") 93 | void saveEmptyPostTest() { 94 | // given 95 | Post emptyPost = Post.builder().build(); 96 | // when 97 | 98 | // then 99 | assertThrowsExactly(DataIntegrityViolationException.class, () -> postRepository.save(emptyPost)); 100 | } 101 | 102 | @Test 103 | @DisplayName("Post의 기본 값이 잘 설정되는지 테스트") 104 | void saveDefaultValueTest() { 105 | // given 106 | 107 | // when 108 | Post actualPost = postRepository.save(givenPost1); 109 | // then 110 | assertAll( 111 | () -> assertEquals(DEFAULT_VIEWS, actualPost.getViews()), 112 | () -> assertEquals(DEFAULT_INTEREST_NUMBER, actualPost.getInterestNumber()), 113 | () -> assertEquals(DEFAULT_CHAT_NUMBER, actualPost.getChatNumber()), 114 | () -> assertEquals(DEFAULT_IS_SALE_COMPLETION, actualPost.getIsSaleCompletion()), 115 | () -> assertEquals(DEFAULT_REMOVED, actualPost.isRemoved()) 116 | ); 117 | } 118 | 119 | @Test 120 | @DisplayName("") 121 | void findAllTest() { 122 | // given 123 | List expectPostList = List.of(expectPost1, expectPost2); 124 | // when 125 | List actualPostList = postRepository.findAll(); 126 | // then 127 | assertIterableEquals(expectPostList, actualPostList); 128 | } 129 | 130 | @Test 131 | @DisplayName("존재하는 ID로 죄회할 때 제대로 가져오는지 테스트") 132 | void findByActualIdTest() { 133 | // given 134 | 135 | // when 136 | Post actualPost = postRepository.getById(expectPost1.getId()); 137 | // then 138 | assertEquals(expectPost1, actualPost); 139 | } 140 | 141 | @Test 142 | @DisplayName("존재하지 않는 ID로 조회할 때 Optional.empty()로 처리 되는지 테스트") 143 | void findByNotRealIdTest() { 144 | // given 145 | long givenId = 9999999L; 146 | // when 147 | // then 148 | assertEquals(Optional.empty(), postRepository.findById(givenId)); 149 | } 150 | } --------------------------------------------------------------------------------