├── src └── frontend │ ├── src │ ├── css │ │ └── app.css │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── chat │ │ │ ├── ChatRoomDrawer.vue │ │ │ ├── ChatRoomTitleBox.vue │ │ │ ├── ChatItem.vue │ │ │ ├── ChatList.vue │ │ │ ├── ChatInput.vue │ │ │ ├── ChatInfo.vue │ │ │ └── ChatRoomHistoryBox.vue │ │ ├── question │ │ │ ├── FormTitle.vue │ │ │ ├── MarkdownContent.vue │ │ │ ├── FormButton.vue │ │ │ ├── QuestionFilters.vue │ │ │ ├── AnswerList.vue │ │ │ └── QuestionControl.vue │ │ ├── SnackBar.vue │ │ ├── notice │ │ │ ├── NoticeSearch.vue │ │ │ └── NoticeType.vue │ │ ├── FooterBar.vue │ │ └── favorite │ │ │ └── FavoriteType.vue │ ├── plugins │ │ └── vuetify.js │ ├── views │ │ ├── notice │ │ │ ├── NoticeCreateView.vue │ │ │ ├── NoticeEditView.vue │ │ │ └── NoticeDetailView.vue │ │ ├── MainPage.vue │ │ ├── user │ │ │ └── MyPageView.vue │ │ ├── hashtags │ │ │ └── HashtagsView.vue │ │ ├── admin │ │ │ └── AdminMainView.vue │ │ ├── question │ │ │ ├── QuestionEditView.vue │ │ │ └── QuestionCreateView.vue │ │ ├── favorite │ │ │ ├── NoticeFavoriteView.vue │ │ │ └── QuestionFavoriteView.vue │ │ └── LoginPage.vue │ ├── utils │ │ ├── editor.js │ │ ├── noticeUtil.js │ │ └── validator.js │ ├── store │ │ ├── modules │ │ │ ├── hashtags.js │ │ │ ├── snackBar.js │ │ │ └── loginUser.js │ │ └── index.js │ ├── main.js │ └── api │ │ └── index.js │ ├── babel.config.js │ ├── public │ ├── favicon.png │ └── index.html │ ├── .gitignore │ ├── README.md │ └── vue.config.js ├── devbie-app-api ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.properties │ │ │ ├── application.yml │ │ │ └── log4j2.yml │ │ └── java │ │ │ └── underdogs │ │ │ └── devbie │ │ │ ├── config │ │ │ ├── AuditingConfig.java │ │ │ ├── AopConfiguration.java │ │ │ ├── profile │ │ │ │ ├── ProfileLocal.java │ │ │ │ ├── ProfileDevelop.java │ │ │ │ └── ProfileProduction.java │ │ │ ├── ETagHeaderFilter.java │ │ │ ├── QuerydslConfiguration.java │ │ │ ├── AuthConfig.java │ │ │ └── ChatConfig.java │ │ │ ├── advice │ │ │ └── dto │ │ │ │ └── ErrorResponse.java │ │ │ ├── DevbieApplication.java │ │ │ ├── favorite │ │ │ ├── NoticeFavoriteController.java │ │ │ └── QuestionFavoriteController.java │ │ │ ├── recommendation │ │ │ ├── AnswerRecommendationController.java │ │ │ └── QuestionRecommendationController.java │ │ │ ├── auth │ │ │ ├── AuthController.java │ │ │ └── interceptor │ │ │ │ ├── utils │ │ │ │ ├── InterceptorValidator.java │ │ │ │ └── AuthorizationExtractor.java │ │ │ │ └── BearerAuthInterceptor.java │ │ │ └── aop │ │ │ └── ServiceLoggingAspect.java │ └── test │ │ ├── resources │ │ ├── notice_delete.sql │ │ ├── application.properties │ │ ├── devbie.png │ │ └── notice_create.sql │ │ └── java │ │ └── underdogs │ │ └── devbie │ │ └── DevbieApplicationTests.java └── build.gradle ├── lombok.config ├── devbie-common ├── build.gradle └── src │ └── main │ └── java │ └── underdogs │ └── devbie │ ├── user │ └── RoleType.java │ ├── exception │ ├── NotExistException.java │ ├── CreateFailException.java │ ├── BadRequestException.java │ ├── ForbiddenException.java │ └── UnAuthorizedException.java │ └── auth │ ├── LoginUser.java │ ├── NoValidate.java │ └── Role.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── devbie-core ├── src │ ├── test │ │ ├── resources │ │ │ └── devbie.png │ │ └── java │ │ │ └── underdogs │ │ │ └── devbie │ │ │ ├── chat │ │ │ └── domain │ │ │ │ ├── ChatRoomTest.java │ │ │ │ ├── ChatTest.java │ │ │ │ ├── ChatSessionInformationTest.java │ │ │ │ ├── ChatMessageTest.java │ │ │ │ ├── TitleColorTest.java │ │ │ │ ├── ChatNamesTest.java │ │ │ │ ├── ChatNameTest.java │ │ │ │ └── ChatNameFactoryTest.java │ │ │ ├── notice │ │ │ └── domain │ │ │ │ ├── CompanyTest.java │ │ │ │ ├── ApplyUrlTest.java │ │ │ │ ├── LanguageTest.java │ │ │ │ ├── RecruitmentTypeTest.java │ │ │ │ └── DurationTest.java │ │ │ ├── answer │ │ │ └── domain │ │ │ │ ├── AnswersTest.java │ │ │ │ └── AnswerContentTest.java │ │ │ ├── TestApplication.java │ │ │ ├── question │ │ │ └── domain │ │ │ │ ├── TagNameTest.java │ │ │ │ ├── VisitsTest.java │ │ │ │ ├── HashtagTest.java │ │ │ │ ├── QuestionTitleTest.java │ │ │ │ ├── QuestionContentTest.java │ │ │ │ └── QuestionHashtagTest.java │ │ │ ├── recommendation │ │ │ └── domain │ │ │ │ ├── RecommendationTest.java │ │ │ │ ├── AnswerRecommendationTest.java │ │ │ │ └── QuestionRecommendationTest.java │ │ │ └── favorite │ │ │ └── domain │ │ │ ├── NoticeFavoriteTest.java │ │ │ └── QuestionFavoriteTest.java │ └── main │ │ └── java │ │ └── underdogs │ │ └── devbie │ │ ├── chat │ │ ├── dto │ │ │ ├── StompMessageResponseData.java │ │ │ ├── MessageSendRequest.java │ │ │ ├── ChatRoomCreateRequest.java │ │ │ ├── ChatRoomResponse.java │ │ │ ├── MessageResponses.java │ │ │ ├── ChatNameResponse.java │ │ │ ├── MessageResponse.java │ │ │ └── StompMessageResponse.java │ │ ├── domain │ │ │ ├── StompMethodType.java │ │ │ ├── ChatRoomRepository.java │ │ │ ├── ChatRepository.java │ │ │ ├── ChatSessionInformation.java │ │ │ ├── Adjective.java │ │ │ ├── ChatMessage.java │ │ │ ├── CrewName.java │ │ │ ├── ChatNames.java │ │ │ ├── TitleColor.java │ │ │ ├── Chat.java │ │ │ ├── ChatRoom.java │ │ │ └── ChatName.java │ │ └── exception │ │ │ ├── SessionIdAlreadyExistException.java │ │ │ ├── SessionNotExistException.java │ │ │ └── NoSuchTitleColorException.java │ │ ├── notice │ │ ├── domain │ │ │ ├── NoticeType.java │ │ │ ├── RecruitmentType.java │ │ │ ├── NoticeRepository.java │ │ │ ├── JobPosition.java │ │ │ ├── NoticeRepositoryCustom.java │ │ │ ├── ApplyUrl.java │ │ │ ├── Language.java │ │ │ └── Company.java │ │ ├── expception │ │ │ ├── NoticeNotFoundException.java │ │ │ └── NoSuchLanguageException.java │ │ ├── vo │ │ │ ├── NoticeCacheVo.java │ │ │ ├── LanguagePair.java │ │ │ └── JobPositionPair.java │ │ ├── dto │ │ │ ├── ImageUploadRequest.java │ │ │ ├── NoticeResponse.java │ │ │ ├── NoticeReadRequest.java │ │ │ ├── CustomPageRequest.java │ │ │ ├── NoticeDescriptionResponse.java │ │ │ └── FilterResponses.java │ │ └── service │ │ │ └── cache │ │ │ └── NoticeKeyGenerator.java │ │ ├── answer │ │ ├── domain │ │ │ ├── AnswerRepository.java │ │ │ ├── AnswerRepositoryCustom.java │ │ │ ├── Answers.java │ │ │ ├── AnswerContent.java │ │ │ └── AnswerRepositoryImpl.java │ │ ├── exception │ │ │ ├── AnswerNotExistedException.java │ │ │ └── NotMatchedAnswerAuthorException.java │ │ └── dto │ │ │ ├── AnswerUpdateRequest.java │ │ │ ├── AnswerResponses.java │ │ │ ├── AnswerCreateRequest.java │ │ │ └── AnswerResponse.java │ │ ├── question │ │ ├── exception │ │ │ ├── HashtagNotExistedException.java │ │ │ ├── QuestionNotMeetingEssentialsException.java │ │ │ ├── QuestionNotExistedException.java │ │ │ └── NotMatchedQuestionAuthorException.java │ │ ├── domain │ │ │ ├── repository │ │ │ │ ├── QuestionRepository.java │ │ │ │ ├── QuestionHashtagRepositoryCustom.java │ │ │ │ ├── QuestionRepositoryCustom.java │ │ │ │ ├── HashtagRepository.java │ │ │ │ ├── QuestionHashtagRepository.java │ │ │ │ └── QuestionHashtagRepositoryImpl.java │ │ │ ├── OrderBy.java │ │ │ ├── Visits.java │ │ │ ├── TagName.java │ │ │ ├── QuestionTitle.java │ │ │ ├── QuestionContent.java │ │ │ ├── Hashtag.java │ │ │ └── QuestionHashtag.java │ │ └── dto │ │ │ ├── QuestionReadRequest.java │ │ │ ├── HashtagUpdateRequest.java │ │ │ ├── HashtagCreateRequest.java │ │ │ ├── HashtagResponses.java │ │ │ ├── HashtagsRequest.java │ │ │ ├── QuestionResponses.java │ │ │ ├── QuestionPageRequest.java │ │ │ ├── QuestionCreateRequest.java │ │ │ └── QuestionUpdateRequest.java │ │ ├── recommendation │ │ ├── domain │ │ │ ├── AnswerRecommendationRepositoryCustom.java │ │ │ ├── QuestionRecommendationRepositoryCustom.java │ │ │ ├── RecommendationRepository.java │ │ │ ├── AnswerRecommendationRepository.java │ │ │ ├── QuestionRecommendationRepository.java │ │ │ ├── RecommendationType.java │ │ │ ├── AnswerRecommendationRepositoryImpl.java │ │ │ ├── QuestionRecommendationRepositoryImpl.java │ │ │ ├── RecommendationCount.java │ │ │ ├── AnswerRecommendation.java │ │ │ └── QuestionRecommendation.java │ │ ├── dto │ │ │ ├── RecommendationRequest.java │ │ │ └── RecommendationResponse.java │ │ └── service │ │ │ └── RecommendationService.java │ │ ├── user │ │ ├── domain │ │ │ └── UserRepository.java │ │ └── dto │ │ │ ├── UserUpdateImageRequest.java │ │ │ ├── UserCreateRequest.java │ │ │ ├── UserUpdateInfoRequest.java │ │ │ └── UserResponse.java │ │ ├── auth │ │ ├── exception │ │ │ ├── InvalidAuthenticationException.java │ │ │ ├── AccessTokenLoadException.java │ │ │ ├── NotExistUserRoleException.java │ │ │ ├── ExpiredTokenException.java │ │ │ └── LoginUserNotFoundException.java │ │ ├── dto │ │ │ ├── AccessTokenRequest.java │ │ │ ├── AccessTokenResponse.java │ │ │ ├── JwtTokenResponse.java │ │ │ ├── UserTokenDto.java │ │ │ └── UserInfoDto.java │ │ └── service │ │ │ └── AuthService.java │ │ ├── favorite │ │ ├── domain │ │ │ ├── FavoriteRepository.java │ │ │ ├── NoticeFavoriteRepository.java │ │ │ ├── QuestionFavoriteRepository.java │ │ │ ├── Favorite.java │ │ │ ├── NoticeFavorite.java │ │ │ └── QuestionFavorite.java │ │ └── service │ │ │ ├── FavoriteService.java │ │ │ ├── NoticeFavoriteService.java │ │ │ └── QuestionFavoriteService.java │ │ └── common │ │ └── BaseTimeEntity.java └── build.gradle ├── Dockerfile ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── epic_template.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── settings.gradle ├── .gitmodules ├── devbie-aws-client ├── build.gradle └── src │ └── main │ └── java │ └── underdogs │ └── devbie │ └── s3 │ └── S3Service.java ├── devbie-core-web └── build.gradle ├── LICENSE └── HELP.md /src/frontend/src/css/app.css: -------------------------------------------------------------------------------- 1 | @import url("~reset-css/reset.css"); -------------------------------------------------------------------------------- /devbie-app-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=develop 2 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true 3 | 4 | -------------------------------------------------------------------------------- /devbie-app-api/src/test/resources/notice_delete.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM language; 2 | DELETE FROM notice; 3 | -------------------------------------------------------------------------------- /src/frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /devbie-app-api/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.data= 2 | spring.flyway.enabled=false 3 | -------------------------------------------------------------------------------- /devbie-common/build.gradle: -------------------------------------------------------------------------------- 1 | bootJar { enabled = false } 2 | jar { enabled = true } 3 | 4 | dependencies { 5 | } 6 | -------------------------------------------------------------------------------- /src/frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2020-devbie/HEAD/src/frontend/public/favicon.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2020-devbie/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2020-devbie/HEAD/src/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /devbie-core/src/test/resources/devbie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2020-devbie/HEAD/devbie-core/src/test/resources/devbie.png -------------------------------------------------------------------------------- /devbie-app-api/src/test/resources/devbie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2020-devbie/HEAD/devbie-app-api/src/test/resources/devbie.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | COPY devbie-app-api/build/libs/devbie-app-api-1.0.jar app.jar 4 | 5 | ENTRYPOINT ["java", "-jar", "app.jar"] 6 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/user/RoleType.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user; 2 | 3 | public enum RoleType { 4 | 5 | USER, ADMIN, GUEST; 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @khb1109 @KimSeongGyu1 @slowCoyle @sonypark @yeonnseok 5 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/StompMessageResponseData.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | public interface StompMessageResponseData { 4 | } 5 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/NoticeType.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | public enum NoticeType { 4 | 5 | JOB, EDUCATION 6 | } 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'devbie' 2 | include 'devbie-common' 3 | include 'devbie-app-api' 4 | include 'devbie-core' 5 | include 'devbie-core-web' 6 | include 'devbie-aws-client' 7 | 8 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatRoomDrawer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docker"] 2 | path = docker 3 | url = https://github.com/KimSeongGyu1/devbie_docker 4 | [submodule "devbie-app-api/src/main/resources/properties"] 5 | path = devbie-app-api/src/main/resources/properties 6 | url = https://github.com/KimSeongGyu1/devbie_properties 7 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/exception/NotExistException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.exception; 2 | 3 | public class NotExistException extends RuntimeException { 4 | 5 | public NotExistException(String notExistObject) { 6 | super(notExistObject); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/RecruitmentType.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | public enum RecruitmentType { 4 | 5 | OPEN, ANY; 6 | 7 | public boolean isAnyTimeRecruitment() { 8 | return this == ANY; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jul 23 12:38:18 KST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: "새로운 기능 추가 \U0001F4A1" 4 | title: "[FEAT] 제목" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 목적 및 필요성 11 | - 이 기능의 목적 및 필요성 설명 12 | 13 | ## 작업 상세 내용 14 | - [ ] 해당 기능을 구현하기 위해 필요한 세부 내용 기술 15 | 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/domain/AnswerRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface AnswerRepository extends JpaRepository, AnswerRepositoryCustom{ 6 | } 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Resolve #이슈번호 2 | 3 | - 왜 이 PR이 요구되는지, 코드 변화가 필요한 이유가 무엇인지 4 | 5 | ## Changes 6 | 7 | - 위의 요구사항에 따라 실제로 어떤 변경을 했는지 8 | 9 | ## Notes 10 | 11 | - 추후 작업 시 알아두어야 할 것들 (버그나 에러 가능성에 대한 언급, 잔여 작업에 대한 설명 등) 12 | 13 | ## References 14 | 15 | - 참고한 사이트나 자료들 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/domain/AnswerRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import java.util.List; 4 | 5 | public interface AnswerRepositoryCustom { 6 | 7 | List findByQuestionIdOrderByRecommendationCount(Long questionId); 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/NoticeRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface NoticeRepository extends JpaRepository, NoticeRepositoryCustom { 6 | } 7 | -------------------------------------------------------------------------------- /src/frontend/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify"; 3 | import "vuetify/dist/vuetify.min.css"; 4 | 5 | Vue.use(Vuetify); 6 | 7 | const opts = { 8 | icons: { 9 | iconfont: "mdiSvg" 10 | } 11 | }; 12 | 13 | export default new Vuetify(opts); 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/epic_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Epic template 3 | about: "세부 기능을 포괄하는 Epic 생성 \U0001F5C2" 4 | title: "[EPIC] 제목" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 구현할 기능 11 | - 구현할 세부 기능을 포괄하는 주제 및 기능을 기술 12 | 13 | ## 작업 상세 내용 14 | - [ ] 해당 기능을 구현하기 위해 필요한 세부 내용 기술 15 | 16 | -------------------------------------------------------------------------------- /devbie-aws-client/build.gradle: -------------------------------------------------------------------------------- 1 | bootJar { enabled = false } 2 | jar { enabled = true } 3 | 4 | dependencies { 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.820') 7 | implementation 'com.amazonaws:aws-java-sdk-s3' 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/exception/HashtagNotExistedException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.exception; 2 | 3 | public class HashtagNotExistedException extends RuntimeException { 4 | 5 | public HashtagNotExistedException() { 6 | super("존재하지 않는 해시태그입니다."); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/frontend/src/views/notice/NoticeCreateView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/frontend/src/views/MainPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/AuditingConfig.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @EnableJpaAuditing 7 | @Configuration 8 | public class AuditingConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/frontend/src/views/notice/NoticeEditView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/frontend/src/views/user/MyPageView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /devbie-app-api/src/test/java/underdogs/devbie/DevbieApplicationTests.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DevbieApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/AopConfiguration.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 5 | 6 | @EnableAspectJAutoProxy 7 | @Configuration 8 | public class AopConfiguration { 9 | } 10 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/exception/CreateFailException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.exception; 2 | 3 | public class CreateFailException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "생성 인자가 올바르지 않습니다."; 6 | 7 | public CreateFailException() { 8 | super(MESSAGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/AnswerRecommendationRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface AnswerRecommendationRepositoryCustom { 6 | 7 | Optional findByObjectAndUserId(Long answerId, Long userId); 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/QuestionRecommendationRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface QuestionRecommendationRepositoryCustom { 6 | 7 | Optional findByObjectAndUserId(Long questionId, Long userId); 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/user/domain/UserRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user.domain; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface UserRepository extends JpaRepository { 8 | Optional findByOauthId(String id); 9 | } 10 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/StompMethodType.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 8 | @Getter 9 | public enum StompMethodType { 10 | 11 | ENTER, TALK, QUIT 12 | } 13 | -------------------------------------------------------------------------------- /src/frontend/src/components/question/FormTitle.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/JobPosition.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum JobPosition { 9 | 10 | BACKEND("백엔드"), 11 | FRONTEND("프론트엔드"); 12 | 13 | private final String text; 14 | } 15 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatRoomRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface ChatRoomRepository extends JpaRepository { 8 | 9 | Optional findByNoticeId(Long noticeId); 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatRoomTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class ChatRoomTest { 8 | 9 | @Test 10 | void from() { 11 | assertThat(ChatRoom.from(1L)).isInstanceOf(ChatRoom.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/exception/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.exception; 2 | 3 | public class BadRequestException extends RuntimeException { 4 | 5 | private static final String MESSAGE_PREFIX = "잘못된 요청입니다: 원인 : "; 6 | 7 | public BadRequestException(String reason) { 8 | super(MESSAGE_PREFIX + reason); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/exception/ForbiddenException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.exception; 2 | 3 | public class ForbiddenException extends RuntimeException { 4 | 5 | private static final String MESSAGE_PREFIX = "요청을 수행할 권리가 없습니다. 원인: "; 6 | 7 | public ForbiddenException(String reason) { 8 | super(MESSAGE_PREFIX + reason); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/exception/SessionIdAlreadyExistException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.exception; 2 | 3 | public class SessionIdAlreadyExistException extends RuntimeException { 4 | 5 | private static final String MESSAGE = "세션id가 이미 존재합니다"; 6 | 7 | public SessionIdAlreadyExistException() { 8 | super(MESSAGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/QuestionRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import underdogs.devbie.question.domain.Question; 6 | 7 | public interface QuestionRepository extends JpaRepository, QuestionRepositoryCustom { 8 | } 9 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/exception/InvalidAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.exception; 2 | 3 | import underdogs.devbie.exception.ForbiddenException; 4 | 5 | public class InvalidAuthenticationException extends ForbiddenException { 6 | 7 | public InvalidAuthenticationException(String reason) { 8 | super(reason); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/auth/LoginUser.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface LoginUser { 11 | } 12 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/exception/UnAuthorizedException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.exception; 2 | 3 | public class UnAuthorizedException extends RuntimeException { 4 | 5 | private static final String MESSAGE_PREFIX = "요청자의 신분을 확인하지 못하였습니다. 원인 : "; 6 | 7 | public UnAuthorizedException(String reason) { 8 | super(MESSAGE_PREFIX + reason); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/RecommendationRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface RecommendationRepository { 6 | 7 | Optional findByObjectAndUserId(Long objectId, Long userId); 8 | 9 | T save(T t); 10 | 11 | void delete(T t); 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/QuestionHashtagRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import java.util.List; 4 | 5 | public interface QuestionHashtagRepositoryCustom { 6 | 7 | void deleteAllByHashtagIds(List ids, Long questionId); 8 | 9 | List findQuestionIdsByHashtagId(Long hashtagId); 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class ChatTest { 8 | 9 | @Test 10 | void of() { 11 | assertThat(Chat.of("하늘하늘한 동글", TitleColor.AMBER, "메세지", ChatRoom.from(1L))).isInstanceOf(Chat.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/auth/NoValidate.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.METHOD, ElementType.TYPE}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface NoValidate { 11 | } 12 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/exception/QuestionNotMeetingEssentialsException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.exception; 2 | 3 | import underdogs.devbie.exception.BadRequestException; 4 | 5 | public class QuestionNotMeetingEssentialsException extends BadRequestException { 6 | 7 | public QuestionNotMeetingEssentialsException(String reason) { 8 | super(reason); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/exception/SessionNotExistException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.exception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class SessionNotExistException extends NotExistException { 6 | 7 | private static final String MESSAGE = "세션이 존재하지 않습니다"; 8 | 9 | public SessionNotExistException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | h2: 3 | console: 4 | enabled: true 5 | jpa: 6 | show-sql: true 7 | properties: 8 | hibernate: 9 | format_sql: true 10 | default_batch_fetch_size: 100 11 | 12 | cloud: 13 | aws: 14 | s3: 15 | bucket: underdong-devbie-images 16 | region: 17 | static: ap-northeast-2 18 | stack: 19 | auto: false 20 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/exception/AnswerNotExistedException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.exception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class AnswerNotExistedException extends NotExistException { 6 | 7 | private static final String MESSAGE = "답변이 존재하지 않습니다."; 8 | 9 | public AnswerNotExistedException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/expception/NoticeNotFoundException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.expception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class NoticeNotFoundException extends NotExistException { 6 | 7 | private static final String MESSAGE = "해당 공고를 찾을 수 없습니다."; 8 | 9 | public NoticeNotFoundException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/exception/AccessTokenLoadException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.exception; 2 | 3 | import underdogs.devbie.exception.UnAuthorizedException; 4 | 5 | public class AccessTokenLoadException extends UnAuthorizedException { 6 | 7 | private static final String MESSAGE = "토큰을 Load하지 못하였습니다."; 8 | 9 | public AccessTokenLoadException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/exception/NoSuchTitleColorException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.exception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class NoSuchTitleColorException extends NotExistException { 6 | 7 | private static final String MESSAGE = "존재하지 않는 TitleColor 입니다."; 8 | 9 | public NoSuchTitleColorException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/NoticeRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | 6 | public interface NoticeRepositoryCustom { 7 | 8 | Page findAllBy(NoticeType noticeType, JobPosition jobPosition, 9 | Language language, String keyword, Pageable pageable); 10 | } 11 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/expception/NoSuchLanguageException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.expception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class NoSuchLanguageException extends NotExistException { 6 | 7 | private static final String MESSAGE = "존재하지 않는 프로그래밍 언어 입니다."; 8 | 9 | public NoSuchLanguageException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/AnswerRecommendationRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface AnswerRecommendationRepository extends 6 | JpaRepository, 7 | RecommendationRepository, 8 | AnswerRecommendationRepositoryCustom { 9 | } 10 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/advice/dto/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.advice.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @AllArgsConstructor(access = AccessLevel.PUBLIC) 10 | @Getter 11 | public class ErrorResponse { 12 | private String message; 13 | } 14 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/exception/NotExistUserRoleException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.exception; 2 | 3 | import underdogs.devbie.exception.UnAuthorizedException; 4 | 5 | public class NotExistUserRoleException extends UnAuthorizedException { 6 | 7 | private static final String message = "존재하지 않는 UserRole 입니다."; 8 | 9 | public NotExistUserRoleException() { 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/exception/QuestionNotExistedException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.exception; 2 | 3 | import underdogs.devbie.exception.NotExistException; 4 | 5 | public class QuestionNotExistedException extends NotExistException { 6 | 7 | private static final String MESSAGE = "존재하지 않는 질문입니다."; 8 | 9 | public QuestionNotExistedException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface ChatRepository extends JpaRepository { 6 | 7 | // @Query("select c from Chat c join ChatRoom r on c.chatRoom.id=r.id where r.noticeId=:noticeId") 8 | // List findByNoticeId(@Param("noticeId") Long noticeId); 9 | } 10 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/FavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface FavoriteRepository { 7 | 8 | Optional findByObjectAndUserId(Long objectId, Long userId); 9 | 10 | List findAllByUserId(Long userId); 11 | 12 | T save(T t); 13 | 14 | void delete(T t); 15 | } 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/QuestionRecommendationRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface QuestionRecommendationRepository extends 6 | JpaRepository, 7 | RecommendationRepository, 8 | QuestionRecommendationRepositoryCustom { 9 | } 10 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/exception/ExpiredTokenException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.exception; 2 | 3 | import underdogs.devbie.exception.UnAuthorizedException; 4 | 5 | public class ExpiredTokenException extends UnAuthorizedException { 6 | 7 | private static final String EXPIRED_TOKEN_MESSAGE = "토큰의 유효 기간이 만료되었습니다."; 8 | 9 | public ExpiredTokenException() { 10 | super(EXPIRED_TOKEN_MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/exception/LoginUserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.exception; 2 | 3 | import underdogs.devbie.exception.UnAuthorizedException; 4 | 5 | public class LoginUserNotFoundException extends UnAuthorizedException { 6 | 7 | private static final String MESSAGE = "토큰에 존재하는 정보와 일치하는 사용자가 존재하지 않습니다."; 8 | 9 | public LoginUserNotFoundException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/QuestionRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | 6 | import underdogs.devbie.question.domain.Question; 7 | 8 | public interface QuestionRepositoryCustom { 9 | 10 | Page findAllBy(String title, String content, Pageable pageable); 11 | } 12 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/exception/NotMatchedQuestionAuthorException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.exception; 2 | 3 | import underdogs.devbie.exception.ForbiddenException; 4 | 5 | public class NotMatchedQuestionAuthorException extends ForbiddenException { 6 | 7 | private static final String MESSAGE = "작성자만 할 수 있습니다."; 8 | 9 | public NotMatchedQuestionAuthorException() { 10 | super(MESSAGE); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/profile/ProfileLocal.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config.profile; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @Profile(value="local") 9 | @PropertySource({"classpath:properties/local/application.properties"}) 10 | public class ProfileLocal { 11 | } 12 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/dto/AccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | @ToString 12 | public class AccessTokenRequest { 13 | 14 | private String code; 15 | private String client_id; 16 | private String client_secret; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/profile/ProfileDevelop.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config.profile; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @Profile(value="develop") 9 | @PropertySource({"classpath:properties/develop/application.properties"}) 10 | public class ProfileDevelop { 11 | } 12 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/exception/NotMatchedAnswerAuthorException.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.exception; 2 | 3 | import underdogs.devbie.auth.exception.InvalidAuthenticationException; 4 | 5 | public class NotMatchedAnswerAuthorException extends InvalidAuthenticationException { 6 | 7 | private static final String FEATURE_NAME = "답변 작성 수정"; 8 | 9 | public NotMatchedAnswerAuthorException() { 10 | super(FEATURE_NAME); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatSessionInformation.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public class ChatSessionInformation { 9 | 10 | private final long noticeId; 11 | private final ChatName chatName; 12 | 13 | public boolean hasNoticeOf(long noticeId) { 14 | return this.noticeId == noticeId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/frontend/src/views/hashtags/HashtagsView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/profile/ProfileProduction.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config.profile; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Profile; 5 | import org.springframework.context.annotation.PropertySource; 6 | 7 | @Configuration 8 | @Profile(value = "production") 9 | @PropertySource({"classpath:properties/production/application.properties"}) 10 | public class ProfileProduction { 11 | } -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/ETagHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.filter.ShallowEtagHeaderFilter; 6 | 7 | @Configuration 8 | public class ETagHeaderFilter { 9 | @Bean 10 | public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { 11 | return new ShallowEtagHeaderFilter(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/dto/AccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | @Getter 12 | @ToString 13 | public class AccessTokenResponse { 14 | 15 | private String access_token; 16 | private String scope; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/DevbieApplication.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | 7 | @SpringBootApplication 8 | @EnableCaching 9 | public class DevbieApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(DevbieApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/HashtagRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import underdogs.devbie.question.domain.Hashtag; 8 | import underdogs.devbie.question.domain.TagName; 9 | 10 | public interface HashtagRepository extends JpaRepository { 11 | 12 | Optional findByTagName(TagName tagName); 13 | } 14 | -------------------------------------------------------------------------------- /src/frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | outputDir: path.resolve(__dirname, "../../" + "docker/nginx/static"), 5 | devServer: { 6 | host: "localhost", 7 | proxy: { 8 | "/api": { 9 | target: "http://localhost:9000", 10 | ws: true, 11 | changeOrigin: true 12 | }, 13 | "/chat": { 14 | target: "http://localhost:9000", 15 | ws: true, 16 | changeOrigin: true 17 | } 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /devbie-common/src/main/java/underdogs/devbie/auth/Role.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import underdogs.devbie.user.RoleType; 9 | 10 | @Target({ElementType.METHOD}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface Role { 13 | 14 | RoleType[] role() default {RoleType.ADMIN, RoleType.USER, RoleType.GUEST}; 15 | } 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/MessageSendRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @AllArgsConstructor 10 | @Getter 11 | public class MessageSendRequest { 12 | 13 | private Long noticeId; 14 | private String name; 15 | private String message; 16 | private String titleColor; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/vo/NoticeCacheVo.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.vo; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.Setter; 7 | import underdogs.devbie.notice.dto.NoticeReadRequest; 8 | 9 | @RequiredArgsConstructor 10 | @Getter 11 | @Setter 12 | @EqualsAndHashCode 13 | public class NoticeCacheVo { 14 | 15 | private final NoticeReadRequest noticeReadRequest; 16 | private final int page; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-core-web/build.gradle: -------------------------------------------------------------------------------- 1 | bootJar { enabled = false } 2 | jar { enabled = true } 3 | 4 | dependencies { 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | 7 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' 8 | compile group: 'org.apache.commons', name: 'commons-io', version: '1.3.2' 9 | compile group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1' 10 | compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.12' 11 | } 12 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/dto/JwtTokenResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.ToString; 7 | 8 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 9 | @Getter 10 | @ToString 11 | public class JwtTokenResponse { 12 | 13 | private final String token; 14 | 15 | public static JwtTokenResponse from(String token) { 16 | return new JwtTokenResponse(token); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/ChatRoomCreateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import javax.validation.constraints.NotNull; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor() 12 | @Getter 13 | public class ChatRoomCreateRequest { 14 | 15 | @NotNull(message = "Notice ID가 존재하지 않습니다.") 16 | private Long noticeId; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/RecommendationType.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | public enum RecommendationType { 4 | 5 | RECOMMENDED, 6 | NON_RECOMMENDED; 7 | 8 | public boolean is(RecommendationType recommendationType) { 9 | return this == recommendationType; 10 | } 11 | 12 | public RecommendationType toggleType() { 13 | if (this == RECOMMENDED) { 14 | return NON_RECOMMENDED; 15 | } 16 | return RECOMMENDED; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/Adjective.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | public enum Adjective { 4 | 5 | 찬란한, 6 | 어슴프레한, 7 | 발랄한, 8 | 우아한, 9 | 푸르른, 10 | 부드러운, 11 | 정겨운, 12 | 시원한, 13 | 다정한, 14 | 조용한, 15 | 상냥한, 16 | 새침한, 17 | 화난, 18 | 활동적인, 19 | 놀라운, 20 | 매력적인, 21 | 지루한, 22 | 도전적인, 23 | 발랄할, 24 | 영리한, 25 | 편안한, 26 | 재미있는, 27 | 완벽한, 28 | 창의적인, 29 | 바쁜, 30 | 재치있는, 31 | 유쾌한, 32 | 날쌘, 33 | 민첩한, 34 | 공손한 35 | } 36 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatSessionInformationTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class ChatSessionInformationTest { 8 | 9 | @Test 10 | void hasNoticeOf() { 11 | ChatSessionInformation chatSessionInformation = 12 | new ChatSessionInformation(1L, ChatName.of("testName", TitleColor.AMBER)); 13 | 14 | assertThat(chatSessionInformation.hasNoticeOf(1L)).isTrue(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/notice/domain/CompanyTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class CompanyTest { 11 | 12 | @DisplayName("Company 생성 테스트 - 빈 이름 입력시 예외 발생") 13 | @Test 14 | void constructorWithEmptyName() { 15 | assertThatThrownBy(() -> new Company("")) 16 | .isInstanceOf(CreateFailException.class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/src/utils/editor.js: -------------------------------------------------------------------------------- 1 | import VueMarkdownEditor from "@kangc/v-md-editor"; 2 | import "@kangc/v-md-editor/lib/style/base-editor.css"; 3 | import githubTheme from "@kangc/v-md-editor/lib/theme/github.js"; 4 | import enUS from "@kangc/v-md-editor/lib/lang/en-US"; 5 | import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index"; 6 | import Vue from "vue"; 7 | 8 | VueMarkdownEditor.lang.use("en-US", enUS); 9 | VueMarkdownEditor.use(createCopyCodePlugin()); 10 | VueMarkdownEditor.use(githubTheme); 11 | Vue.use(VueMarkdownEditor); 12 | 13 | export const editor = new Vue(); 14 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/answer/domain/AnswersTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.assertj.core.util.Lists; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class AnswersTest { 10 | 11 | @DisplayName("Answers 정적 팩토리 메서드") 12 | @Test 13 | void from() { 14 | Answers answers = Answers.from(Lists.newArrayList(new Answer(), new Answer(), new Answer())); 15 | 16 | assertThat(answers).isInstanceOf(Answers.class); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatMessageTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ChatMessageTest { 9 | 10 | @Test 11 | void from() { 12 | ChatMessage message = ChatMessage.from("메세지"); 13 | assertAll( 14 | () -> assertThat(message).isInstanceOf(ChatMessage.class), 15 | () -> assertThat(message.getMessage()).isEqualTo("메세지") 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/notice/domain/ApplyUrlTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class ApplyUrlTest { 11 | 12 | @DisplayName("Company 생성 테스트 - 잘못된 형식의 URL 입력") 13 | @Test 14 | void constructorWithWrongUrl() { 15 | assertThatThrownBy(() -> new ApplyUrl("잘못된URL.com")) 16 | .isInstanceOf(CreateFailException.class); 17 | } 18 | } -------------------------------------------------------------------------------- /src/frontend/src/components/question/MarkdownContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/dto/RecommendationRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | import underdogs.devbie.recommendation.domain.RecommendationType; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 12 | @Getter 13 | @ToString 14 | public class RecommendationRequest { 15 | 16 | private RecommendationType recommendationType; 17 | } 18 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/QuestionReadRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 13 | @Builder 14 | @Getter 15 | @Setter 16 | @ToString 17 | public class QuestionReadRequest { 18 | 19 | private String title; 20 | private String content; 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: "버그 또는 오타 수정 \U0001F6E0️." 4 | title: "[BUG] 제목" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 버그 내용 11 | - 어떤 버그인지 간단히 설명 12 | 13 | ## 버그 발생 시나리오 14 | - 버그가 발생하기까지의 과정 설명 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | ## 기대한 결과 22 | - 원래 의도했던 결과는 무엇인지 설명 23 | 24 | ## 스크린샷 (optional) 25 | (가능하다면) 버그 발생한 부분 스크린샷 첨부 26 | 27 | ## 작업 환경 28 | - OS: [e.g. Mac, Window] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | ## 추가 정보 33 | - 버그가 발생한 이유를 설명하기 위한 추가적인 정보 첨부 34 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Embeddable 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 13 | @Getter 14 | public class ChatMessage { 15 | 16 | private String message; 17 | 18 | public static ChatMessage from(String message) { 19 | return new ChatMessage(message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/favorite/NoticeFavoriteController.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import underdogs.devbie.favorite.service.NoticeFavoriteService; 7 | 8 | @RestController 9 | @RequestMapping("/api/favorite-notice") 10 | public class NoticeFavoriteController extends FavoriteController { 11 | 12 | public NoticeFavoriteController(NoticeFavoriteService noticeFavoriteService) { 13 | this.favoriteService = noticeFavoriteService; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/src/views/notice/NoticeDetailView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/favorite/QuestionFavoriteController.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import underdogs.devbie.favorite.service.QuestionFavoriteService; 7 | 8 | @RestController 9 | @RequestMapping("/api/favorite-question") 10 | public class QuestionFavoriteController extends FavoriteController { 11 | 12 | public QuestionFavoriteController(QuestionFavoriteService questionFavoriteService) { 13 | this.favoriteService = questionFavoriteService; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/QuerydslConfiguration.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import javax.persistence.EntityManager; 4 | import javax.persistence.PersistenceContext; 5 | 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import com.querydsl.jpa.impl.JPAQueryFactory; 10 | 11 | @Configuration 12 | public class QuerydslConfiguration { 13 | 14 | @PersistenceContext 15 | private EntityManager entityManager; 16 | 17 | @Bean 18 | public JPAQueryFactory jpaQueryFactory() { 19 | return new JPAQueryFactory(entityManager); 20 | } 21 | } -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/notice/domain/LanguageTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.notice.expception.NoSuchLanguageException; 9 | 10 | class LanguageTest { 11 | 12 | @DisplayName("등록되지 않은 언어 예외 발생") 13 | @Test 14 | void from() { 15 | assertThatThrownBy(() -> Language.from("KOTLIN")) 16 | .isInstanceOf(NoSuchLanguageException.class) 17 | .hasMessageContaining("존재하지 않는 프로그래밍 언어 입니다."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/TitleColorTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.chat.exception.NoSuchTitleColorException; 9 | 10 | class TitleColorTest { 11 | 12 | @DisplayName("지원하지 않는 타이틀 컬러") 13 | @Test 14 | void from() { 15 | assertThatThrownBy(() -> TitleColor.from("#9E9E9A")) 16 | .isInstanceOf(NoSuchTitleColorException.class) 17 | .hasMessageContaining("존재하지 않는 TitleColor 입니다."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/frontend/src/store/modules/hashtags.js: -------------------------------------------------------------------------------- 1 | import { getAction } from "../../api"; 2 | 3 | export default { 4 | state: { 5 | hashtags: [] 6 | }, 7 | mutations: { 8 | SET_HASHTAGS(state, data) { 9 | state.hashtags = data; 10 | } 11 | }, 12 | actions: { 13 | async FETCH_HASHTAGS({ commit }) { 14 | try { 15 | const { data } = await getAction(`/api/hashtags`); 16 | commit("SET_HASHTAGS", data.hashtags); 17 | } catch (error) { 18 | console.log(error); 19 | } 20 | } 21 | }, 22 | getters: { 23 | fetchedHashtags(state) { 24 | return state.hashtags; 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/QuestionHashtagRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import underdogs.devbie.question.domain.QuestionHashtag; 9 | 10 | public interface QuestionHashtagRepository extends JpaRepository, QuestionHashtagRepositoryCustom { 11 | 12 | Optional findByQuestionIdAndHashtagId(Long questionId, Long hashtagId); 13 | 14 | List findAllByQuestionId(Long questionId); 15 | } 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/dto/AnswerUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 12 | @Getter 13 | public class AnswerUpdateRequest { 14 | 15 | @NotBlank 16 | private String content; 17 | 18 | public static AnswerUpdateRequest from(String content) { 19 | return new AnswerUpdateRequest(content); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/OrderBy.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import org.springframework.data.domain.Sort.Direction; 4 | 5 | import lombok.Getter; 6 | 7 | @Getter 8 | public enum OrderBy { 9 | CREATED_DATE("createdDate", Direction.DESC), 10 | VISITS("visits.visitCount", Direction.DESC), 11 | RECOMMENDATIONS("recommendationCount.recommendedCount", Direction.DESC); 12 | 13 | private final String propertyName; 14 | private final Direction direction; 15 | 16 | OrderBy(String propertyName, Direction direction) { 17 | this.propertyName = propertyName; 18 | this.direction = direction; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/notice/domain/RecruitmentTypeTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.CsvSource; 8 | 9 | class RecruitmentTypeTest { 10 | 11 | @DisplayName("채용타입이 상시채용인지 확인") 12 | @ParameterizedTest 13 | @CsvSource(value = {"ANY,true", "OPEN,false"}) 14 | void isAnyTimeRecruitment(RecruitmentType recruitmentType, boolean expect) { 15 | assertThat(recruitmentType.isAnyTimeRecruitment()) 16 | .isEqualTo(expect); 17 | } 18 | } -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/recommendation/AnswerRecommendationController.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import underdogs.devbie.recommendation.service.AnswerRecommendationService; 7 | 8 | @RestController 9 | @RequestMapping("/api/recommendation-answer") 10 | public class AnswerRecommendationController extends RecommendationController { 11 | 12 | public AnswerRecommendationController(AnswerRecommendationService answerRecommendationService) { 13 | this.recommendationService = answerRecommendationService; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatNamesTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.Collections; 6 | import java.util.HashSet; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | class ChatNamesTest { 11 | 12 | @Test 13 | void from() { 14 | assertThat(ChatNames.from(Collections.EMPTY_SET)).isInstanceOf(ChatNames.class); 15 | } 16 | 17 | @Test 18 | void add() { 19 | ChatNames chatNames = ChatNames.from(new HashSet<>()); 20 | chatNames.add(ChatName.of("찬란한 동글", TitleColor.AMBER)); 21 | 22 | assertThat(chatNames.getChatNames()).hasSize(1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/recommendation/QuestionRecommendationController.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import underdogs.devbie.recommendation.service.QuestionRecommendationService; 7 | 8 | @RestController 9 | @RequestMapping("/api/recommendation-question") 10 | public class QuestionRecommendationController extends RecommendationController { 11 | 12 | public QuestionRecommendationController(QuestionRecommendationService questionRecommendationService) { 13 | this.recommendationService = questionRecommendationService; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/ImageUploadRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import javax.validation.constraints.NotNull; 4 | 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.Setter; 13 | import lombok.ToString; 14 | 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Builder 18 | @Getter 19 | @Setter 20 | @ToString 21 | public class ImageUploadRequest { 22 | 23 | @NotNull 24 | private MultipartFile image; 25 | } 26 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/user/dto/UserUpdateImageRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user.dto; 2 | 3 | import javax.validation.constraints.NotNull; 4 | 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.Setter; 13 | import lombok.ToString; 14 | 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Builder 18 | @Getter 19 | @Setter 20 | @ToString 21 | public class UserUpdateImageRequest { 22 | 23 | @NotNull 24 | private MultipartFile image; 25 | } 26 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/ChatRoomResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import java.util.List; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import underdogs.devbie.chat.domain.Chat; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor 13 | @Getter 14 | public class ChatRoomResponse { 15 | 16 | private MessageResponses messageResponses; 17 | 18 | private Integer headCount; 19 | 20 | public static ChatRoomResponse of(List chats, Integer headCount) { 21 | return new ChatRoomResponse(MessageResponses.from(chats), headCount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/domain/Answers.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | import lombok.ToString; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 13 | @ToString 14 | public class Answers { 15 | private List answers; 16 | 17 | public static Answers from(List answers) { 18 | return new Answers(answers); 19 | } 20 | 21 | public List getAnswers() { 22 | return Collections.unmodifiableList(answers); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/common/BaseTimeEntity.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.common; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.persistence.EntityListeners; 6 | import javax.persistence.MappedSuperclass; 7 | 8 | import org.springframework.data.annotation.CreatedDate; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 11 | 12 | import lombok.Getter; 13 | 14 | @Getter 15 | @MappedSuperclass 16 | @EntityListeners(AuditingEntityListener.class) 17 | public abstract class BaseTimeEntity { 18 | 19 | @CreatedDate 20 | private LocalDateTime createdDate; 21 | 22 | @LastModifiedDate 23 | private LocalDateTime modifiedDate; 24 | } -------------------------------------------------------------------------------- /src/frontend/src/components/question/FormButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/CrewName.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | public enum CrewName { 4 | 5 | 동글, 6 | 빙봉, 7 | 타미, 8 | 코일, 9 | 제이미, 10 | 서브웨이, 11 | 제이, 12 | 라빈, 13 | 비밥, 14 | 로운, 15 | 토니, 16 | 범블비, 17 | 코즈, 18 | 알트, 19 | 유안, 20 | 럿고, 21 | 카일, 22 | 히히, 23 | 엘리, 24 | 티거, 25 | 디디, 26 | 앨런, 27 | 하비, 28 | 터틀, 29 | 무늬, 30 | 두강, 31 | 소니, 32 | 핀, 33 | 보스독, 34 | 레베카, 35 | 스티치, 36 | 예지니어스, 37 | 호돌, 38 | 라흐, 39 | 시카, 40 | 오구, 41 | 쿨라임, 42 | 학성, 43 | 카프카, 44 | 라면, 45 | 라테, 46 | 히로, 47 | 쪼밀리, 48 | 오렌지, 49 | 또링, 50 | 그니, 51 | 작은곰, 52 | 그래 53 | } 54 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/Visits.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | 12 | @Embeddable 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | @ToString 17 | @EqualsAndHashCode 18 | public class Visits { 19 | 20 | private Long visitCount; 21 | 22 | public static Visits init() { 23 | return new Visits(0L); 24 | } 25 | 26 | public void increase() { 27 | this.visitCount++; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import loginUser from "./modules/loginUser"; 4 | import questions from "./modules/questions"; 5 | import answers from "./modules/answers"; 6 | import notices from "./modules/notices"; 7 | import recommendations from "./modules/recommendations"; 8 | import snackBar from "./modules/snackBar"; 9 | import hashtags from "./modules/hashtags"; 10 | import chat from "./modules/chat"; 11 | import favorite from "./modules/favorite"; 12 | 13 | Vue.use(Vuex); 14 | 15 | export const store = new Vuex.Store({ 16 | modules: { 17 | loginUser, 18 | questions, 19 | answers, 20 | notices, 21 | recommendations, 22 | snackBar, 23 | hashtags, 24 | chat, 25 | favorite 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/MessageResponses.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import static java.util.stream.Collectors.*; 4 | 5 | import java.util.List; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import underdogs.devbie.chat.domain.Chat; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor 15 | @Getter 16 | public class MessageResponses { 17 | 18 | private List messageResponses; 19 | 20 | public static MessageResponses from(List chats) { 21 | return new MessageResponses(chats.stream() 22 | .map(MessageResponse::from) 23 | .collect(toList())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/ChatNameResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import underdogs.devbie.chat.domain.ChatName; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 11 | @Getter 12 | public class ChatNameResponse implements StompMessageResponseData { 13 | 14 | private String name; 15 | private String color; 16 | private String sessionId; 17 | 18 | public static ChatNameResponse of(ChatName chatName, String sessionId) { 19 | return new ChatNameResponse(chatName.getChatName(), chatName.getColor().getColor(), sessionId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/TagName.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @Embeddable 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Getter 17 | @ToString 18 | @EqualsAndHashCode 19 | public class TagName { 20 | 21 | @Column(unique = true) 22 | private String name; 23 | 24 | public static TagName from(String name) { 25 | return new TagName(name.toLowerCase()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/MessageResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import underdogs.devbie.chat.domain.Chat; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | @Getter 12 | public class MessageResponse implements StompMessageResponseData { 13 | 14 | private Long id; 15 | private String name; 16 | private String titleColor; 17 | private String message; 18 | 19 | public static MessageResponse from(Chat chat) { 20 | return new MessageResponse(chat.getId(), chat.getName().getChatName(), chat.getName().getColor().getColor(), 21 | chat.getMessage().getMessage()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/frontend/src/views/admin/AdminMainView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 35 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/dto/StompMessageResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import underdogs.devbie.chat.domain.StompMethodType; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 11 | @Getter 12 | public class StompMessageResponse { 13 | 14 | private StompMethodType stompMethodType; 15 | private T data; 16 | 17 | public static StompMessageResponse of( 18 | StompMethodType stompMethodType, 19 | T data 20 | ) { 21 | return new StompMessageResponse(stompMethodType, data); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/TestApplication.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie; 2 | 3 | import javax.persistence.EntityManager; 4 | import javax.persistence.PersistenceContext; 5 | 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.test.context.TestConfiguration; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | import com.querydsl.jpa.impl.JPAQueryFactory; 11 | 12 | @SpringBootApplication 13 | class TestApplication { 14 | 15 | @TestConfiguration 16 | public static class TestConfig { 17 | 18 | @PersistenceContext 19 | private EntityManager entityManager; 20 | 21 | @Bean 22 | public JPAQueryFactory jpaQueryFactory() { 23 | return new JPAQueryFactory(entityManager); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatRoomTitleBox.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/HashtagUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.question.domain.Hashtag; 12 | import underdogs.devbie.question.domain.TagName; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Builder 17 | @Getter 18 | @ToString 19 | public class HashtagUpdateRequest { 20 | 21 | @NotBlank 22 | private String tagName; 23 | 24 | public Hashtag toEntity() { 25 | return new Hashtag(null, TagName.from(tagName)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/HashtagCreateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.question.domain.Hashtag; 12 | import underdogs.devbie.question.domain.TagName; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Builder 17 | @Getter 18 | @ToString 19 | public class HashtagCreateRequest { 20 | 21 | @NotBlank 22 | private String tagName; 23 | 24 | public Hashtag toEntity() { 25 | return new Hashtag(null, TagName.from(tagName)); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/dto/UserTokenDto.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | import underdogs.devbie.user.RoleType; 10 | import underdogs.devbie.user.domain.User; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 14 | @Builder 15 | @Getter 16 | @ToString 17 | public class UserTokenDto { 18 | 19 | private Long id; 20 | 21 | private RoleType roleType; 22 | 23 | public static UserTokenDto from(User user) { 24 | return UserTokenDto.builder() 25 | .id(user.getId()) 26 | .roleType(user.getRoleType()) 27 | .build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/domain/AnswerContent.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import javax.persistence.Lob; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | 14 | @Embeddable 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Getter 18 | @ToString 19 | @EqualsAndHashCode 20 | public class AnswerContent { 21 | 22 | @Lob 23 | @Column(columnDefinition = "TEXT") 24 | private String content; 25 | 26 | public static AnswerContent from(String content) { 27 | return new AnswerContent(content); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/NoticeResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import java.util.Set; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.notice.domain.JobPosition; 12 | import underdogs.devbie.notice.domain.NoticeType; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Builder 17 | @Getter 18 | @ToString 19 | public class NoticeResponse { 20 | 21 | private Long id; 22 | private String name; 23 | private String title; 24 | private NoticeType noticeType; 25 | private Set languages; 26 | private JobPosition jobPosition; 27 | private String image; 28 | } 29 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/user/dto/UserCreateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.user.domain.User; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Builder 16 | @Getter 17 | @ToString 18 | public class UserCreateRequest { 19 | 20 | @NotBlank 21 | private String name; 22 | 23 | @NotBlank 24 | private String email; 25 | 26 | public User toEntity() { 27 | return User.builder() 28 | .name(name) 29 | .email(email) 30 | .build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import vuetify from "./plugins/vuetify"; 4 | import { router } from "./router"; 5 | import { store } from "./store"; 6 | import { editor } from "./utils/editor"; 7 | import VueAnalytics from "vue-analytics"; 8 | import VueMq from "vue-mq"; 9 | 10 | const isProd = process.env.NODE_ENV === "production"; 11 | 12 | Vue.use(VueAnalytics, { 13 | id: "UA-176434466-1", 14 | debug: { 15 | enable: !isProd, 16 | sendHitTask: isProd 17 | }, 18 | router 19 | }); 20 | 21 | Vue.use(VueMq, { 22 | breakpoints: { 23 | mobile: 450, 24 | tablet: 900, 25 | laptop: 1250, 26 | desktop: Infinity 27 | } 28 | }); 29 | 30 | Vue.config.productionTip = false; 31 | 32 | new Vue({ 33 | router, 34 | store, 35 | vuetify, 36 | editor, 37 | render: h => h(App) 38 | }).$mount("#app"); 39 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/NoticeReadRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import javax.validation.constraints.NotNull; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import underdogs.devbie.notice.domain.JobPosition; 12 | import underdogs.devbie.notice.domain.Language; 13 | import underdogs.devbie.notice.domain.NoticeType; 14 | 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Builder 18 | @Getter 19 | @Setter 20 | public class NoticeReadRequest { 21 | 22 | @NotNull 23 | private NoticeType noticeType; 24 | private JobPosition jobPosition; 25 | private Language language; 26 | private String keyword; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/vo/LanguagePair.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.vo; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import underdogs.devbie.notice.domain.Language; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | @EqualsAndHashCode 17 | public class LanguagePair { 18 | 19 | private Map pair; 20 | 21 | public static LanguagePair from(Language language) { 22 | Map map = new HashMap<>(); 23 | 24 | map.put("key", language.name()); 25 | map.put("text", language.getText()); 26 | 27 | return new LanguagePair(map); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/frontend/src/store/modules/snackBar.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | snackbarState: false, 4 | snackbarText: "" 5 | }, 6 | mutations: { 7 | SET_SNACKBAR_STATE(state, snackbarState) { 8 | state.snackbarState = snackbarState; 9 | }, 10 | SET_SNACKBAR_TEXT(state, snackbarText) { 11 | state.snackbarText = snackbarText; 12 | } 13 | }, 14 | actions: { 15 | UPDATE_SNACKBAR_STATE({ commit }, snackbarState) { 16 | commit("SET_SNACKBAR_STATE", snackbarState); 17 | }, 18 | UPDATE_SNACKBAR_TEXT({ commit }, snackbarText) { 19 | commit("SET_SNACKBAR_STATE", true); 20 | commit("SET_SNACKBAR_TEXT", snackbarText); 21 | } 22 | }, 23 | getters: { 24 | fetchedSnackBarState(state) { 25 | return state.snackbarState; 26 | }, 27 | fetchedSnackBarText(state) { 28 | return state.snackbarText; 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/frontend/src/views/question/QuestionEditView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/domain/AnswerRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import static underdogs.devbie.answer.domain.QAnswer.*; 4 | 5 | import java.util.List; 6 | 7 | import com.querydsl.jpa.impl.JPAQueryFactory; 8 | 9 | public class AnswerRepositoryImpl implements AnswerRepositoryCustom { 10 | 11 | private final JPAQueryFactory jpaQueryFactory; 12 | 13 | public AnswerRepositoryImpl(JPAQueryFactory jpaQueryFactory) { 14 | this.jpaQueryFactory = jpaQueryFactory; 15 | } 16 | 17 | @Override 18 | public List findByQuestionIdOrderByRecommendationCount(Long questionId) { 19 | return jpaQueryFactory 20 | .selectFrom(answer) 21 | .where(answer.questionId.eq(questionId)) 22 | .orderBy(answer.recommendationCount.recommendedCount.desc()) 23 | .fetch(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/dto/UserInfoDto.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | import underdogs.devbie.user.RoleType; 9 | import underdogs.devbie.user.domain.User; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor 13 | @Getter 14 | @ToString 15 | public class UserInfoDto { 16 | 17 | private String id; 18 | private String email; 19 | private String login; 20 | private String avatar_url; 21 | 22 | public User toEntity() { 23 | return User.builder() 24 | .oauthId(id) 25 | .email(email) 26 | .name(login) 27 | .image(avatar_url) 28 | .roleType(RoleType.USER) 29 | .build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatNames.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 14 | @Getter 15 | public class ChatNames { 16 | 17 | private Set chatNames = new HashSet<>(); 18 | 19 | public static ChatNames from(Set chatNames) { 20 | return new ChatNames(chatNames); 21 | } 22 | 23 | public void add(ChatName chatName) { 24 | chatNames.add(chatName); 25 | } 26 | 27 | public Set getChatNames() { 28 | return Collections.unmodifiableSet(chatNames); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/TagNameTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class TagNameTest { 9 | 10 | public static final String TEST_HASHTAG_NAME = "test tagname"; 11 | 12 | @DisplayName("TagName - from") 13 | @Test 14 | void from() { 15 | TagName tagName = TagName.from(TEST_HASHTAG_NAME); 16 | 17 | assertThat(tagName.getName()).isEqualTo(TEST_HASHTAG_NAME); 18 | } 19 | 20 | @DisplayName("TagName name - equals") 21 | @Test 22 | void equals() { 23 | TagName tagName = TagName.from(TEST_HASHTAG_NAME); 24 | TagName anotherTagName = TagName.from(TEST_HASHTAG_NAME); 25 | 26 | assertThat(tagName).isEqualTo(anotherTagName); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/frontend/src/views/question/QuestionCreateView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/HashtagResponses.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | import underdogs.devbie.question.domain.Hashtag; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Builder 17 | @Getter 18 | @ToString 19 | public class HashtagResponses { 20 | 21 | private List hashtags; 22 | 23 | public static HashtagResponses from(List hashtags) { 24 | return new HashtagResponses(hashtags.stream() 25 | .map(HashtagResponse::from) 26 | .collect(Collectors.toList())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/vo/JobPositionPair.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.vo; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import underdogs.devbie.notice.domain.JobPosition; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | @EqualsAndHashCode 17 | public class JobPositionPair { 18 | 19 | private Map pair; 20 | 21 | public static JobPositionPair from(JobPosition jobPosition) { 22 | Map map = new HashMap<>(); 23 | 24 | map.put("key", jobPosition.name()); 25 | map.put("text", jobPosition.getText()); 26 | 27 | return new JobPositionPair(map); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/user/dto/UserUpdateInfoRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user.dto; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotBlank; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | import underdogs.devbie.user.domain.User; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Builder 17 | @Getter 18 | @ToString 19 | public class UserUpdateInfoRequest { 20 | 21 | @NotBlank 22 | private String name; 23 | 24 | @NotBlank 25 | @Email 26 | private String email; 27 | 28 | public User toEntity() { 29 | return User.builder() 30 | .name(name) 31 | .email(email) 32 | .build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/NoticeFavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | 10 | public interface NoticeFavoriteRepository extends 11 | JpaRepository, 12 | FavoriteRepository { 13 | 14 | @Override 15 | @Query("select n.noticeId from NoticeFavorite n where n.userId = :userId") 16 | List findAllByUserId(@Param("userId") Long userId); 17 | 18 | @Override 19 | @Query("select n from NoticeFavorite n where n.noticeId = :noticeId and n.userId = :userId") 20 | Optional findByObjectAndUserId(@Param("noticeId") Long noticeId, @Param("userId") Long userId); 21 | } 22 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/QuestionTitle.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.question.exception.QuestionNotMeetingEssentialsException; 12 | 13 | @Embeddable 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @Getter 17 | @ToString 18 | @EqualsAndHashCode 19 | public class QuestionTitle { 20 | 21 | private String title; 22 | 23 | public static QuestionTitle from(String title) { 24 | if (title.isEmpty()) { 25 | throw new QuestionNotMeetingEssentialsException(title + "이 빈값입니다."); 26 | } 27 | return new QuestionTitle(title); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/AnswerRecommendationRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import static underdogs.devbie.recommendation.domain.QAnswerRecommendation.*; 4 | 5 | import java.util.Optional; 6 | 7 | import com.querydsl.jpa.impl.JPAQueryFactory; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class AnswerRecommendationRepositoryImpl implements AnswerRecommendationRepositoryCustom { 12 | 13 | private final JPAQueryFactory jpaQueryFactory; 14 | 15 | @Override 16 | public Optional findByObjectAndUserId(Long answerId, Long userId) { 17 | return Optional.ofNullable(jpaQueryFactory 18 | .selectFrom(answerRecommendation) 19 | .where(answerRecommendation.answerId.eq(answerId), 20 | answerRecommendation.userId.eq(userId)) 21 | .fetchOne()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/dto/RecommendationResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.dto; 2 | 3 | import java.util.Optional; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.ToString; 10 | import underdogs.devbie.recommendation.domain.Recommendation; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 14 | @Getter 15 | @ToString 16 | public class RecommendationResponse { 17 | 18 | private String recommendationType; 19 | 20 | public static RecommendationResponse from(Optional recommendation) { 21 | String recommendationType = recommendation 22 | .map(r -> r.getRecommendationType().name()) 23 | .orElse("NOT_EXIST"); 24 | 25 | return new RecommendationResponse(recommendationType); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/frontend/src/views/favorite/NoticeFavoriteView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 42 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/QuestionFavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | 10 | public interface QuestionFavoriteRepository 11 | extends JpaRepository, FavoriteRepository { 12 | 13 | @Override 14 | @Query("select q.questionId from QuestionFavorite q where q.userId = :userId") 15 | List findAllByUserId(@Param("userId") Long userId); 16 | 17 | @Override 18 | @Query("select q from QuestionFavorite q where q.questionId = :questionId and q.userId = :userId") 19 | Optional findByObjectAndUserId(@Param("questionId") Long questionId, 20 | @Param("userId") Long userId); 21 | } 22 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/service/cache/NoticeKeyGenerator.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.service.cache; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.springframework.cache.interceptor.KeyGenerator; 6 | import org.springframework.data.domain.PageRequest; 7 | import org.springframework.stereotype.Component; 8 | 9 | import underdogs.devbie.notice.dto.NoticeReadRequest; 10 | import underdogs.devbie.notice.vo.NoticeCacheVo; 11 | 12 | @Component 13 | public class NoticeKeyGenerator implements KeyGenerator { 14 | 15 | @Override 16 | public Object generate(Object target, Method method, Object... params) { 17 | NoticeReadRequest noticeReadRequest = (NoticeReadRequest)params[0]; 18 | PageRequest pageRequest = (PageRequest)params[1]; 19 | 20 | NoticeCacheVo noticeCacheVo = new NoticeCacheVo(noticeReadRequest, pageRequest.getPageNumber()); 21 | 22 | return noticeCacheVo.hashCode(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/user/dto/UserResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.user.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.ToString; 8 | import underdogs.devbie.user.RoleType; 9 | import underdogs.devbie.user.domain.User; 10 | 11 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 12 | @Builder 13 | @Getter 14 | @ToString 15 | public class UserResponse { 16 | 17 | private Long id; 18 | private String email; 19 | private String image; 20 | private String name; 21 | private RoleType roleType; 22 | 23 | public static UserResponse from(User user) { 24 | return UserResponse.builder() 25 | .id(user.getId()) 26 | .email(user.getEmail()) 27 | .name(user.getName()) 28 | .image(user.getImage()) 29 | .roleType(user.getRoleType()) 30 | .build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/QuestionRecommendationRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import static underdogs.devbie.recommendation.domain.QQuestionRecommendation.*; 4 | 5 | import java.util.Optional; 6 | 7 | import com.querydsl.jpa.impl.JPAQueryFactory; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class QuestionRecommendationRepositoryImpl implements QuestionRecommendationRepositoryCustom { 12 | 13 | private final JPAQueryFactory jpaQueryFactory; 14 | 15 | @Override 16 | public Optional findByObjectAndUserId(Long questionId, Long userId) { 17 | return Optional.ofNullable(jpaQueryFactory 18 | .selectFrom(questionRecommendation) 19 | .where(questionRecommendation.questionId.eq(questionId), 20 | questionRecommendation.userId.eq(userId)) 21 | .fetchOne()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/ApplyUrl.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | 6 | import javax.persistence.Embeddable; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.exception.CreateFailException; 14 | 15 | @Embeddable 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Getter 18 | @ToString 19 | @EqualsAndHashCode 20 | public class ApplyUrl { 21 | 22 | private String applyUrl; 23 | 24 | public ApplyUrl(String url) { 25 | validate(url); 26 | this.applyUrl = url; 27 | } 28 | 29 | private void validate(String url) { 30 | try { 31 | new URL(url); 32 | } catch (MalformedURLException e) { 33 | throw new CreateFailException(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | 48 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/dto/AnswerResponses.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.dto; 2 | 3 | import static java.util.stream.Collectors.*; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import underdogs.devbie.answer.domain.Answers; 13 | import underdogs.devbie.user.domain.User; 14 | 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Getter 18 | public class AnswerResponses { 19 | 20 | private List answerResponses; 21 | 22 | public static AnswerResponses of(Answers answers, List users) { 23 | return IntStream.range(0, users.size()) 24 | .mapToObj(i -> AnswerResponse.of(answers.getAnswers().get(i), users.get(i))) 25 | .collect(collectingAndThen(toList(), AnswerResponses::new)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/Language.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import java.util.Arrays; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | import underdogs.devbie.notice.expception.NoSuchLanguageException; 10 | 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | @ToString 15 | public enum Language { 16 | 17 | C("C"), 18 | CPP("C++"), 19 | JAVA("JAVA"), 20 | C_SHARP("C#"), 21 | JAVASCRIPT("JAVASCRIPT"), 22 | TYPESCRIPT("TYPESCRIPT"), 23 | PYTHON("PYTHON"), 24 | RUBY("RUBY"), 25 | PHP("PHP"), 26 | SWIFT("SWIFT"); 27 | 28 | private String text; 29 | 30 | public static Language from(String input) { 31 | return Arrays.stream(values()) 32 | .filter(language -> language.text.equals(input)) 33 | .findFirst() 34 | .orElseThrow(NoSuchLanguageException::new); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/answer/domain/AnswerContentTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class AnswerContentTest { 9 | 10 | private final static String ANSWER_CONTENT = "answer content"; 11 | 12 | @DisplayName("AnswerContent - from") 13 | @Test 14 | void answerContentInitTest() { 15 | AnswerContent answerContent = AnswerContent.from(ANSWER_CONTENT); 16 | 17 | assertThat(answerContent.getContent()).isEqualTo(ANSWER_CONTENT); 18 | } 19 | 20 | @DisplayName("AnswerContent - equals") 21 | @Test 22 | void answerContentEqaulsTest() { 23 | AnswerContent answerContent = AnswerContent.from(ANSWER_CONTENT); 24 | AnswerContent anotherAnswerContent = AnswerContent.from(ANSWER_CONTENT); 25 | 26 | assertThat(answerContent).isEqualTo(anotherAnswerContent); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/frontend/src/components/SnackBar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/VisitsTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class VisitsTest { 9 | 10 | @DisplayName("Visits - 초기 Visits 생성") 11 | @Test 12 | void init() { 13 | Visits initialVisits = Visits.init(); 14 | 15 | assertThat(initialVisits.getVisitCount()).isEqualTo(0L); 16 | } 17 | 18 | @DisplayName("Visits - value 증가") 19 | @Test 20 | void increase() { 21 | Visits initialVisits = Visits.init(); 22 | initialVisits.increase(); 23 | 24 | assertThat(initialVisits.getVisitCount()).isEqualTo(1L); 25 | } 26 | 27 | @DisplayName("Visits - equals") 28 | @Test 29 | void testEquals() { 30 | Visits initialVisits = Visits.init(); 31 | Visits anotherVisits = Visits.init(); 32 | 33 | assertThat(initialVisits).isEqualTo(anotherVisits); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatList.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 44 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/HashtagsRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.Set; 4 | import java.util.stream.Collectors; 5 | 6 | import javax.validation.constraints.NotEmpty; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.ToString; 14 | import underdogs.devbie.question.domain.Hashtag; 15 | import underdogs.devbie.question.domain.TagName; 16 | 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 19 | @Builder 20 | @Getter 21 | @ToString 22 | public class HashtagsRequest { 23 | 24 | @NotEmpty 25 | private Set hashtags; 26 | 27 | public Set toEntity() { 28 | return hashtags.stream() 29 | .map(h -> Hashtag.builder() 30 | .tagName(TagName.from(h)) 31 | .build()) 32 | .collect(Collectors.toSet()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /devbie-app-api/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation 'org.springframework.boot:spring-boot-starter-web' 3 | implementation 'org.springframework.boot:spring-boot-starter-cache' 4 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 5 | compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' 6 | compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' 7 | compile "org.flywaydb:flyway-core" 8 | 9 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 10 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 11 | } 12 | 13 | testCompile group: 'io.rest-assured', name: 'rest-assured', version: '3.3.0' 14 | 15 | // todo 고민되는 것 16 | implementation 'io.jsonwebtoken:jjwt:0.9.1' 17 | implementation 'org.springframework.boot:spring-boot-starter-validation' 18 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // pageable을 서비스내로 이동시키면 될 듯하다. 19 | compile 'mysql:mysql-connector-java' 20 | } 21 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatNameTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ChatNameTest { 9 | 10 | @Test 11 | void ofByEnum() { 12 | ChatName chatName = ChatName.of(Adjective.찬란한, CrewName.동글, TitleColor.AMBER); 13 | 14 | assertAll( 15 | () -> assertThat(chatName).isNotNull(), 16 | () -> assertEquals(chatName.getChatName(), "찬란한 동글"), 17 | () -> assertEquals(chatName.getColor(), TitleColor.AMBER) 18 | ); 19 | } 20 | 21 | @Test 22 | void ofByString() { 23 | ChatName chatName = ChatName.of("찬란한 동글", TitleColor.AMBER); 24 | 25 | assertAll( 26 | () -> assertThat(chatName).isNotNull(), 27 | () -> assertEquals(chatName.getChatName(), "찬란한 동글"), 28 | () -> assertEquals(chatName.getColor(), TitleColor.AMBER) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/frontend/src/utils/noticeUtil.js: -------------------------------------------------------------------------------- 1 | const dict = { 2 | "C++": "CPP", 3 | JAVA: "JAVA", 4 | C: "C", 5 | "C#": "C_SHARP", 6 | JAVASCRIPT: "JAVASCRIPT", 7 | TYPESCRIPT: "TYPESCRIPT", 8 | PYTHON: "PYTHON", 9 | RUBY: "RUBY", 10 | PHP: "PHP", 11 | SWIFT: "SWIFT" 12 | }; 13 | 14 | export const languageTranslator = language => { 15 | return dict[language]; 16 | }; 17 | 18 | export const createNoticeUrl = (noticeType, keyword, language, jobPosition) => { 19 | const query = new URLSearchParams( 20 | createNoticeObj(noticeType, keyword, language, jobPosition) 21 | ); 22 | return `/notices?${query}`; 23 | }; 24 | 25 | export const createNoticeObj = (noticeType, keyword, language, jobPosition) => { 26 | return { 27 | noticeType: noticeType || "JOB", 28 | keyword: keyword || "", 29 | language: language || "", 30 | jobPosition: jobPosition || "" 31 | }; 32 | }; 33 | 34 | export const urlValidator = url => { 35 | try { 36 | new URL(url); 37 | return true; 38 | } catch (error) { 39 | return false; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/QuestionContent.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import javax.persistence.Lob; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.question.exception.QuestionNotMeetingEssentialsException; 14 | 15 | @Embeddable 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 18 | @Getter 19 | @ToString 20 | @EqualsAndHashCode 21 | public class QuestionContent { 22 | 23 | @Lob 24 | @Column(columnDefinition = "TEXT") 25 | private String content; 26 | 27 | public static QuestionContent from(String content) { 28 | if (content.isEmpty()) { 29 | throw new QuestionNotMeetingEssentialsException(content + "이 빈값입니다."); 30 | } 31 | return new QuestionContent(content); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/domain/Company.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Embeddable; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | import underdogs.devbie.exception.CreateFailException; 13 | 14 | @Embeddable 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @Getter 17 | @ToString 18 | @EqualsAndHashCode 19 | public class Company { 20 | 21 | private static final int SALARY_MINIMUM_VALUE = 0; 22 | 23 | private String name; 24 | 25 | public Company(String name) { 26 | validateParameters(name); 27 | this.name = name; 28 | } 29 | 30 | private void validateParameters(String name) throws CreateFailException { 31 | if (checkEmpty(name)) { 32 | throw new CreateFailException(); 33 | } 34 | } 35 | 36 | private boolean checkEmpty(String name) { 37 | return Objects.isNull(name) || name.trim().isEmpty(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/recommendation/domain/RecommendationTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class RecommendationTest { 9 | 10 | @DisplayName("추천 플래그가 같은지 확인") 11 | @Test 12 | void isMatchedRecommended() { 13 | QuestionRecommendation recommendation = 14 | QuestionRecommendation.of(1L, 1L, RecommendationType.RECOMMENDED); 15 | 16 | boolean result = recommendation.hasRecommendationTypeOf(RecommendationType.RECOMMENDED); 17 | 18 | assertThat(result).isTrue(); 19 | } 20 | 21 | @DisplayName("추천 플래그 토글") 22 | @Test 23 | void toggleIsRecommendedFlag() { 24 | QuestionRecommendation recommendation = 25 | QuestionRecommendation.of(1L, 1L, RecommendationType.RECOMMENDED); 26 | 27 | recommendation.toggleRecommended(); 28 | 29 | assertThat(recommendation.getRecommendationType()).isEqualTo(RecommendationType.NON_RECOMMENDED); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Devbie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/frontend/src/utils/validator.js: -------------------------------------------------------------------------------- 1 | import { urlValidator } from "@/utils/noticeUtil"; 2 | 3 | const validator = { 4 | member: { 5 | name: [v => !!v || "닉네임 입력이 필요합니다."], 6 | email: [ 7 | v => !!v || "이메일 입력이 필요합니다.", 8 | v => /.+@.+/.test(v) || "유효한 이메일을 입력해주세요" 9 | ] 10 | }, 11 | notice: { 12 | text: [ 13 | v => !!v || "텍스트 입력이 필요합니다.", 14 | v => v.trim() !== "" || "공백은 입력이 불가능합니다." 15 | ], 16 | selected: [v => !!v || "항목을 선택해주세요."], 17 | language: [ 18 | v => !!v || "항목을 선택해주세요.", 19 | v => v.length !== 0 || "1개이상 선택해주세요." 20 | ], 21 | url: [ 22 | v => !!v || "URL 입력이 필요합니다.", 23 | v => v.trim() !== "" || "공백은 입력이 불가능합니다.", 24 | v => !!urlValidator(v) || "옳바른 URL을 입력해야합니다." 25 | ] 26 | }, 27 | hashtag: { 28 | input: [ 29 | v => 30 | !( 31 | v.toString().length !== 0 && 32 | v 33 | .toString() 34 | .split(",") 35 | .map(value => value.trim()) 36 | .includes("") 37 | ) || "해시태그는 공백일 수 없습니다." 38 | ] 39 | } 40 | }; 41 | 42 | export default validator; 43 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/Favorite.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.MappedSuperclass; 9 | 10 | import lombok.AccessLevel; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import underdogs.devbie.common.BaseTimeEntity; 14 | import underdogs.devbie.exception.CreateFailException; 15 | 16 | @MappedSuperclass 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | public abstract class Favorite extends BaseTimeEntity { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | protected Long id; 24 | 25 | protected Long userId; 26 | 27 | public Favorite(Long userId) { 28 | validateParameters(userId); 29 | this.userId = userId; 30 | } 31 | 32 | private void validateParameters(Long userId) { 33 | if (Objects.isNull(userId)) { 34 | throw new CreateFailException(); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/favorite/domain/NoticeFavoriteTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class NoticeFavoriteTest { 11 | 12 | @DisplayName("NoticeFavorite 생성자 테스트 - noticeId 없을 때 예외 발생") 13 | @Test 14 | void noticeFavoriteInitialize_WithoutNoticeId() { 15 | assertThatThrownBy(() -> NoticeFavorite.of(null, 1L)) 16 | .isInstanceOf(CreateFailException.class); 17 | } 18 | 19 | @DisplayName("NoticeFavorite 생성자 테스트 - userId 없을 때 예외 발생") 20 | @Test 21 | void noticeFavoriteInitialize_WithoutUserId() { 22 | assertThatThrownBy(() -> NoticeFavorite.of(1L, null)) 23 | .isInstanceOf(CreateFailException.class); 24 | } 25 | 26 | @DisplayName("NoticeFavorite 생성자 테스트") 27 | @Test 28 | void noticeFavoriteInitialize() { 29 | assertThat(NoticeFavorite.of(1L, 1L)) 30 | .isInstanceOf(NoticeFavorite.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatInput.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/TitleColor.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import java.util.Arrays; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | import underdogs.devbie.chat.exception.NoSuchTitleColorException; 10 | 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | @ToString 15 | public enum TitleColor { 16 | 17 | RED_ORANGE("#F44336"), 18 | RUBY("#E91E63"), 19 | DARK_ORCHID("#9C27B0"), 20 | PURPLE_HEART("#673AB7"), 21 | FREE_SPEECH_BLUE("#3F51B5"), 22 | DODGER_BLUE("#2196F3"), 23 | DART_CYAN("#009688"), 24 | FRUIT_SALAD("#4CAF50"), 25 | AMBER("#FFC107"), 26 | ORANGE_PEEL("#FF9800"), 27 | BAROSSA("#4D2F40"), 28 | IRONSTONE("#795548"), 29 | NOBEL("#9E9E9E"); 30 | 31 | private String color; 32 | 33 | public static TitleColor from(String input) { 34 | return Arrays.stream(values()) 35 | .filter(color -> color.color.equals(input)) 36 | .findFirst() 37 | .orElseThrow(NoSuchTitleColorException::new); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/frontend/src/components/notice/NoticeSearch.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/AuthConfig.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | import lombok.RequiredArgsConstructor; 10 | import underdogs.devbie.auth.interceptor.BearerAuthInterceptor; 11 | import underdogs.devbie.auth.resolver.LoginUserArgumentResolver; 12 | 13 | @Configuration 14 | @RequiredArgsConstructor 15 | public class AuthConfig implements WebMvcConfigurer { 16 | 17 | private final BearerAuthInterceptor bearerAuthInterceptor; 18 | private final LoginUserArgumentResolver loginUserArgumentResolver; 19 | 20 | @Override 21 | public void addInterceptors(InterceptorRegistry registry) { 22 | registry.addInterceptor(bearerAuthInterceptor) 23 | .addPathPatterns("/api/**"); 24 | } 25 | 26 | @Override 27 | public void addArgumentResolvers(List argumentResolvers) { 28 | argumentResolvers.add(loginUserArgumentResolver); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/CustomPageRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import java.util.Objects; 4 | 5 | import org.springframework.data.domain.PageRequest; 6 | import org.springframework.data.domain.Sort; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor 16 | @Getter 17 | @ToString 18 | public final class CustomPageRequest { 19 | 20 | private static final int FIRST_PAGE = 1; 21 | private static final int DEFAULT_SIZE = 10; 22 | 23 | private Integer page; 24 | 25 | public void setPage(Integer page) { 26 | this.page = Math.max(page, 1); 27 | } 28 | 29 | public PageRequest toPageRequest() { 30 | setDefaultValue(); 31 | return PageRequest.of(page - 1, DEFAULT_SIZE, Sort.by(Sort.Direction.DESC, "createdDate")); 32 | } 33 | 34 | private void setDefaultValue() { 35 | if (Objects.isNull(page)) { 36 | this.page = FIRST_PAGE; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/service/FavoriteService.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.service; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import underdogs.devbie.exception.NotExistException; 9 | import underdogs.devbie.favorite.domain.Favorite; 10 | import underdogs.devbie.favorite.domain.FavoriteRepository; 11 | 12 | @Service 13 | @Transactional(readOnly = true) 14 | public abstract class FavoriteService { 15 | 16 | protected FavoriteRepository favoriteRepository; 17 | 18 | public abstract Object findFavorites(Long userId); 19 | 20 | @Transactional 21 | public abstract void createFavorite(Long objectId, Long userId); 22 | 23 | @Transactional 24 | public void deleteFavorite(Long objectId, Long userId) { 25 | Optional optionalFavorite = favoriteRepository.findByObjectAndUserId(objectId, userId); 26 | Favorite favorite = optionalFavorite.orElseThrow(() -> new NotExistException("Favorite")); 27 | 28 | favoriteRepository.delete(favorite); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/favorite/domain/QuestionFavoriteTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class QuestionFavoriteTest { 11 | 12 | @DisplayName("QuestionFavorite 생성자 테스트 - questionId 없을 때 예외 발생") 13 | @Test 14 | void questionFavoriteInitialize_WithoutQuestionId() { 15 | assertThatThrownBy(() -> QuestionFavorite.of(null, 1L)) 16 | .isInstanceOf(CreateFailException.class); 17 | } 18 | 19 | @DisplayName("QuestionFavorite 생성자 테스트 - userId 없을 때 예외 발생") 20 | @Test 21 | void questionFavoriteInitialize_WithoutUserId() { 22 | assertThatThrownBy(() -> QuestionFavorite.of(1L, null)) 23 | .isInstanceOf(CreateFailException.class); 24 | } 25 | 26 | @DisplayName("QuestionFavorite 생성자 테스트") 27 | @Test 28 | void questionFavoriteInitialize() { 29 | assertThat(QuestionFavorite.of(1L, 1L)) 30 | .isInstanceOf(QuestionFavorite.class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/QuestionResponses.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | import underdogs.devbie.question.domain.Question; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | @ToString 17 | public class QuestionResponses { 18 | 19 | private List questions; 20 | private int lastPage; 21 | 22 | public static QuestionResponses of(List questions, int lastPage) { 23 | return new QuestionResponses(questions.stream() 24 | .map(QuestionResponse::from) 25 | .collect(Collectors.toList()), lastPage); 26 | } 27 | 28 | public static QuestionResponses from(List questions) { 29 | return new QuestionResponses(questions.stream() 30 | .map(QuestionResponse::from) 31 | .collect(Collectors.toList()), 1000); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/config/ChatConfig.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class ChatConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/chat") 16 | .setAllowedOrigins("*") 17 | .withSockJS() 18 | .setClientLibraryUrl("https://cdn.jsdelivr.net/npm/sockjs-client@1.5.0/dist/sockjs.min.js"); 19 | } 20 | 21 | @Override 22 | public void configureMessageBroker(MessageBrokerRegistry registry) { 23 | registry.enableSimpleBroker("/channel"); 24 | registry.setApplicationDestinationPrefixes("/send"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/frontend/src/views/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 40 | 41 | 52 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/dto/AnswerCreateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import underdogs.devbie.answer.domain.Answer; 11 | import underdogs.devbie.answer.domain.AnswerContent; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | public class AnswerCreateRequest { 17 | 18 | @NotNull(message = "Question ID가 존재하지 않습니다.") 19 | private Long questionId; 20 | 21 | @NotBlank(message = "본문이 비어있습니다.") 22 | private String content; 23 | 24 | public static AnswerCreateRequest of(Long questionId, String content) { 25 | return new AnswerCreateRequest(questionId, content); 26 | } 27 | 28 | public Answer toEntity(Long userId) { 29 | return Answer.builder() 30 | .userId(userId) 31 | .questionId(questionId) 32 | .content(AnswerContent.from(content)) 33 | .build(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/chat/domain/ChatNameFactoryTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ChatNameFactoryTest { 12 | 13 | @DisplayName("중복되지 않은 ChatName 반환") 14 | @Test 15 | void createNonOverlappingName() { 16 | Set chatNames = new HashSet<>(); 17 | chatNames.add(ChatName.of(Adjective.찬란한, CrewName.동글, TitleColor.AMBER)); 18 | ChatName actual = ChatNameFactory.createNonOverlappingName(ChatNames.from(chatNames)); 19 | 20 | assertThat(actual).isNotNull(); 21 | assertThat(actual.getChatName()).isNotEqualTo("찬란한 동글"); 22 | } 23 | 24 | @DisplayName("존재하는 모든 ChatName이 존재할 때 IndexOutOfBoundsException 발생") 25 | @Test 26 | void createNonOverlappingNameWhenExistAllChatName() { 27 | Set chatNames = ChatNameFactory.getNames(); 28 | assertThatThrownBy(() -> ChatNameFactory.createNonOverlappingName(ChatNames.from(chatNames))) 29 | .isInstanceOf(IndexOutOfBoundsException.class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/NoticeFavorite.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.Index; 7 | import javax.persistence.Table; 8 | 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.exception.CreateFailException; 14 | 15 | @Entity 16 | @Table(indexes = @Index(name = "i_notice_favorite", columnList = "userId, noticeId")) 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | @ToString 20 | public class NoticeFavorite extends Favorite { 21 | 22 | private Long noticeId; 23 | 24 | private NoticeFavorite(Long userId, Long noticeId) { 25 | super(userId); 26 | this.noticeId = noticeId; 27 | } 28 | 29 | public static NoticeFavorite of(Long noticeId, Long userId) { 30 | validateParameters(noticeId); 31 | return new NoticeFavorite(userId, noticeId); 32 | } 33 | 34 | private static void validateParameters(Long noticeId) { 35 | if (Objects.isNull(noticeId)) { 36 | throw new CreateFailException(); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/Chat.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | import javax.persistence.JoinColumn; 8 | import javax.persistence.ManyToOne; 9 | 10 | import lombok.AccessLevel; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Entity 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @AllArgsConstructor 19 | @Builder 20 | @Getter 21 | public class Chat { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | private Long id; 26 | 27 | private ChatName name; 28 | 29 | private ChatMessage message; 30 | 31 | @ManyToOne 32 | @JoinColumn(name = "chatroom_id") 33 | private ChatRoom chatRoom; 34 | 35 | public static Chat of(String name, TitleColor titleColor, String message, ChatRoom chatRoom) { 36 | return Chat.builder() 37 | .name(ChatName.of(name, titleColor)) 38 | .message(ChatMessage.from(message)) 39 | .chatRoom(chatRoom) 40 | .build(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/service/RecommendationService.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.service; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import underdogs.devbie.recommendation.domain.Recommendation; 8 | import underdogs.devbie.recommendation.domain.RecommendationRepository; 9 | import underdogs.devbie.recommendation.domain.RecommendationType; 10 | import underdogs.devbie.recommendation.dto.RecommendationResponse; 11 | 12 | @Transactional(readOnly = true) 13 | public abstract class RecommendationService { 14 | 15 | protected RecommendationRepository recommendationRepository; 16 | 17 | public RecommendationResponse search(Long objectId, Long userId) { 18 | Optional optRecommendation = recommendationRepository.findByObjectAndUserId(objectId, userId); 19 | 20 | return RecommendationResponse.from(optRecommendation); 21 | } 22 | 23 | @Transactional 24 | public abstract void deleteRecommendation(Long objectId, Long userId); 25 | 26 | @Transactional 27 | public abstract void createOrUpdateRecommendation(Long objectId, Long userId, RecommendationType recommendationType); 28 | } 29 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/HashtagTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static underdogs.devbie.question.domain.TagNameTest.*; 5 | 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import underdogs.devbie.exception.CreateFailException; 10 | 11 | class HashtagTest { 12 | 13 | @DisplayName("Hashtag 빌더 테스트 - tagName이 없을 때 예외 발생") 14 | @Test 15 | void hashtagBuilderWithoutTagName() { 16 | assertThatThrownBy(() -> Hashtag.builder() 17 | .build()) 18 | .isInstanceOf(CreateFailException.class); 19 | } 20 | 21 | @DisplayName("Hashtag 내용 수정") 22 | @Test 23 | void updateHashtag() { 24 | Hashtag hashtag = Hashtag.builder() 25 | .id(100L) 26 | .tagName(TagName.from(TEST_HASHTAG_NAME)) 27 | .build(); 28 | Hashtag updateHashtag = Hashtag.builder() 29 | .id(100L) 30 | .tagName(TagName.from("changed name")) 31 | .build(); 32 | 33 | hashtag.update(updateHashtag); 34 | 35 | assertThat(hashtag.getTagName().getName()).isEqualTo(updateHashtag.getTagName().getName()); 36 | } 37 | } -------------------------------------------------------------------------------- /src/frontend/src/store/modules/loginUser.js: -------------------------------------------------------------------------------- 1 | import { getAction, patchAction } from "../../api"; 2 | 3 | export default { 4 | state: { 5 | loginUser: [] 6 | }, 7 | mutations: { 8 | SET_LOGIN_USER(state, data) { 9 | state.loginUser = data; 10 | }, 11 | DELETE_LOGIN_USER(state) { 12 | state.loginUser = []; 13 | } 14 | }, 15 | actions: { 16 | async FETCH_LOGIN_USER({ commit }) { 17 | const { data } = await getAction("/api/users"); 18 | commit("SET_LOGIN_USER", data); 19 | return data; 20 | }, 21 | async UPDATE_USER_INFO(state, payload) { 22 | try { 23 | await patchAction(`/api/users/me`, payload); 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }, 28 | async UPDATE_USER_IMAGE(state, payload) { 29 | try { 30 | await patchAction( 31 | `/api/users/me/image`, 32 | payload, 33 | `content-type: multipart/form-data` 34 | ); 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | } 39 | }, 40 | getters: { 41 | fetchedLoginUser(state) { 42 | return state.loginUser; 43 | }, 44 | isLoggedIn(state) { 45 | return state.loginUser.length !== 0; 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/domain/QuestionFavorite.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.Index; 7 | import javax.persistence.Table; 8 | 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.exception.CreateFailException; 14 | 15 | @Entity 16 | @Table(indexes = @Index(name = "i_question_favorite", columnList = "userId, questionId")) 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | @ToString 20 | public class QuestionFavorite extends Favorite { 21 | 22 | private Long questionId; 23 | 24 | public QuestionFavorite(Long userId, Long questionId) { 25 | super(userId); 26 | this.questionId = questionId; 27 | } 28 | 29 | public static QuestionFavorite of(Long questionId, Long userId) { 30 | validateParameters(questionId); 31 | return new QuestionFavorite(userId, questionId); 32 | } 33 | 34 | private static void validateParameters(Long questionId) { 35 | if (Objects.isNull(questionId)) { 36 | throw new CreateFailException(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatRoom.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Index; 11 | import javax.persistence.OneToMany; 12 | import javax.persistence.Table; 13 | 14 | import lombok.AccessLevel; 15 | import lombok.AllArgsConstructor; 16 | import lombok.Builder; 17 | import lombok.Getter; 18 | import lombok.NoArgsConstructor; 19 | 20 | @Entity 21 | @Table(indexes = @Index(name = "i_chat_room", columnList = "noticeId")) 22 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 23 | @AllArgsConstructor 24 | @Builder 25 | @Getter 26 | public class ChatRoom { 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | private Long id; 31 | 32 | private Long noticeId; 33 | 34 | @OneToMany(mappedBy = "chatRoom") 35 | private List chats = new ArrayList<>(); 36 | 37 | public static ChatRoom from(Long noticeId) { 38 | return ChatRoom.builder() 39 | .noticeId(noticeId) 40 | .chats(new ArrayList<>()) 41 | .build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/src/components/question/QuestionFilters.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 59 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/auth/AuthController.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PostMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import lombok.RequiredArgsConstructor; 11 | import underdogs.devbie.auth.dto.JwtTokenResponse; 12 | import underdogs.devbie.auth.service.AuthService; 13 | 14 | @RestController 15 | @RequestMapping("/api/auth") 16 | @RequiredArgsConstructor 17 | public class AuthController { 18 | 19 | private final AuthService authService; 20 | 21 | @NoValidate 22 | @GetMapping("/login-url") 23 | public ResponseEntity fetchLoginUrl() { 24 | return ResponseEntity.ok(authService.fetchLoginUrl()); 25 | } 26 | 27 | @NoValidate 28 | @PostMapping("/login") 29 | public ResponseEntity login(@RequestParam(value = "code") String code) { 30 | JwtTokenResponse jwtTokenResponse = authService.createToken(code); 31 | 32 | return ResponseEntity.ok(jwtTokenResponse); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | // 1. HTTP Request & Response와 관련된 기본 설정 4 | const config = { 5 | baseUrl: "" 6 | }; 7 | 8 | const devbieToken = `bearer ${localStorage.getItem("devbieToken")}`; 9 | 10 | function getAction(url) { 11 | return axios.get(`${config.baseUrl}${url}`, { 12 | headers: { 13 | Authorization: devbieToken 14 | } 15 | }); 16 | } 17 | 18 | function postAction(url, request, configOption) { 19 | return axios.post(`${config.baseUrl}${url}`, request, { 20 | headers: { 21 | Authorization: devbieToken, 22 | ...configOption 23 | } 24 | }); 25 | } 26 | 27 | function putAction(url, request) { 28 | return axios.put(`${config.baseUrl}${url}`, request, { 29 | headers: { 30 | Authorization: devbieToken 31 | } 32 | }); 33 | } 34 | 35 | function patchAction(url, request, configOption) { 36 | return axios.patch(`${config.baseUrl}${url}`, request, { 37 | headers: { 38 | Authorization: devbieToken, 39 | ...configOption 40 | } 41 | }); 42 | } 43 | 44 | function deleteAction(url) { 45 | return axios.delete(`${config.baseUrl}${url}`, { 46 | headers: { 47 | Authorization: devbieToken 48 | } 49 | }); 50 | } 51 | 52 | export { getAction, postAction, putAction, patchAction, deleteAction }; 53 | -------------------------------------------------------------------------------- /devbie-core/build.gradle: -------------------------------------------------------------------------------- 1 | bootJar { enabled = false } 2 | jar { enabled = true } 3 | 4 | dependencies { 5 | compile("com.querydsl:querydsl-jpa") // querydsl 6 | compile("com.querydsl:querydsl-apt") // querydsl 7 | 8 | implementation 'org.springframework.boot:spring-boot-starter-web' 9 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 10 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 11 | 12 | // 고민되는 것들 13 | 14 | implementation('org.springframework.boot:spring-boot-starter-validation') 15 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 16 | implementation 'io.jsonwebtoken:jjwt:0.9.1' 17 | } 18 | 19 | // querydsl 적용 20 | apply plugin: "com.ewerk.gradle.plugins.querydsl" // Plugin 적용 21 | def querydslSrcDir = "$buildDir/generated/querydsl" // QClass 생성 위치 22 | 23 | querydsl { 24 | library = "com.querydsl:querydsl-apt" 25 | jpa = true 26 | querydslSourcesDir = querydslSrcDir 27 | } 28 | 29 | sourceSets { 30 | main { 31 | java { 32 | srcDirs = ['src/main/java', querydslSrcDir] 33 | } 34 | } 35 | } 36 | 37 | compileQuerydsl { 38 | options.annotationProcessorPath = configurations.querydsl 39 | } 40 | 41 | configurations { 42 | querydsl.extendsFrom compileClasspath 43 | } -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/QuestionPageRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.Objects; 4 | 5 | import org.springframework.data.domain.PageRequest; 6 | import org.springframework.data.domain.Sort; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.Setter; 13 | import lombok.ToString; 14 | import underdogs.devbie.question.domain.OrderBy; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @AllArgsConstructor 18 | @Setter 19 | @Getter 20 | @ToString 21 | public final class QuestionPageRequest { 22 | 23 | private static final int FIRST_PAGE = 1; 24 | private static final int DEFAULT_SIZE = 10; 25 | 26 | private Integer page; 27 | private OrderBy orderBy; 28 | 29 | public void setPage(Integer page) { 30 | this.page = Math.max(page, 1); 31 | } 32 | 33 | public PageRequest toPageRequest() { 34 | setDefaultValue(); 35 | return PageRequest.of(page - 1, DEFAULT_SIZE, Sort.by(orderBy.getDirection(), orderBy.getPropertyName(), "createdDate")); 36 | } 37 | 38 | private void setDefaultValue() { 39 | if (Objects.isNull(page)) { 40 | this.page = FIRST_PAGE; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 13 | devbie 14 | 21 | 22 | 23 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/answer/dto/AnswerResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.answer.dto; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import underdogs.devbie.answer.domain.Answer; 9 | import underdogs.devbie.user.domain.User; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 13 | @Builder 14 | @Getter 15 | public class AnswerResponse { 16 | 17 | private Long id; 18 | private Long userId; 19 | private String author; 20 | private Long questionId; 21 | private String content; 22 | private Long recommendedCount; 23 | private Long nonRecommendedCount; 24 | 25 | public static AnswerResponse of(Answer answer, User author) { 26 | return AnswerResponse.builder() 27 | .id(answer.getId()) 28 | .userId(answer.getUserId()) 29 | .author(author.getName()) 30 | .questionId(answer.getQuestionId()) 31 | .content(answer.getContent().getContent()) 32 | .recommendedCount(answer.getRecommendationCount().getRecommendedCount()) 33 | .nonRecommendedCount(answer.getRecommendationCount().getNonRecommendedCount()) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/auth/service/AuthService.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import lombok.RequiredArgsConstructor; 6 | import underdogs.devbie.auth.dto.JwtTokenResponse; 7 | import underdogs.devbie.auth.dto.UserInfoDto; 8 | import underdogs.devbie.auth.dto.UserTokenDto; 9 | import underdogs.devbie.auth.jwt.JwtTokenProvider; 10 | import underdogs.devbie.auth.oauth.GithubClient; 11 | import underdogs.devbie.user.domain.User; 12 | import underdogs.devbie.user.service.UserService; 13 | 14 | @Service 15 | @RequiredArgsConstructor 16 | public class AuthService { 17 | 18 | private final GithubClient githubClient; 19 | private final JwtTokenProvider jwtTokenProvider; 20 | private final UserService userService; 21 | 22 | public String fetchLoginUrl() { 23 | return githubClient.fetchLoginUrl(); 24 | } 25 | 26 | public JwtTokenResponse createToken(String code) { 27 | String accessToken = githubClient.fetchAccessToken(code); 28 | 29 | UserInfoDto userInfoDto = githubClient.fetchUserInfo(accessToken); 30 | 31 | User user = userService.saveOrUpdateUser(userInfoDto); 32 | 33 | String token = jwtTokenProvider.createToken(UserTokenDto.from(user)); 34 | return JwtTokenResponse.from(token); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/QuestionCreateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.Set; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.ToString; 14 | import underdogs.devbie.question.domain.Question; 15 | import underdogs.devbie.question.domain.QuestionContent; 16 | import underdogs.devbie.question.domain.QuestionHashtags; 17 | import underdogs.devbie.question.domain.QuestionTitle; 18 | 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 21 | @Builder 22 | @Getter 23 | @ToString 24 | public class QuestionCreateRequest { 25 | 26 | @NotBlank 27 | private String title; 28 | 29 | @NotBlank 30 | private String content; 31 | 32 | private Set hashtags; 33 | 34 | public Question toEntity(Long userId) { 35 | return Question.builder() 36 | .userId(userId) 37 | .title(QuestionTitle.from(title)) 38 | .content(QuestionContent.from(content)) 39 | .hashtags(QuestionHashtags.from(new LinkedHashSet<>())) 40 | .build(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/dto/QuestionUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.dto; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.Set; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.ToString; 14 | import underdogs.devbie.question.domain.Question; 15 | import underdogs.devbie.question.domain.QuestionContent; 16 | import underdogs.devbie.question.domain.QuestionHashtags; 17 | import underdogs.devbie.question.domain.QuestionTitle; 18 | 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 21 | @Builder 22 | @Getter 23 | @ToString 24 | public class QuestionUpdateRequest { 25 | 26 | @NotBlank 27 | private String title; 28 | 29 | @NotBlank 30 | private String content; 31 | 32 | private Set hashtags; 33 | 34 | public Question toEntity(Long userId) { 35 | return Question.builder() 36 | .userId(userId) 37 | .title(QuestionTitle.from(title)) 38 | .content(QuestionContent.from(content)) 39 | .hashtags(QuestionHashtags.from(new LinkedHashSet<>())) 40 | .build(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/RecommendationCount.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.ToString; 11 | 12 | @Embeddable 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 15 | @Getter 16 | @ToString 17 | @EqualsAndHashCode 18 | public class RecommendationCount { 19 | 20 | private Long recommendedCount; 21 | private Long nonRecommendedCount; 22 | 23 | public static RecommendationCount init() { 24 | return new RecommendationCount(0L, 0L); 25 | } 26 | 27 | public void increaseRecommendationCount(RecommendationType recommendationType) { 28 | if (recommendationType.is(RecommendationType.RECOMMENDED)) { 29 | recommendedCount ++; 30 | return; 31 | } 32 | nonRecommendedCount ++; 33 | } 34 | 35 | public void decreaseRecommendationCount(RecommendationType recommendationType) { 36 | if (recommendationType.is(RecommendationType.RECOMMENDED)) { 37 | recommendedCount --; 38 | return; 39 | } 40 | nonRecommendedCount --; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/src/components/question/AnswerList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | 38 | 62 | -------------------------------------------------------------------------------- /src/frontend/src/views/favorite/QuestionFavoriteView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 63 | -------------------------------------------------------------------------------- /src/frontend/src/components/FooterBar.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 35 | 36 | 49 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/AnswerRecommendation.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.Index; 7 | import javax.persistence.Table; 8 | 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.exception.CreateFailException; 14 | 15 | @Entity 16 | @Table(indexes = @Index(name = "i_answer_recommendation", columnList = "userId, answerId")) 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | @ToString 20 | public class AnswerRecommendation extends Recommendation { 21 | 22 | private Long answerId; 23 | 24 | private AnswerRecommendation(Long answerId, Long userId, RecommendationType recommendationType) { 25 | super(userId, recommendationType); 26 | this.answerId = answerId; 27 | } 28 | 29 | public static AnswerRecommendation of(Long answerId, Long userId, RecommendationType recommendationType) { 30 | validateParameters(answerId); 31 | return new AnswerRecommendation(answerId, userId, recommendationType); 32 | } 33 | 34 | private static void validateParameters(Long answerId) { 35 | if (Objects.isNull(answerId)) { 36 | throw new CreateFailException(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/auth/interceptor/utils/InterceptorValidator.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.interceptor.utils; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.method.HandlerMethod; 8 | 9 | import underdogs.devbie.auth.NoValidate; 10 | import underdogs.devbie.auth.Role; 11 | import underdogs.devbie.auth.exception.NotExistUserRoleException; 12 | import underdogs.devbie.user.RoleType; 13 | 14 | @Component 15 | public class InterceptorValidator { 16 | 17 | public boolean isNotValid(Object handler) { 18 | NoValidate noValidate = ((HandlerMethod)handler).getMethodAnnotation(NoValidate.class); 19 | return Objects.nonNull(noValidate); 20 | } 21 | 22 | public void validateRole(Object handler, String role) { 23 | Role methodAnnotation = ((HandlerMethod)handler).getMethodAnnotation(Role.class); 24 | RoleType roleType = RoleType.valueOf(role); 25 | 26 | if (Objects.isNull(methodAnnotation)) { 27 | return; 28 | } 29 | 30 | validateUserRole(methodAnnotation, roleType); 31 | } 32 | 33 | private void validateUserRole(Role methodAnnotation, RoleType roleType) { 34 | if (!Arrays.asList(methodAnnotation.role()) 35 | .contains(roleType)) { 36 | throw new NotExistUserRoleException(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /devbie-app-api/src/test/resources/notice_create.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO notice(name, image, job_position, title, notice_type, content, recruitment_type) VALUES 2 | ('bossdog2', 'https://cdn.vuetifyjs.com/images/cards/road.jpg', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 3 | ,('bossdog4', 'https://cdn.vuetifyjs.com/images/cards/house.jpg', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 4 | ,('bossdog3', 'test', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 5 | ,('bossdog1', 'https://cdn.vuetifyjs.com/images/cards/plane.jpg', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 6 | ,('bossdog5', 'https://cdn.vuetifyjs.com/images/cards/plane.jpg', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 7 | ,('bossdog', 'https://cdn.vuetifyjs.com/images/cards/house.jpg', 'BACKEND', 'title', 'JOB', 'hi!', 'ANY') 8 | ,('bossdog6', 'https://cdn.vuetifyjs.com/images/cards/road.jpg', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 9 | ,('bossdog7', 'test', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 10 | ,('yeji1', 'test', 'BACKEND', 'title', 'JOB','hi!', 'ANY') 11 | ,('bossdog6', 'https://cdn.vuetifyjs.com/images/cards/road.jpg', 'FRONTEND', 'title', 'JOB','hi!', 'ANY') 12 | ,('bossdog7', 'test', 'FRONTEND', 'title', 'JOB','hi!', 'ANY') 13 | ,('yeji1', 'test', 'FRONTEND', 'title', 'JOB','hi!', 'ANY'); 14 | 15 | INSERT INTO language(notice_id, languages) values 16 | (1, 'CPP') 17 | ,(1, 'JAVA') 18 | ,(2, 'CPP') 19 | ,(3, 'CPP') 20 | ,(4, 'CPP') 21 | ,(5, 'CPP') 22 | ,(6, 'CPP') 23 | ,(7, 'CPP') 24 | ,(8, 'CPP'); 25 | -------------------------------------------------------------------------------- /devbie-aws-client/src/main/java/underdogs/devbie/s3/S3Service.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.s3; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.annotation.PostConstruct; 6 | 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import com.amazonaws.services.s3.AmazonS3; 12 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 13 | import com.amazonaws.services.s3.model.CannedAccessControlList; 14 | import com.amazonaws.services.s3.model.PutObjectRequest; 15 | 16 | @Service 17 | public class S3Service { 18 | 19 | private AmazonS3 s3Client; 20 | 21 | @Value("${cloud.aws.region.static:sample}") 22 | private String region; 23 | 24 | @Value("${cloud.aws.s3.bucket:sample}") 25 | private String bucket; 26 | 27 | @PostConstruct 28 | public void setS3Client() { 29 | s3Client = AmazonS3ClientBuilder.standard() 30 | .withRegion(this.region) 31 | .build(); 32 | } 33 | 34 | public String upload(MultipartFile file) throws IOException { 35 | String fileName = file.getOriginalFilename(); 36 | 37 | s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null) 38 | .withCannedAcl(CannedAccessControlList.PublicRead)); 39 | return s3Client.getUrl(bucket, fileName).toString(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/NoticeDescriptionResponse.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import java.util.Objects; 4 | import java.util.Set; 5 | import java.util.stream.Collectors; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.notice.domain.Language; 14 | import underdogs.devbie.notice.domain.NoticeDescription; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 18 | @Builder 19 | @Getter 20 | @ToString 21 | public class NoticeDescriptionResponse { 22 | 23 | private Set languages; 24 | private String content; 25 | private String applyUrl; 26 | 27 | public static NoticeDescriptionResponse from(NoticeDescription noticeDescription) { 28 | Set languages = noticeDescription.getLanguages() 29 | .stream() 30 | .map(Language::getText) 31 | .collect(Collectors.toSet()); 32 | 33 | String applyUrl = ""; 34 | if (Objects.nonNull(noticeDescription.getApplyUrl())) { 35 | applyUrl = noticeDescription.getApplyUrl().getApplyUrl(); 36 | } 37 | 38 | return new NoticeDescriptionResponse(languages, noticeDescription.getContent(), 39 | applyUrl); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/recommendation/domain/QuestionRecommendation.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.Index; 7 | import javax.persistence.Table; 8 | 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import lombok.ToString; 13 | import underdogs.devbie.exception.CreateFailException; 14 | 15 | @Entity 16 | @Table(indexes = @Index(name = "i_question_recommendation", columnList = "userId, questionId")) 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @Getter 19 | @ToString 20 | public class QuestionRecommendation extends Recommendation { 21 | 22 | private Long questionId; 23 | 24 | private QuestionRecommendation(Long questionId, Long userId, RecommendationType recommendationType) { 25 | super(userId, recommendationType); 26 | this.questionId = questionId; 27 | } 28 | 29 | public static QuestionRecommendation of(Long questionId, Long userId, RecommendationType recommendationType) { 30 | validateParameters(questionId); 31 | return new QuestionRecommendation(questionId, userId, recommendationType); 32 | } 33 | 34 | private static void validateParameters(Long questionId) { 35 | if (Objects.isNull(questionId)) { 36 | throw new CreateFailException(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/notice/dto/FilterResponses.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.dto; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import underdogs.devbie.notice.domain.JobPosition; 12 | import underdogs.devbie.notice.domain.Language; 13 | import underdogs.devbie.notice.vo.JobPositionPair; 14 | import underdogs.devbie.notice.vo.LanguagePair; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 18 | @Getter 19 | public class FilterResponses { 20 | 21 | private static final FilterResponses filterResponses; 22 | 23 | static { 24 | List languages = Arrays.stream(Language.values()) 25 | .map(LanguagePair::from) 26 | .collect(Collectors.toList()); 27 | 28 | List jobPositions = Arrays.stream(JobPosition.values()) 29 | .map(JobPositionPair::from) 30 | .collect(Collectors.toList()); 31 | 32 | filterResponses = new FilterResponses(languages, jobPositions); 33 | } 34 | 35 | private List languages; 36 | private List jobPositions; 37 | 38 | public static FilterResponses get() { 39 | return filterResponses; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/auth/interceptor/utils/AuthorizationExtractor.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.interceptor.utils; 2 | 3 | import java.util.Enumeration; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | import org.apache.logging.log4j.util.Strings; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class AuthorizationExtractor { 12 | public static final String AUTHORIZATION = "Authorization"; 13 | public static final String ACCESS_TOKEN_TYPE = 14 | AuthorizationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; 15 | 16 | public String extract(HttpServletRequest request, String type) { 17 | Enumeration headers = request.getHeaders(AUTHORIZATION); 18 | while (headers.hasMoreElements()) { 19 | String value = headers.nextElement(); 20 | if ((value.toLowerCase().startsWith(type.toLowerCase()))) { 21 | String authHeaderValue = value.substring(type.length()).trim(); 22 | request.setAttribute(ACCESS_TOKEN_TYPE, value.substring(0, type.length()).trim()); 23 | int commaIndex = authHeaderValue.indexOf(','); 24 | if (commaIndex > 0) { 25 | authHeaderValue = authHeaderValue.substring(0, commaIndex); 26 | } 27 | return authHeaderValue; 28 | } 29 | } 30 | return Strings.EMPTY; 31 | } 32 | } -------------------------------------------------------------------------------- /src/frontend/src/components/favorite/FavoriteType.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 62 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/QuestionTitleTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.question.exception.QuestionNotMeetingEssentialsException; 9 | 10 | public class QuestionTitleTest { 11 | 12 | public static final String TEST_QUESTION_TITLE = "Test Question Title"; 13 | 14 | @DisplayName("Question Title - from") 15 | @Test 16 | void from() { 17 | QuestionTitle questionTitle = QuestionTitle.from(TEST_QUESTION_TITLE); 18 | 19 | assertThat(questionTitle.getTitle()).isEqualTo(TEST_QUESTION_TITLE); 20 | } 21 | 22 | @DisplayName("Question Title - throw QuestionNotMeetingEssentialsException") 23 | @Test 24 | void fromThrownQuestionNotMeetingEssentialsException() { 25 | assertThatThrownBy(() -> { 26 | QuestionTitle questionTitle = QuestionTitle.from(""); 27 | }).isInstanceOf(QuestionNotMeetingEssentialsException.class); 28 | } 29 | 30 | @DisplayName("Question Title - eqauls") 31 | @Test 32 | void equals() { 33 | QuestionTitle questionTitle = QuestionTitle.from(TEST_QUESTION_TITLE); 34 | QuestionTitle anotherQuestionTitle = QuestionTitle.from(TEST_QUESTION_TITLE); 35 | 36 | assertThat(questionTitle).isEqualTo(anotherQuestionTitle); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatInfo.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 35 | 60 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/resources/log4j2.yml: -------------------------------------------------------------------------------- 1 | Configutation: 2 | name: Default 3 | status: warn 4 | 5 | Properties: 6 | Property: 7 | name: log-path 8 | value: "logs" 9 | 10 | Appenders: 11 | Console: 12 | name: Console_Appender 13 | target: SYSTEM_OUT 14 | PatternLayout: 15 | pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" 16 | File: 17 | name: File_Appender 18 | fileName: ${log-path}/logfile.log 19 | PatternLayout: 20 | pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" 21 | RollingFile: 22 | - name: RollingFile_Appender 23 | fileName: ${log-path}/rollingfile.log 24 | filePattern: "logs/archive/rollingfile.log.%d{yyyy-MM-dd-hh-mm}.gz" 25 | PatternLayout: 26 | pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" 27 | Policies: 28 | SizeBasedTriggeringPolicy: 29 | size: 1 KB 30 | DefaultRollOverStrategy: 31 | max: 30 32 | Loggers: 33 | Root: 34 | level: info 35 | AppenderRef: 36 | - ref: Console_Appender 37 | - ref: File_Appender 38 | - ref: RollingFile_Appender 39 | Logger: 40 | - name: underdogs.devbie 41 | additivity: false 42 | level: debug 43 | AppenderRef: 44 | - ref: Console_Appender 45 | - ref: File_Appender 46 | - ref: RollingFile_Appender 47 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/QuestionContentTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.question.exception.QuestionNotMeetingEssentialsException; 9 | 10 | public class QuestionContentTest { 11 | 12 | public static final String TEST_QUESTION_CONTENT = "Test Content"; 13 | 14 | @DisplayName("QuestionContent - from") 15 | @Test 16 | void from() { 17 | QuestionContent questionContent = QuestionContent.from(TEST_QUESTION_CONTENT); 18 | 19 | assertThat(questionContent.getContent()).isEqualTo(TEST_QUESTION_CONTENT); 20 | } 21 | 22 | @DisplayName("Question Content - throw QuestionNotMeetingEssentialsException") 23 | @Test 24 | void fromThrownQuestionNotMeetingEssentialsException() { 25 | assertThatThrownBy(() -> { 26 | QuestionContent questionContent = QuestionContent.from(""); 27 | }).isInstanceOf(QuestionNotMeetingEssentialsException.class); 28 | } 29 | 30 | @DisplayName("QuestionContent - equals") 31 | @Test 32 | void equals() { 33 | QuestionContent questionContent = QuestionContent.from(TEST_QUESTION_CONTENT); 34 | QuestionContent anotherQuestionContent = QuestionContent.from(TEST_QUESTION_CONTENT); 35 | 36 | assertThat(questionContent).isEqualTo(anotherQuestionContent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/src/components/question/QuestionControl.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 40 | 41 | 70 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/Hashtag.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Embedded; 6 | import javax.persistence.Entity; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.Index; 11 | import javax.persistence.Table; 12 | 13 | import lombok.AccessLevel; 14 | import lombok.Builder; 15 | import lombok.Getter; 16 | import lombok.NoArgsConstructor; 17 | import lombok.ToString; 18 | import underdogs.devbie.common.BaseTimeEntity; 19 | import underdogs.devbie.exception.CreateFailException; 20 | 21 | @Entity 22 | @Table(indexes = @Index(name = "i_hashtag", columnList = "name")) 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | @Getter 25 | @ToString 26 | public class Hashtag extends BaseTimeEntity { 27 | 28 | @Id 29 | @GeneratedValue(strategy = GenerationType.IDENTITY) 30 | private Long id; 31 | 32 | @Embedded 33 | private TagName tagName; 34 | 35 | @Builder 36 | public Hashtag(Long id, TagName tagName) { 37 | validateParameters(tagName); 38 | this.id = id; 39 | this.tagName = tagName; 40 | } 41 | 42 | private void validateParameters(TagName tagName) { 43 | if (Objects.isNull(tagName)) { 44 | throw new CreateFailException(); 45 | } 46 | } 47 | 48 | public void update(Hashtag hashtag) { 49 | this.tagName = hashtag.getTagName(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Gradle documentation](https://docs.gradle.org) 7 | * [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/gradle-plugin/reference/html/) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/gradle-plugin/reference/html/#build-image) 9 | * [Spring Web](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications) 10 | * [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-jpa-and-spring-data) 11 | * [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#using-boot-devtools) 12 | 13 | ### Guides 14 | The following guides illustrate how to use some features concretely: 15 | 16 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 17 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 18 | * [Building REST services with Spring](https://spring.io/guides/tutorials/bookmarks/) 19 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 20 | * [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) 21 | 22 | ### Additional Links 23 | These additional references should also help you: 24 | 25 | * [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) 26 | 27 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/repository/QuestionHashtagRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain.repository; 2 | 3 | import static underdogs.devbie.question.domain.QQuestionHashtag.*; 4 | 5 | import java.util.List; 6 | 7 | import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; 8 | 9 | import com.querydsl.jpa.impl.JPAQueryFactory; 10 | import underdogs.devbie.question.domain.QuestionHashtag; 11 | 12 | public class QuestionHashtagRepositoryImpl extends QuerydslRepositorySupport 13 | implements QuestionHashtagRepositoryCustom { 14 | 15 | private final JPAQueryFactory jpaQueryFactory; 16 | 17 | public QuestionHashtagRepositoryImpl(JPAQueryFactory jpaQueryFactory) { 18 | super(QuestionHashtag.class); 19 | this.jpaQueryFactory = jpaQueryFactory; 20 | } 21 | 22 | @Override 23 | public void deleteAllByHashtagIds(List hashtagIds, Long questionId) { 24 | jpaQueryFactory 25 | .delete(questionHashtag) 26 | .where(questionHashtag.hashtag.id.in(hashtagIds), 27 | questionHashtag.question.id.eq(questionId)) 28 | .execute(); 29 | } 30 | 31 | @Override 32 | public List findQuestionIdsByHashtagId(Long hashtagId) { 33 | return jpaQueryFactory 34 | .select(questionHashtag.question.id) 35 | .from(questionHashtag) 36 | .where(questionHashtag.hashtag.id.eq(hashtagId)) 37 | .fetchResults() 38 | .getResults(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/chat/domain/ChatName.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.chat.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Embeddable; 6 | import javax.persistence.EnumType; 7 | import javax.persistence.Enumerated; 8 | 9 | import lombok.AccessLevel; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Embeddable 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 17 | @Getter 18 | public class ChatName { 19 | 20 | private String chatName; 21 | 22 | @Enumerated(value = EnumType.STRING) 23 | private TitleColor color; 24 | 25 | public static ChatName of(Adjective adjective, CrewName crewName, TitleColor titleColor) { 26 | return new ChatName(String.format("%s %s", adjective.name(), crewName.name()), titleColor); 27 | } 28 | 29 | public static ChatName of(String name, TitleColor titleColor) { 30 | return new ChatName(name, titleColor); 31 | } 32 | 33 | public void setColor(TitleColor color) { 34 | this.color = color; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) 40 | return true; 41 | if (o == null || getClass() != o.getClass()) 42 | return false; 43 | ChatName chatName1 = (ChatName)o; 44 | return Objects.equals(chatName, chatName1.chatName); 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | return Objects.hash(chatName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/auth/interceptor/BearerAuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.auth.interceptor; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | 9 | import io.jsonwebtoken.Claims; 10 | import lombok.RequiredArgsConstructor; 11 | import underdogs.devbie.auth.interceptor.utils.AuthorizationExtractor; 12 | import underdogs.devbie.auth.interceptor.utils.InterceptorValidator; 13 | import underdogs.devbie.auth.jwt.JwtTokenProvider; 14 | 15 | @Component 16 | @RequiredArgsConstructor 17 | public class BearerAuthInterceptor implements HandlerInterceptor { 18 | 19 | private final AuthorizationExtractor authExtractor; 20 | private final JwtTokenProvider jwtTokenProvider; 21 | private final InterceptorValidator interceptorValidator; 22 | 23 | @Override 24 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 25 | if (interceptorValidator.isNotValid(handler)) { 26 | return true; 27 | } 28 | 29 | String token = authExtractor.extract(request, "bearer"); 30 | Claims claims = jwtTokenProvider.extractValidSubject(token); 31 | String userId = claims.get("userId").toString(); 32 | String role = claims.get("role").toString(); 33 | 34 | interceptorValidator.validateRole(handler, role); 35 | 36 | request.setAttribute("userId", userId); 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /devbie-app-api/src/main/java/underdogs/devbie/aop/ServiceLoggingAspect.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.aop; 2 | 3 | import java.util.Arrays; 4 | import java.util.Date; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.annotation.Around; 10 | import org.aspectj.lang.annotation.Aspect; 11 | import org.aspectj.lang.annotation.Pointcut; 12 | import org.springframework.stereotype.Component; 13 | 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | @Component 17 | @Aspect 18 | @Slf4j 19 | public class ServiceLoggingAspect { 20 | 21 | @Pointcut("execution(* underdogs.devbie..*Service.*(..))") 22 | public void loggerPointCut() { 23 | } 24 | 25 | @Around("loggerPointCut()") 26 | public Object methodLogger(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 27 | Object result = proceedingJoinPoint.proceed(); 28 | 29 | String serviceName = proceedingJoinPoint.getSignature().getDeclaringType().getSimpleName(); 30 | String methodName = proceedingJoinPoint.getSignature().getName(); 31 | 32 | Map params = new HashMap<>(); 33 | 34 | try { 35 | params.put("service", serviceName); 36 | params.put("method", methodName); 37 | params.put("params", Arrays.toString(proceedingJoinPoint.getArgs())); 38 | params.put("log_time", new Date()); 39 | } catch (Exception e) { 40 | log.error("LoggingAspect error", e); 41 | } 42 | 43 | log.info("params : {}", params); 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/question/domain/QuestionHashtagTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | import static underdogs.devbie.question.domain.QuestionTest.*; 5 | import static underdogs.devbie.question.domain.TagNameTest.*; 6 | 7 | import java.util.LinkedHashSet; 8 | 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import underdogs.devbie.exception.CreateFailException; 13 | 14 | class QuestionHashtagTest { 15 | 16 | @DisplayName("QuestionHashtag 빌더 테스트 - Question이 없을 경우 예외 발생") 17 | @Test 18 | void questionHashtagBuilderWithoutQuestion() { 19 | Hashtag hashTag = Hashtag.builder() 20 | .tagName(TagName.from(TEST_HASHTAG_NAME)) 21 | .build(); 22 | 23 | assertThatThrownBy(() -> QuestionHashtag.builder() 24 | .hashtag(hashTag) 25 | .build()) 26 | .isInstanceOf(CreateFailException.class); 27 | } 28 | 29 | @DisplayName("QuestionHashtag 빌더 테스트 - Hashtag가 없을 경우 예외 발생") 30 | @Test 31 | void questionHashtagBuilderWithoutHashtag() { 32 | Question question = Question.builder() 33 | .userId(1L) 34 | .title(TEST_QUESTION_TITLE) 35 | .content(TEST_QUESTION_CONTENT) 36 | .hashtags(QuestionHashtags.from(new LinkedHashSet<>())) 37 | .build(); 38 | 39 | assertThatThrownBy(() -> QuestionHashtag.builder() 40 | .question(question) 41 | .build()) 42 | .isInstanceOf(CreateFailException.class); 43 | } 44 | } -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/service/NoticeFavoriteService.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.service; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.stereotype.Service; 7 | 8 | import underdogs.devbie.exception.BadRequestException; 9 | import underdogs.devbie.favorite.domain.Favorite; 10 | import underdogs.devbie.favorite.domain.NoticeFavorite; 11 | import underdogs.devbie.favorite.domain.NoticeFavoriteRepository; 12 | import underdogs.devbie.notice.dto.NoticeResponses; 13 | import underdogs.devbie.notice.service.NoticeService; 14 | 15 | @Service 16 | public class NoticeFavoriteService extends FavoriteService { 17 | 18 | private NoticeService noticeService; 19 | 20 | public NoticeFavoriteService(NoticeFavoriteRepository noticeFavoriteRepository, 21 | NoticeService noticeService) { 22 | this.favoriteRepository = noticeFavoriteRepository; 23 | this.noticeService = noticeService; 24 | } 25 | 26 | @Override 27 | public NoticeResponses findFavorites(Long userId) { 28 | List noticeIds = favoriteRepository.findAllByUserId(userId); 29 | return noticeService.findAllByIds(noticeIds); 30 | } 31 | 32 | @Override 33 | public void createFavorite(Long objectId, Long userId) { 34 | Optional optionalFavorite = favoriteRepository.findByObjectAndUserId(objectId, userId); 35 | if (optionalFavorite.isPresent()) { 36 | throw new BadRequestException("이미 즐겨찾기 추가된 항목입니다."); 37 | } 38 | favoriteRepository.save(NoticeFavorite.of(objectId, userId)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/recommendation/domain/AnswerRecommendationTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class AnswerRecommendationTest { 11 | 12 | @DisplayName("AnswerRecommendation 생성 테스트 - userId가 없을 때 예외 발생") 13 | @Test 14 | void recommendationInitializeWithoutUserId() { 15 | assertThatThrownBy(() -> 16 | AnswerRecommendation.of(1L, null, RecommendationType.RECOMMENDED) 17 | ).isInstanceOf(CreateFailException.class); 18 | } 19 | 20 | @DisplayName("AnswerRecommendation 생성 테스트 - recommendationType 없을 때 예외 발생") 21 | @Test 22 | void recommendationInitializeWithoutRecommendationType() { 23 | assertThatThrownBy(() -> 24 | AnswerRecommendation.of(1L, 1L, null) 25 | ).isInstanceOf(CreateFailException.class); 26 | } 27 | 28 | @DisplayName("AnswerRecommendation 생성 테스트 - answerId가 없을 때 예외 발생") 29 | @Test 30 | void recommendationInitializeWithoutAnswerId() { 31 | assertThatThrownBy(() -> 32 | AnswerRecommendation.of(null, 1L, RecommendationType.RECOMMENDED) 33 | ).isInstanceOf(CreateFailException.class); 34 | } 35 | 36 | @DisplayName("AnswerRecommendation 생성 테스트") 37 | @Test 38 | void recommendationInitialize() { 39 | assertThat(AnswerRecommendation.of(1L, 1L, RecommendationType.RECOMMENDED)) 40 | .isInstanceOf(AnswerRecommendation.class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/notice/domain/DurationTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.notice.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import java.time.LocalDate; 6 | 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import underdogs.devbie.exception.CreateFailException; 11 | 12 | class DurationTest { 13 | 14 | @DisplayName("Duration 생성 테스트 - 공개채용시 날짜가 null인 경우") 15 | @Test 16 | void constructorInvalidNullData() { 17 | assertThatThrownBy(() -> 18 | new Duration( 19 | RecruitmentType.OPEN, 20 | null, null 21 | )) 22 | .isInstanceOf(CreateFailException.class); 23 | } 24 | 25 | @DisplayName("Duration 생성 테스트 - 공개채용시 마감일자가 모집일자가 빠른 경우") 26 | @Test 27 | void constructorInvalidDate() { 28 | assertThatThrownBy(() -> 29 | new Duration( 30 | RecruitmentType.OPEN, 31 | LocalDate.now(), LocalDate.now().minusDays(1) 32 | )) 33 | .isInstanceOf(CreateFailException.class); 34 | } 35 | 36 | @DisplayName("Duration 비교 연산 테스트") 37 | @Test 38 | void equals() { 39 | Duration duration1 = new Duration( 40 | RecruitmentType.OPEN, 41 | LocalDate.of(2020, 5, 5), 42 | LocalDate.of(2020, 5, 10) 43 | ); 44 | Duration duration2 = new Duration( 45 | RecruitmentType.OPEN, 46 | LocalDate.of(2020, 5, 5), 47 | LocalDate.of(2020, 5, 10) 48 | ); 49 | 50 | assertThat(duration1).isEqualTo(duration2); 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/question/domain/QuestionHashtag.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.question.domain; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.Index; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | import javax.persistence.Table; 13 | 14 | import lombok.AccessLevel; 15 | import lombok.Builder; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import underdogs.devbie.exception.CreateFailException; 19 | 20 | @Entity 21 | @Table(name = "question_hashtag", 22 | indexes = @Index(name = "i_question_hashtag", columnList = "hashtag_id, question_id")) 23 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 24 | @Getter 25 | public class QuestionHashtag { 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private Long id; 30 | 31 | @ManyToOne 32 | @JoinColumn(name = "question_id") 33 | private Question question; 34 | 35 | @ManyToOne 36 | @JoinColumn(name = "hashtag_id") 37 | private Hashtag hashtag; 38 | 39 | @Builder 40 | public QuestionHashtag(Long id, Question question, Hashtag hashtag) { 41 | validateParameters(question, hashtag); 42 | this.question = question; 43 | this.hashtag = hashtag; 44 | } 45 | 46 | private void validateParameters(Question question, Hashtag hashtag) { 47 | if (Objects.isNull(question) || Objects.isNull(hashtag)) { 48 | throw new CreateFailException(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/frontend/src/components/chat/ChatRoomHistoryBox.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 49 | 50 | 57 | -------------------------------------------------------------------------------- /devbie-core/src/main/java/underdogs/devbie/favorite/service/QuestionFavoriteService.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.favorite.service; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.stereotype.Service; 7 | 8 | import underdogs.devbie.exception.BadRequestException; 9 | import underdogs.devbie.favorite.domain.Favorite; 10 | import underdogs.devbie.favorite.domain.QuestionFavorite; 11 | import underdogs.devbie.favorite.domain.QuestionFavoriteRepository; 12 | import underdogs.devbie.question.dto.QuestionResponses; 13 | import underdogs.devbie.question.service.QuestionService; 14 | 15 | @Service 16 | public class QuestionFavoriteService extends FavoriteService { 17 | 18 | private QuestionService questionService; 19 | 20 | public QuestionFavoriteService(QuestionFavoriteRepository questionFavoriteRepository, 21 | QuestionService questionService) { 22 | this.favoriteRepository = questionFavoriteRepository; 23 | this.questionService = questionService; 24 | } 25 | 26 | @Override 27 | public QuestionResponses findFavorites(Long userId) { 28 | List questionIds = favoriteRepository.findAllByUserId(userId); 29 | return questionService.findAllByIds(questionIds); 30 | } 31 | 32 | @Override 33 | public void createFavorite(Long objectId, Long userId) { 34 | Optional optionalFavorite = favoriteRepository.findByObjectAndUserId(objectId, userId); 35 | if (optionalFavorite.isPresent()) { 36 | throw new BadRequestException("이미 즐겨찾기 추가된 항목입니다."); 37 | } 38 | favoriteRepository.save(QuestionFavorite.of(objectId, userId)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devbie-core/src/test/java/underdogs/devbie/recommendation/domain/QuestionRecommendationTest.java: -------------------------------------------------------------------------------- 1 | package underdogs.devbie.recommendation.domain; 2 | 3 | import static org.assertj.core.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import underdogs.devbie.exception.CreateFailException; 9 | 10 | class QuestionRecommendationTest { 11 | 12 | @DisplayName("QuestionRecommendation 생성 테스트 - userId가 없을 때 예외 발생") 13 | @Test 14 | void recommendationInitializeWithoutUserId() { 15 | assertThatThrownBy(() -> 16 | QuestionRecommendation.of(1L, null, RecommendationType.RECOMMENDED) 17 | ).isInstanceOf(CreateFailException.class); 18 | } 19 | 20 | @DisplayName("QuestionRecommendation 생성 테스트 - recommendationType 없을 때 예외 발생") 21 | @Test 22 | void recommendationInitializeWithoutRecommendationType() { 23 | assertThatThrownBy(() -> 24 | QuestionRecommendation.of(1L, 1L, null) 25 | ).isInstanceOf(CreateFailException.class); 26 | } 27 | 28 | @DisplayName("QuestionRecommendation 생성 테스트 - questionId가 없을 때 예외 발생") 29 | @Test 30 | void questionRecommendationInitializeWithoutQuestionId() { 31 | assertThatThrownBy(() -> 32 | QuestionRecommendation.of(null, 1L, RecommendationType.RECOMMENDED) 33 | ).isInstanceOf(CreateFailException.class); 34 | } 35 | 36 | @DisplayName("QuestionRecommendation 생성 테스트") 37 | @Test 38 | void recommendationInitialize() { 39 | assertThat(QuestionRecommendation.of(1L, 1L, RecommendationType.RECOMMENDED)) 40 | .isInstanceOf(QuestionRecommendation.class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/src/components/notice/NoticeType.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | 46 | 74 | --------------------------------------------------------------------------------