├── 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 |
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 extends GrantedAuthority> 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 extends GrantedAuthority> 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 | 
52 |
53 | ## Architecture
54 |
55 | 
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 | }
--------------------------------------------------------------------------------