├── backend
├── README.md
├── settings.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── woowacourse
│ │ │ │ └── moamoa
│ │ │ │ ├── study
│ │ │ │ ├── domain
│ │ │ │ │ ├── MemberRole.java
│ │ │ │ │ ├── RecruitStatus.java
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── InvalidPeriodException.java
│ │ │ │ │ │ └── NotParticipatedMemberException.java
│ │ │ │ │ ├── repository
│ │ │ │ │ │ ├── JpaStudyRepository.java
│ │ │ │ │ │ └── StudyRepository.java
│ │ │ │ │ ├── StudyStatus.java
│ │ │ │ │ └── AttachedTag.java
│ │ │ │ ├── service
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── FailureParticipationException.java
│ │ │ │ │ │ ├── InvalidUpdatingException.java
│ │ │ │ │ │ ├── FailureKickOutException.java
│ │ │ │ │ │ ├── OwnerCanNotLeaveException.java
│ │ │ │ │ │ └── StudyNotFoundException.java
│ │ │ │ │ └── response
│ │ │ │ │ │ ├── MyRoleResponse.java
│ │ │ │ │ │ ├── MyStudiesResponse.java
│ │ │ │ │ │ └── StudyResponse.java
│ │ │ │ ├── query
│ │ │ │ │ └── data
│ │ │ │ │ │ ├── StudySummaryData.java
│ │ │ │ │ │ ├── StudyOwnerAndTagsData.java
│ │ │ │ │ │ ├── MyStudySummaryData.java
│ │ │ │ │ │ └── StudyDetailsData.java
│ │ │ │ └── schedule
│ │ │ │ │ └── AutoCloseEnrollmentTask.java
│ │ │ │ ├── tag
│ │ │ │ ├── domain
│ │ │ │ │ ├── CategoryName.java
│ │ │ │ │ └── Category.java
│ │ │ │ ├── exception
│ │ │ │ │ └── TagNotExistException.java
│ │ │ │ ├── query
│ │ │ │ │ ├── response
│ │ │ │ │ │ ├── CategoryData.java
│ │ │ │ │ │ ├── TagData.java
│ │ │ │ │ │ └── TagSummaryData.java
│ │ │ │ │ └── request
│ │ │ │ │ │ └── CategoryIdRequest.java
│ │ │ │ ├── service
│ │ │ │ │ ├── response
│ │ │ │ │ │ └── TagsResponse.java
│ │ │ │ │ └── SearchingTagService.java
│ │ │ │ └── controller
│ │ │ │ │ └── converter
│ │ │ │ │ └── CategoryIdConverter.java
│ │ │ │ ├── common
│ │ │ │ ├── exception
│ │ │ │ │ ├── NotFoundException.java
│ │ │ │ │ ├── BadRequestException.java
│ │ │ │ │ ├── InvalidFormatException.java
│ │ │ │ │ └── UnauthorizedException.java
│ │ │ │ ├── config
│ │ │ │ │ ├── JpaAuditingConfig.java
│ │ │ │ │ └── SchedulerConfig.java
│ │ │ │ ├── utils
│ │ │ │ │ └── DateTimeSystem.java
│ │ │ │ ├── advice
│ │ │ │ │ └── response
│ │ │ │ │ │ └── ErrorResponse.java
│ │ │ │ └── entity
│ │ │ │ │ ├── BaseEntity.java
│ │ │ │ │ └── ReadOnlyCollectionPersister.java
│ │ │ │ ├── auth
│ │ │ │ ├── service
│ │ │ │ │ ├── response
│ │ │ │ │ │ ├── TokenResponse.java
│ │ │ │ │ │ └── AccessTokenResponse.java
│ │ │ │ │ ├── oauthclient
│ │ │ │ │ │ ├── OAuthClient.java
│ │ │ │ │ │ └── response
│ │ │ │ │ │ │ └── GithubProfileResponse.java
│ │ │ │ │ └── request
│ │ │ │ │ │ └── AccessTokenRequest.java
│ │ │ │ ├── exception
│ │ │ │ │ ├── TokenExpirationException.java
│ │ │ │ │ └── TokenNotFoundException.java
│ │ │ │ ├── config
│ │ │ │ │ └── AuthenticatedMemberId.java
│ │ │ │ ├── infrastructure
│ │ │ │ │ └── TokenProvider.java
│ │ │ │ └── controller
│ │ │ │ │ └── interceptor
│ │ │ │ │ └── PathRequestMatcher.java
│ │ │ │ ├── member
│ │ │ │ ├── domain
│ │ │ │ │ └── repository
│ │ │ │ │ │ ├── JpaMemberRepository.java
│ │ │ │ │ │ └── MemberRepository.java
│ │ │ │ ├── service
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── MemberNotFoundException.java
│ │ │ │ │ │ └── NotParticipatedMemberException.java
│ │ │ │ │ └── response
│ │ │ │ │ │ └── MemberResponse.java
│ │ │ │ ├── query
│ │ │ │ │ └── data
│ │ │ │ │ │ ├── MemberData.java
│ │ │ │ │ │ ├── OwnerData.java
│ │ │ │ │ │ └── ParticipatingMemberData.java
│ │ │ │ └── controller
│ │ │ │ │ └── MemberController.java
│ │ │ │ ├── comment
│ │ │ │ ├── domain
│ │ │ │ │ ├── repository
│ │ │ │ │ │ ├── JpaCommentRepository.java
│ │ │ │ │ │ └── CommentRepository.java
│ │ │ │ │ ├── Author.java
│ │ │ │ │ └── AssociatedCommunity.java
│ │ │ │ ├── service
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── CommentNotFoundException.java
│ │ │ │ │ │ ├── UnEditingCommentException.java
│ │ │ │ │ │ └── UnWrittenCommentException.java
│ │ │ │ │ ├── request
│ │ │ │ │ │ ├── EditingCommentRequest.java
│ │ │ │ │ │ └── CommentRequest.java
│ │ │ │ │ └── response
│ │ │ │ │ │ ├── AuthorResponse.java
│ │ │ │ │ │ ├── CommentResponse.java
│ │ │ │ │ │ └── CommentsResponse.java
│ │ │ │ └── query
│ │ │ │ │ └── data
│ │ │ │ │ └── CommentData.java
│ │ │ │ ├── studyroom
│ │ │ │ ├── domain
│ │ │ │ │ ├── article
│ │ │ │ │ │ └── repository
│ │ │ │ │ │ │ ├── ArticleRepository.java
│ │ │ │ │ │ │ └── TempArticleRepository.java
│ │ │ │ │ ├── link
│ │ │ │ │ │ └── repository
│ │ │ │ │ │ │ └── LinkArticleRepository.java
│ │ │ │ │ ├── studyroom
│ │ │ │ │ │ └── repository
│ │ │ │ │ │ │ ├── StudyRoomRepository.java
│ │ │ │ │ │ │ └── JpaStudyRoomRepository.java
│ │ │ │ │ ├── review
│ │ │ │ │ │ ├── repository
│ │ │ │ │ │ │ ├── JpaReviewRepository.java
│ │ │ │ │ │ │ └── ReviewRepository.java
│ │ │ │ │ │ ├── Reviewer.java
│ │ │ │ │ │ └── AssociatedStudy.java
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── UnwritableException.java
│ │ │ │ │ │ └── UneditableException.java
│ │ │ │ │ └── Accessor.java
│ │ │ │ ├── service
│ │ │ │ │ ├── exception
│ │ │ │ │ │ ├── ReviewNotFoundException.java
│ │ │ │ │ │ ├── ArticleNotFoundException.java
│ │ │ │ │ │ ├── UnViewableException.java
│ │ │ │ │ │ └── TempArticleNotFoundException.java
│ │ │ │ │ ├── request
│ │ │ │ │ │ ├── ReviewRequest.java
│ │ │ │ │ │ ├── SizeRequest.java
│ │ │ │ │ │ ├── LinkArticleRequest.java
│ │ │ │ │ │ └── ArticleRequest.java
│ │ │ │ │ └── response
│ │ │ │ │ │ ├── CreatedArticleIdResponse.java
│ │ │ │ │ │ ├── temp
│ │ │ │ │ │ ├── CreatedTempArticleIdResponse.java
│ │ │ │ │ │ └── StudyResponse.java
│ │ │ │ │ │ ├── AuthorResponse.java
│ │ │ │ │ │ ├── WriterResponse.java
│ │ │ │ │ │ ├── ArticleSummariesResponse.java
│ │ │ │ │ │ ├── LinksResponse.java
│ │ │ │ │ │ ├── ArticleResponse.java
│ │ │ │ │ │ ├── ReviewResponse.java
│ │ │ │ │ │ └── ReviewsResponse.java
│ │ │ │ ├── query
│ │ │ │ │ └── data
│ │ │ │ │ │ ├── StudyData.java
│ │ │ │ │ │ ├── ReviewData.java
│ │ │ │ │ │ ├── LinkArticleData.java
│ │ │ │ │ │ ├── TempArticleData.java
│ │ │ │ │ │ └── ArticleData.java
│ │ │ │ └── controller
│ │ │ │ │ └── converter
│ │ │ │ │ ├── SizeRequestConverter.java
│ │ │ │ │ └── ArticleTypeConverter.java
│ │ │ │ ├── alarm
│ │ │ │ ├── SlackUserProfile.java
│ │ │ │ ├── request
│ │ │ │ │ └── SlackMessageRequest.java
│ │ │ │ ├── response
│ │ │ │ │ ├── SlackUsersResponse.java
│ │ │ │ │ └── SlackUserResponse.java
│ │ │ │ └── AsyncConfig.java
│ │ │ │ └── MoamoaApplication.java
│ │ └── resources
│ │ │ └── application.yml
│ └── test
│ │ ├── java
│ │ └── com
│ │ │ └── woowacourse
│ │ │ ├── concurrent
│ │ │ └── HttpRequestExecutor.java
│ │ │ ├── acceptance
│ │ │ ├── document
│ │ │ │ ├── Document.java
│ │ │ │ └── StudyDocument.java
│ │ │ ├── TestConfig.java
│ │ │ ├── test
│ │ │ │ └── cors
│ │ │ │ │ └── CorsAcceptanceTest.java
│ │ │ ├── steps
│ │ │ │ └── SetRequiredDataToCreatingStudySteps.java
│ │ │ └── fixture
│ │ │ │ └── TagFixtures.java
│ │ │ └── moamoa
│ │ │ ├── fixtures
│ │ │ └── TagFixtures.java
│ │ │ ├── auth
│ │ │ └── config
│ │ │ │ └── AuthenticationExtractorTest.java
│ │ │ └── study
│ │ │ └── webmvc
│ │ │ └── UnauthorizedMyStudyWebMvcTest.java
│ │ └── resources
│ │ └── application.yml
└── .gitignore
├── .gitattributes
├── frontend
├── .storybook
│ └── preview-body.html
├── src
│ ├── utils
│ │ ├── noop.ts
│ │ ├── hasOwnProperty.ts
│ │ ├── getRandomInt.ts
│ │ ├── index.ts
│ │ ├── nLineEllipsis.ts
│ │ ├── isThemeFontSize.ts
│ │ └── arrayOfAll.ts
│ ├── assets
│ │ └── images
│ │ │ ├── logo.png
│ │ │ ├── rocket-cursor.png
│ │ │ ├── no-image-found.png
│ │ │ ├── sth-went-wrong.png
│ │ │ ├── moamoa-site-image.png
│ │ │ └── rocket-cursor-pointer.png
│ ├── mocks
│ │ ├── browser.ts
│ │ ├── handlers
│ │ │ ├── tagHandlers.ts
│ │ │ └── memberHandlers.ts
│ │ └── links.json
│ ├── layout
│ │ ├── index.ts
│ │ ├── header
│ │ │ ├── Header.stories.tsx
│ │ │ └── components
│ │ │ │ ├── logo-link
│ │ │ │ └── LogoLink.stories.tsx
│ │ │ │ ├── search-bar
│ │ │ │ └── SearchBar.stories.tsx
│ │ │ │ └── nav-button
│ │ │ │ └── NavButton.stories.tsx
│ │ ├── footer
│ │ │ ├── Footer.stories.tsx
│ │ │ └── Footer.tsx
│ │ └── main
│ │ │ └── Main.tsx
│ ├── components
│ │ ├── @shared
│ │ │ ├── letter-counter
│ │ │ │ ├── useLetterCount.tsx
│ │ │ │ ├── LetterCounter.stories.tsx
│ │ │ │ └── LetterCounter.tsx
│ │ │ ├── center
│ │ │ │ └── Center.tsx
│ │ │ ├── icons
│ │ │ │ ├── plus-icon
│ │ │ │ │ ├── PlusIcon.stories.tsx
│ │ │ │ │ └── PlusIcon.tsx
│ │ │ │ ├── crown-icon
│ │ │ │ │ ├── CrownIcon.stories.tsx
│ │ │ │ │ └── CrownIcon.tsx
│ │ │ │ ├── login-icon
│ │ │ │ │ ├── LoginIcon.stories.tsx
│ │ │ │ │ └── LoginIcon.tsx
│ │ │ │ ├── folder-icon
│ │ │ │ │ ├── FolderIcon.stories.tsx
│ │ │ │ │ └── FolderIcon.tsx
│ │ │ │ ├── logout-icon
│ │ │ │ │ ├── LogoutIcon.stories.tsx
│ │ │ │ │ └── LogoutIcon.tsx
│ │ │ │ ├── pencil-icon
│ │ │ │ │ ├── PencilIcon.stories.tsx
│ │ │ │ │ └── PencilIcon.tsx
│ │ │ │ ├── search-icon
│ │ │ │ │ ├── SearchIcon.stories.tsx
│ │ │ │ │ └── SearchIcon.tsx
│ │ │ │ ├── bookmark-icon
│ │ │ │ │ ├── BookmarkIcon.stories.tsx
│ │ │ │ │ └── BookmarkIcon.tsx
│ │ │ │ ├── trashcan-icon
│ │ │ │ │ ├── TrashcanIcon.stories.tsx
│ │ │ │ │ └── TrashcanIcon.tsx
│ │ │ │ ├── down-arrow-icon
│ │ │ │ │ ├── DownArrowIcon.stories.tsx
│ │ │ │ │ └── DownArrowIcon.tsx
│ │ │ │ ├── kebab-menu-icon
│ │ │ │ │ ├── KebabMenuIcon.stories.tsx
│ │ │ │ │ └── KebabMenuIcon.tsx
│ │ │ │ ├── meatball-menu-icon
│ │ │ │ │ ├── MeatballMenukIcon.stories.tsx
│ │ │ │ │ └── MeatballMenuIcon.tsx
│ │ │ │ ├── right-up-arrow-icon
│ │ │ │ │ ├── RightUpArrowIcon.stories.tsx
│ │ │ │ │ └── RightUpArrowIcon.tsx
│ │ │ │ ├── left-direction-icon
│ │ │ │ │ ├── LeftDirectionIcon.stories.tsx
│ │ │ │ │ └── LeftDirectionIcon.tsx
│ │ │ │ ├── right-direction-icon
│ │ │ │ │ ├── RightDirectionIcon.stories.tsx
│ │ │ │ │ └── RightDirectionIcon.tsx
│ │ │ │ └── x-mark-icon
│ │ │ │ │ ├── XMarkIcon.stories.tsx
│ │ │ │ │ └── XMarkIcon.tsx
│ │ │ ├── form
│ │ │ │ └── Form.tsx
│ │ │ ├── chip
│ │ │ │ └── Chip.stories.tsx
│ │ │ ├── page-title
│ │ │ │ ├── PageTitle.stories.tsx
│ │ │ │ └── PageTitle.tsx
│ │ │ ├── meta-box
│ │ │ │ └── MetaBox.stories.tsx
│ │ │ ├── route-with-condition
│ │ │ │ └── RouteWithCondition.tsx
│ │ │ ├── section-title
│ │ │ │ ├── SectionTitle.stories.tsx
│ │ │ │ └── SectionTitle.tsx
│ │ │ ├── flex
│ │ │ │ └── Flex.stories.tsx
│ │ │ ├── checkbox
│ │ │ │ └── Checkbox.stories.tsx
│ │ │ ├── input
│ │ │ │ └── Input.stories.tsx
│ │ │ ├── divider
│ │ │ │ └── Divider.stories.tsx
│ │ │ ├── button
│ │ │ │ ├── box-button
│ │ │ │ │ └── BoxButton.stories.tsx
│ │ │ │ ├── text-button
│ │ │ │ │ └── TextButton.stories.tsx
│ │ │ │ ├── icon-button
│ │ │ │ │ └── IconButton.stories.tsx
│ │ │ │ ├── linked-button
│ │ │ │ │ └── LinkedButton.tsx
│ │ │ │ ├── toggle-button
│ │ │ │ │ └── ToggleButton.stories.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── textarea
│ │ │ │ └── Textarea.stories.tsx
│ │ │ ├── avatar
│ │ │ │ └── Avatar.stories.tsx
│ │ │ ├── image
│ │ │ │ └── Image.stories.tsx
│ │ │ ├── page-wrapper
│ │ │ │ └── PageWrapper.tsx
│ │ │ ├── list-item
│ │ │ │ └── ListItem.stories.tsx
│ │ │ ├── label
│ │ │ │ └── Label.tsx
│ │ │ ├── user-info-item
│ │ │ │ └── UserInfoItem.stories.tsx
│ │ │ ├── multi-tag-select
│ │ │ │ └── MultiTagSelect.stories.tsx
│ │ │ ├── markdown-render
│ │ │ │ └── MarkdownRender.tsx
│ │ │ └── button-group
│ │ │ │ └── ButtonGroup.stories.tsx
│ │ └── study-chip
│ │ │ ├── StudyChip.tsx
│ │ │ └── StudyChip.stories.tsx
│ ├── custom-types
│ │ ├── theme.d.ts
│ │ └── common.d.ts
│ ├── pages
│ │ ├── study-room-page
│ │ │ ├── tabs
│ │ │ │ ├── community-tab-panel
│ │ │ │ │ └── CommunityTabPanel.tsx
│ │ │ │ ├── link-room-tab-panel
│ │ │ │ │ ├── LinkRoomTabPanel.stories.tsx
│ │ │ │ │ └── components
│ │ │ │ │ │ ├── user-description
│ │ │ │ │ │ ├── UserDescription.tsx
│ │ │ │ │ │ └── UserDescription.stories.tsx
│ │ │ │ │ │ ├── link-item
│ │ │ │ │ │ └── LinkItem.stories.tsx
│ │ │ │ │ │ ├── link-form
│ │ │ │ │ │ └── LinkForm.stories.tsx
│ │ │ │ │ │ ├── link-preview
│ │ │ │ │ │ └── LinkPreview.stories.tsx
│ │ │ │ │ │ └── link-edit-form
│ │ │ │ │ │ └── LinkEditForm.stories.tsx
│ │ │ │ └── review-tab-panel
│ │ │ │ │ └── components
│ │ │ │ │ ├── reivew-form
│ │ │ │ │ └── ReviewForm.stories.tsx
│ │ │ │ │ └── review-comment
│ │ │ │ │ └── ReviewComment.stories.tsx
│ │ │ └── components
│ │ │ │ └── tab-button
│ │ │ │ ├── TabButton.tsx
│ │ │ │ └── TabButton.stories.tsx
│ │ ├── error-page
│ │ │ ├── hooks
│ │ │ │ └── useErrorPage.ts
│ │ │ ├── ErrorPage.stories.tsx
│ │ │ └── ErrorPage.tsx
│ │ ├── login-redirect-page
│ │ │ └── LoginRedirectPage.tsx
│ │ ├── index.ts
│ │ ├── detail-page
│ │ │ └── components
│ │ │ │ ├── more-button
│ │ │ │ ├── MoreButton.tsx
│ │ │ │ └── MoreButton.stories.tsx
│ │ │ │ ├── study-member-card
│ │ │ │ └── StudyMemberCard.stories.tsx
│ │ │ │ ├── study-float-box
│ │ │ │ └── StudyFloatBox.stories.tsx
│ │ │ │ ├── study-wide-float-box
│ │ │ │ └── StudyWideFloatBox.stories.tsx
│ │ │ │ └── study-review-card
│ │ │ │ └── StudyReviewCard.stories.tsx
│ │ ├── main-page
│ │ │ └── components
│ │ │ │ ├── create-new-study-button
│ │ │ │ ├── CreateNewStudyButton.stories.tsx
│ │ │ │ └── CreateNewStudyButton.tsx
│ │ │ │ ├── filter-slide-button
│ │ │ │ ├── FilterSlideButton.stories.tsx
│ │ │ │ └── FilterSlideButton.tsx
│ │ │ │ ├── filter-button
│ │ │ │ └── FilterButton.stories.tsx
│ │ │ │ └── study-card
│ │ │ │ └── StudyCard.stories.tsx
│ │ ├── study-page
│ │ │ ├── components
│ │ │ │ ├── description-tab
│ │ │ │ │ └── DescriptionTab.stories.tsx
│ │ │ │ └── publish
│ │ │ │ │ └── Publish.tsx
│ │ │ └── layout
│ │ │ │ └── Layout.tsx
│ │ └── my-study-page
│ │ │ ├── components
│ │ │ └── my-study-card
│ │ │ │ └── MyStudyCard.stories.tsx
│ │ │ └── hooks
│ │ │ └── useMyStudyPage.ts
│ ├── hooks
│ │ ├── usePositiveNumberInput.ts
│ │ ├── useAuth.ts
│ │ ├── useUserInfo.ts
│ │ └── useUserRole.ts
│ ├── api
│ │ ├── my-studies
│ │ │ ├── typeChecker.ts
│ │ │ └── index.ts
│ │ ├── tags
│ │ │ └── index.ts
│ │ ├── links
│ │ │ └── typeChecker.ts
│ │ ├── reviews
│ │ │ └── typeChecker.ts
│ │ ├── link-preview
│ │ │ └── typeChecker.ts
│ │ ├── review
│ │ │ └── typeChecker.ts
│ │ └── link
│ │ │ └── typeChecker.ts
│ └── context
│ │ ├── search
│ │ └── SearchProvider.tsx
│ │ ├── userInfo
│ │ └── UserInfoProvider.tsx
│ │ └── login
│ │ └── LoginProvider.tsx
├── static
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpg
│ ├── 6.jpg
│ ├── 7.jpg
│ ├── 8.jpg
│ ├── 9.jpg
│ ├── 10.jpg
│ ├── 11.jpg
│ ├── 12.jpg
│ ├── 13.jpg
│ ├── 14.jpg
│ ├── 15.jpg
│ ├── 16.jpg
│ ├── 17.jpg
│ ├── 18.jpg
│ ├── 19.jpg
│ ├── 20.jpg
│ ├── 21.jpg
│ ├── 22.jpg
│ ├── 23.jpg
│ ├── 24.jpg
│ ├── 25.jpg
│ ├── 26.jpg
│ ├── 27.jpg
│ ├── 28.jpg
│ ├── Pretendard-Light.woff2
│ ├── Pretendard-Regular.woff2
│ ├── Pretendard-SemiBold.woff2
│ └── Pretendard-ExtraBold.woff2
├── public
│ ├── favicon.png
│ └── index.html
├── env
│ ├── .env.local
│ ├── .env.prod
│ └── .env.dev
├── .vscode
│ └── settings.json
├── .gitignore
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── support
│ │ ├── component-index.html
│ │ └── e2e.ts
│ └── tsconfig.json
├── cypress.config.ts
├── .babelrc.json
├── README.md
└── webpack
│ ├── webpack.prod.js
│ ├── webpack.dev.js
│ └── webpack.local.js
└── .github
├── pull_request_template.md
├── ISSUE_TEMPLATE
└── issue_feature_template.md
└── workflows
├── devploy-frontend-prod.yml
└── frontend.yml
/backend/README.md:
--------------------------------------------------------------------------------
1 | # MOAMOA
2 |
3 | hello.
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | frontend/**/*.json linguist-generated
2 |
--------------------------------------------------------------------------------
/backend/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'moamoa'
2 |
--------------------------------------------------------------------------------
/frontend/.storybook/preview-body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/frontend/src/utils/noop.ts:
--------------------------------------------------------------------------------
1 | const noop = (): undefined => undefined;
2 |
3 | export default noop;
4 |
--------------------------------------------------------------------------------
/frontend/static/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/1.jpg
--------------------------------------------------------------------------------
/frontend/static/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/2.jpg
--------------------------------------------------------------------------------
/frontend/static/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/3.jpg
--------------------------------------------------------------------------------
/frontend/static/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/4.jpg
--------------------------------------------------------------------------------
/frontend/static/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/5.jpg
--------------------------------------------------------------------------------
/frontend/static/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/6.jpg
--------------------------------------------------------------------------------
/frontend/static/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/7.jpg
--------------------------------------------------------------------------------
/frontend/static/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/8.jpg
--------------------------------------------------------------------------------
/frontend/static/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/9.jpg
--------------------------------------------------------------------------------
/frontend/static/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/10.jpg
--------------------------------------------------------------------------------
/frontend/static/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/11.jpg
--------------------------------------------------------------------------------
/frontend/static/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/12.jpg
--------------------------------------------------------------------------------
/frontend/static/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/13.jpg
--------------------------------------------------------------------------------
/frontend/static/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/14.jpg
--------------------------------------------------------------------------------
/frontend/static/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/15.jpg
--------------------------------------------------------------------------------
/frontend/static/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/16.jpg
--------------------------------------------------------------------------------
/frontend/static/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/17.jpg
--------------------------------------------------------------------------------
/frontend/static/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/18.jpg
--------------------------------------------------------------------------------
/frontend/static/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/19.jpg
--------------------------------------------------------------------------------
/frontend/static/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/20.jpg
--------------------------------------------------------------------------------
/frontend/static/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/21.jpg
--------------------------------------------------------------------------------
/frontend/static/22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/22.jpg
--------------------------------------------------------------------------------
/frontend/static/23.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/23.jpg
--------------------------------------------------------------------------------
/frontend/static/24.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/24.jpg
--------------------------------------------------------------------------------
/frontend/static/25.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/25.jpg
--------------------------------------------------------------------------------
/frontend/static/26.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/26.jpg
--------------------------------------------------------------------------------
/frontend/static/27.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/27.jpg
--------------------------------------------------------------------------------
/frontend/static/28.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/28.jpg
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ## 요약
3 |
4 | ## 세부사항
5 |
6 | close
7 |
--------------------------------------------------------------------------------
/frontend/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/public/favicon.png
--------------------------------------------------------------------------------
/frontend/env/.env.local:
--------------------------------------------------------------------------------
1 | API_URL=""
2 | CLIENT_ID="cb83d95cd5644436b090"
3 | LINK_PREVIEW_API_URL="https://api.og.moamoa.space"
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/logo.png
--------------------------------------------------------------------------------
/frontend/static/Pretendard-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/Pretendard-Light.woff2
--------------------------------------------------------------------------------
/backend/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/backend/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/frontend/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.preferences.importModuleSpecifier": "non-relative",
3 | "editor.formatOnSave": true
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/static/Pretendard-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/Pretendard-Regular.woff2
--------------------------------------------------------------------------------
/frontend/static/Pretendard-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/Pretendard-SemiBold.woff2
--------------------------------------------------------------------------------
/frontend/env/.env.prod:
--------------------------------------------------------------------------------
1 | API_URL="https://api.moamoa.space"
2 | CLIENT_ID="cf7c0528216f765c83c0"
3 | LINK_PREVIEW_API_URL="https://api.og.moamoa.space"
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/rocket-cursor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/rocket-cursor.png
--------------------------------------------------------------------------------
/frontend/static/Pretendard-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/static/Pretendard-ExtraBold.woff2
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | .cache
4 | dist
5 |
6 | storybook-static/
7 |
8 | cypress/screenshots/
9 | cypress/videos/
10 |
--------------------------------------------------------------------------------
/frontend/env/.env.dev:
--------------------------------------------------------------------------------
1 | API_URL="https://api.dev.moamoa.space"
2 | CLIENT_ID="cb83d95cd5644436b090"
3 | LINK_PREVIEW_API_URL="https://api.og.moamoa.space"
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/no-image-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/no-image-found.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/sth-went-wrong.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/sth-went-wrong.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/moamoa-site-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/moamoa-site-image.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/rocket-cursor-pointer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/woowacourse-teams/2022-moamoa/HEAD/frontend/src/assets/images/rocket-cursor-pointer.png
--------------------------------------------------------------------------------
/frontend/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw';
2 |
3 | import { handlers } from '@mocks/handlers/handlers';
4 |
5 | export const worker = setupWorker(...handlers);
6 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/MemberRole.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain;
2 |
3 | public enum MemberRole {
4 |
5 | MEMBER, NON_MEMBER, OWNER
6 | }
7 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/domain/CategoryName.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.domain;
2 |
3 | public enum CategoryName {
4 |
5 | GENERATION, AREA, SUBJECT
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/layout/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Footer } from '@layout/footer/Footer';
2 | export { default as Header } from '@layout/header/Header';
3 | export { default as Main } from '@layout/main/Main';
4 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/RecruitStatus.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain;
2 |
3 | public enum RecruitStatus {
4 |
5 | RECRUITMENT_START, RECRUITMENT_END
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/utils/hasOwnProperty.ts:
--------------------------------------------------------------------------------
1 | export const hasOwnProperty = (
2 | obj: ObjectType,
3 | prop: KeyType,
4 | ): obj is ObjectType & Record => obj.hasOwnProperty(prop);
5 |
--------------------------------------------------------------------------------
/frontend/src/utils/getRandomInt.ts:
--------------------------------------------------------------------------------
1 | const getRandomInt = (min: number, max: number) => {
2 | min = Math.ceil(min);
3 | max = Math.floor(max);
4 | return Math.floor(Math.random() * (max - min + 1)) + min;
5 | };
6 |
7 | export default getRandomInt;
8 |
--------------------------------------------------------------------------------
/backend/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/concurrent/HttpRequestExecutor.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.concurrent;
2 |
3 | import org.springframework.http.HttpStatus;
4 |
5 | @FunctionalInterface
6 | interface HttpRequestExecutor {
7 |
8 | HttpStatus execute();
9 | }
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_feature_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: issue_feature_template
3 | about: 기능 이슈 관련 템플릿
4 | title: "[기능]"
5 | labels: feature
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## 요약
11 |
12 | ## 세부 설명
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/mocks/handlers/tagHandlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | import tagsJSON from '@mocks/tags.json';
4 |
5 | export const tagHandlers = [
6 | rest.get('/api/tags', (req, res, ctx) => {
7 | return res(ctx.status(200), ctx.json(tagsJSON));
8 | }),
9 | ];
10 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/exception/TagNotExistException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.exception;
2 |
3 | public class TagNotExistException extends RuntimeException {
4 |
5 | public TagNotExistException() {
6 | super("필터가 존재하지 않습니다.");
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/exception/NotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.exception;
2 |
3 | public class NotFoundException extends RuntimeException {
4 |
5 | public NotFoundException(final String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/exception/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.exception;
2 |
3 | public class BadRequestException extends RuntimeException {
4 |
5 | public BadRequestException(final String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/exception/InvalidFormatException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.exception;
2 |
3 | public class InvalidFormatException extends IllegalArgumentException {
4 |
5 | public InvalidFormatException() {
6 | super("잘못된 요청 정보입니다.");
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/exception/UnauthorizedException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.exception;
2 |
3 | public class UnauthorizedException extends RuntimeException {
4 |
5 | public UnauthorizedException(final String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/exception/InvalidPeriodException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain.exception;
2 |
3 | public class InvalidPeriodException extends RuntimeException {
4 |
5 | public InvalidPeriodException() {
6 | super("잘못된 기간 설정입니다.");
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/letter-counter/useLetterCount.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useLetterCount = (maxCount: number, initialCount = 0) => {
4 | const [count, setCount] = useState(initialCount);
5 | return { count, setCount, maxCount };
6 | };
7 |
8 | export default useLetterCount;
9 |
--------------------------------------------------------------------------------
/frontend/src/custom-types/theme.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-interface */
2 | // import문과 declare의 충돌로 나머지 declare문은 common.d.ts에 작성했다
3 | import { theme } from '@styles/theme';
4 |
5 | type ThemeConfig = typeof theme;
6 | declare module '@emotion/react' {
7 | export interface Theme extends ThemeConfig {}
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as getRandomInt } from '@utils/getRandomInt';
2 | export { default as noop } from '@utils/noop';
3 | export * from '@utils/dates';
4 | export * from '@utils/typeChecker';
5 | export { default as checkType } from '@utils/typeChecker';
6 | export { default as arrayOfAll } from '@utils/arrayOfAll';
7 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/TokenResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.service.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @AllArgsConstructor
7 | @Getter
8 | public class TokenResponse {
9 |
10 | private final String accessToken;
11 | }
12 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/FailureParticipationException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.exception;
2 |
3 | public class FailureParticipationException extends RuntimeException {
4 |
5 | public FailureParticipationException() {
6 | super("스터디 가입이 불가능합니다.");
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/utils/nLineEllipsis.ts:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | export const nLineEllipsis = (numOfLine: number) => css`
4 | display: -webkit-box;
5 | overflow: clip;
6 | text-overflow: ellipsis;
7 | -webkit-box-orient: vertical;
8 | -webkit-line-clamp: ${numOfLine};
9 |
10 | word-break: break-all;
11 | `;
12 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/OAuthClient.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.service.oauthclient;
2 |
3 | import com.woowacourse.moamoa.auth.service.oauthclient.response.GithubProfileResponse;
4 |
5 | public interface OAuthClient {
6 |
7 | GithubProfileResponse getProfile(final String code);
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/CategoryData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.query.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @AllArgsConstructor
7 | @Getter
8 | public class CategoryData {
9 |
10 | private final Long id;
11 | private final String name;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/center/Center.tsx:
--------------------------------------------------------------------------------
1 | import Flex from '@shared/flex/Flex';
2 |
3 | type CenterProps = {
4 | children?: React.ReactNode;
5 | };
6 |
7 | const Center: React.FC = ({ children }) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | export default Center;
14 |
--------------------------------------------------------------------------------
/frontend/src/components/study-chip/StudyChip.tsx:
--------------------------------------------------------------------------------
1 | import Chip from '@shared/chip/Chip';
2 |
3 | export type StudyChipProps = {
4 | isOpen: boolean;
5 | };
6 |
7 | const StudyChip = ({ isOpen }: StudyChipProps) => {
8 | return isOpen ? 모집중 : 모집완료;
9 | };
10 |
11 | export default StudyChip;
12 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/repository/JpaStudyRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain.repository;
2 |
3 | import com.woowacourse.moamoa.study.domain.Study;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | interface JpaStudyRepository extends JpaRepository, StudyRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/layout/header/Header.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Header from '@layout/header/Header';
4 |
5 | export default {
6 | title: 'Layout/Header',
7 | component: Header,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/JpaMemberRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.domain.repository;
2 |
3 | import com.woowacourse.moamoa.member.domain.Member;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | interface JpaMemberRepository extends JpaRepository, MemberRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/config/JpaAuditingConfig.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
5 |
6 | @Configuration
7 | @EnableJpaAuditing
8 | public class JpaAuditingConfig {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/utils/DateTimeSystem.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.utils;
2 |
3 | import java.time.LocalDateTime;
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class DateTimeSystem {
8 |
9 | public LocalDateTime now() {
10 | return LocalDateTime.now();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/custom-types/common.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.jpg';
2 | declare module '*.jpeg';
3 | declare module '*.png';
4 | declare module '*.json';
5 |
6 | declare namespace NodeJS {
7 | export type ProcessEnv = {
8 | API_URL: string;
9 | CLIENT_ID: string;
10 | LINK_PREVIEW_API_URL: string;
11 | NODE_ENV: 'development' | 'production';
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/domain/repository/JpaCommentRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.domain.repository;
2 |
3 | import com.woowacourse.moamoa.comment.domain.Comment;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | interface JpaCommentRepository extends JpaRepository, CommentRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/article/repository/ArticleRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.article.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.article.Article;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface ArticleRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenExpirationException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.UnauthorizedException;
4 |
5 | public class TokenExpirationException extends UnauthorizedException {
6 |
7 | public TokenExpirationException() {
8 | super("만료된 토큰입니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/exception/TokenNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.UnauthorizedException;
4 |
5 | public class TokenNotFoundException extends UnauthorizedException {
6 |
7 | public TokenNotFoundException() {
8 | super("토큰이 존재하지 않습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/service/response/AccessTokenResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.service.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @AllArgsConstructor
7 | @Getter
8 | public class AccessTokenResponse {
9 |
10 | private final String accessToken;
11 | private final long expiredTime;
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | import webpackConfig from './webpack/webpack.local';
4 |
5 | export default defineConfig({
6 | e2e: {
7 | baseUrl: 'http://localhost:3000',
8 | },
9 | component: {
10 | devServer: {
11 | framework: 'react',
12 | bundler: 'webpack',
13 | webpackConfig,
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/community-tab-panel/CommunityTabPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 |
3 | import PageWrapper from '@shared/page-wrapper/PageWrapper';
4 |
5 | const CommunityTabPanel: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default CommunityTabPanel;
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/link/repository/LinkArticleRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.link.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.link.LinkArticle;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface LinkArticleRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/plus-icon/PlusIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { PlusIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/PlusIcon',
7 | component: PlusIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/studyroom/repository/StudyRoomRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.studyroom.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.studyroom.StudyRoom;
4 | import java.util.Optional;
5 |
6 | public interface StudyRoomRepository {
7 |
8 | Optional findByStudyId(Long studyId);
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/crown-icon/CrownIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { CrownIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/CrownIcon',
7 | component: CrownIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/login-icon/LoginIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { LoginIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/LoginIcon',
7 | component: LoginIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/MemberNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 |
5 | public class MemberNotFoundException extends NotFoundException {
6 |
7 | public MemberNotFoundException() {
8 | super("회원을 찾을 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/article/repository/TempArticleRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.article.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.article.TempArticle;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface TempArticleRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/review/repository/JpaReviewRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.review.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.review.Review;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface JpaReviewRepository extends JpaRepository, ReviewRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/form/Form.tsx:
--------------------------------------------------------------------------------
1 | import { noop } from '@utils';
2 |
3 | export type FormProps = {
4 | children: React.ReactNode;
5 | onSubmit?: React.FormEventHandler;
6 | };
7 |
8 | const Form: React.FC = ({ children, onSubmit: handleSubmit = noop }) => {
9 | return ;
10 | };
11 |
12 | export default Form;
13 |
--------------------------------------------------------------------------------
/frontend/src/utils/isThemeFontSize.ts:
--------------------------------------------------------------------------------
1 | import { hasOwnProperty } from '@utils/hasOwnProperty';
2 | import { isString } from '@utils/typeChecker';
3 |
4 | import { type ThemeFontSize, theme } from '@styles/theme';
5 |
6 | const isThemeFontSize = (fontSize: unknown): fontSize is ThemeFontSize =>
7 | isString(fontSize) && hasOwnProperty(theme.fontSize, fontSize);
8 |
9 | export default isThemeFontSize;
10 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/exception/CommentNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 |
5 | public class CommentNotFoundException extends NotFoundException {
6 |
7 | public CommentNotFoundException() {
8 | super("댓글을 찾을 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/InvalidUpdatingException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class InvalidUpdatingException extends BadRequestException {
6 |
7 | public InvalidUpdatingException() {
8 | super("스터디 수정이 불가능합니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/ReviewNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 |
5 | public class ReviewNotFoundException extends NotFoundException {
6 |
7 | public ReviewNotFoundException() {
8 | super("후기를 찾을 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/folder-icon/FolderIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { FolderIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/FolderIcon',
7 | component: FolderIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/logout-icon/LogoutIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { LogoutIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/LogoutIcon',
7 | component: LogoutIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/pencil-icon/PencilIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { PencilIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/PencilIcon',
7 | component: PencilIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/search-icon/SearchIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { SearchIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/SearchIcon',
7 | component: SearchIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/layout/footer/Footer.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Footer, { type FooterProps } from '@layout/footer/Footer';
4 |
5 | export default {
6 | title: 'Layout/Footer',
7 | component: Footer,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/exception/UnEditingCommentException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class UnEditingCommentException extends BadRequestException {
6 |
7 | public UnEditingCommentException() {
8 | super("댓글을 삭제할 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/FailureKickOutException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class FailureKickOutException extends BadRequestException {
6 |
7 | public FailureKickOutException() {
8 | super("방장만 스터디원을 강퇴시킬 수 있습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/layout/header/components/logo-link/LogoLink.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import LogoLink from '@layout/header/components/logo-link/LogoLink';
4 |
5 | export default {
6 | title: 'Layout/LogoLink',
7 | component: LogoLink,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/OwnerCanNotLeaveException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class OwnerCanNotLeaveException extends BadRequestException {
6 |
7 | public OwnerCanNotLeaveException() {
8 | super("스터디장은 스터디를 탈퇴할 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/studyroom/repository/JpaStudyRoomRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.studyroom.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.studyroom.StudyRoom;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | interface JpaStudyRoomRepository extends JpaRepository, StudyRoomRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/service/response/TagsResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.service.response;
2 |
3 | import com.woowacourse.moamoa.tag.query.response.TagData;
4 | import java.util.List;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 |
8 | @AllArgsConstructor
9 | @Getter
10 | public class TagsResponse {
11 |
12 | private final List tags;
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/bookmark-icon/BookmarkIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { BookmarkIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/BookmarkIcon',
7 | component: BookmarkIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/trashcan-icon/TrashcanIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { TrashcanIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/TrashcanIcon',
7 | component: TrashcanIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/exception/UnWrittenCommentException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class UnWrittenCommentException extends BadRequestException {
6 |
7 | public UnWrittenCommentException() {
8 | super("댓글을 작성하거나 수정할 수 없습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/down-arrow-icon/DownArrowIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { DownArrowIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/DownArrowIcon',
7 | component: DownArrowIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/kebab-menu-icon/KebabMenuIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { KebabMenuIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/KebabMenuIcon',
7 | component: KebabMenuIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/domain/repository/CommentRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.domain.repository;
2 |
3 | import com.woowacourse.moamoa.comment.domain.Comment;
4 | import java.util.Optional;
5 |
6 | public interface CommentRepository {
7 |
8 | Optional findById(Long id);
9 |
10 | Comment save(Comment comment);
11 |
12 | void deleteById(Long id);
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/exception/NotParticipatedMemberException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 |
5 | public class NotParticipatedMemberException extends BadRequestException {
6 |
7 | public NotParticipatedMemberException() {
8 | super("스터디에 참여하지 않은 회원입니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/StudyData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.query.data;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public class StudyData {
7 |
8 | private final Long id;
9 |
10 | private final String title;
11 |
12 | public StudyData(final Long id, final String title) {
13 | this.id = id;
14 | this.title = title;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/error-page/hooks/useErrorPage.ts:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 |
3 | import { PATH } from '@constants';
4 |
5 | const useErrorPage = () => {
6 | const navigate = useNavigate();
7 |
8 | const handleHomeButtonClick = () => {
9 | navigate(PATH.MAIN, { replace: true });
10 | };
11 |
12 | return {
13 | handleHomeButtonClick,
14 | };
15 | };
16 |
17 | export default useErrorPage;
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/config/AuthenticatedMemberId.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.config;
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 AuthenticatedMemberId {
11 | }
12 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/service/exception/NotParticipatedMemberException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.UnauthorizedException;
4 |
5 | public class NotParticipatedMemberException extends UnauthorizedException {
6 |
7 | public NotParticipatedMemberException() {
8 | super("스터디에 참여한 회원만 작성할 수 있습니다.");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | // cypress ts target을 ES6로 설정
2 | // esnext일 때 에러 발생
3 | {
4 | "extends": "../tsconfig.json",
5 | "compilerOptions": {
6 | "target": "ES6",
7 | "module": "ES6",
8 | "types": ["cypress", "@testing-library/cypress"] //https://docs.cypress.io/guides/tooling/typescript-support#Configure-tsconfig-json
9 | },
10 | "include": ["../src", "../node_modules/cypress", "./**/*.cy.ts", "./**/*.cy.tsx"]
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/meatball-menu-icon/MeatballMenukIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { MeatballMenuIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/MeatballMenuIcon',
7 | component: MeatballMenuIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/right-up-arrow-icon/RightUpArrowIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { RightUpArrowIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/RightUpArrowIcon',
7 | component: RightUpArrowIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyRoleResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.response;
2 |
3 | import com.woowacourse.moamoa.study.domain.MemberRole;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @AllArgsConstructor
9 | @NoArgsConstructor
10 | @Getter
11 | public class MyRoleResponse {
12 |
13 | private MemberRole role;
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/review/repository/ReviewRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.review.repository;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.review.Review;
4 | import java.util.Optional;
5 |
6 | public interface ReviewRepository {
7 |
8 | Review save(Review review);
9 |
10 | Optional findById(Long id);
11 |
12 | void deleteById(Long id);
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/left-direction-icon/LeftDirectionIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { LeftDirectionIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/LeftDirectionIcon',
7 | component: LeftDirectionIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | sql:
3 | init:
4 | mode: never
5 | jpa:
6 | hibernate:
7 | ddl-auto: validate
8 | properties:
9 | hibernate:
10 | dialect: org.hibernate.dialect.MySQL8Dialect
11 | datasource:
12 | driver-class-name: org.h2.Driver
13 | username: sa
14 | password:
15 | url: jdbc:h2:mem://localhost/~/testdb;MODE=MYSQL
16 | profiles:
17 | include: security
18 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/right-direction-icon/RightDirectionIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { RightDirectionIcon } from '@shared/icons';
4 |
5 | export default {
6 | title: 'Materials/Icons/RightDirectionIcon',
7 | component: RightDirectionIcon,
8 | };
9 |
10 | const Template: Story = () => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/chip/Chip.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Chip, { type ChipProps } from '@shared/chip/Chip';
4 |
5 | export default {
6 | title: 'Components/Chip',
7 | component: Chip,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | children: '모집중',
15 | variant: 'primary',
16 | };
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/login-redirect-page/LoginRedirectPage.tsx:
--------------------------------------------------------------------------------
1 | import PageWrapper from '@shared/page-wrapper/PageWrapper';
2 |
3 | import useLoginRedirectPage from '@login-redirect-page/hooks/useLoginRedirectPage';
4 |
5 | const LoginRedirectPage: React.FC = () => {
6 | useLoginRedirectPage();
7 |
8 | return (
9 |
10 | 로그인 진행 중입니다...
11 |
12 | );
13 | };
14 |
15 | export default LoginRedirectPage;
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/StudyStatus.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain;
2 |
3 | public enum StudyStatus {
4 |
5 | PREPARE, IN_PROGRESS, DONE;
6 |
7 | public StudyStatus nextStatus() {
8 | if (this.equals(PREPARE)) {
9 | return IN_PROGRESS;
10 | }
11 | if (this.equals(IN_PROGRESS)) {
12 | return DONE;
13 | }
14 | return PREPARE;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/infrastructure/TokenProvider.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.infrastructure;
2 |
3 | import com.woowacourse.moamoa.auth.service.response.TokenResponse;
4 |
5 | public interface TokenProvider {
6 |
7 | TokenResponse createToken(final Long payload);
8 |
9 | String getPayload(final String token);
10 |
11 | boolean validateToken(final String token);
12 |
13 | long getValidityInMilliseconds();
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.query.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.ToString;
6 |
7 | @Getter
8 | @AllArgsConstructor
9 | @ToString
10 | public class TagData {
11 |
12 | private final Long id;
13 | private final String name;
14 | private final String description;
15 | private final CategoryData category;
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/layout/header/components/search-bar/SearchBar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import SearchBar, { type SearchBarProps } from '@layout/header/components/search-bar/SearchBar';
4 |
5 | export default {
6 | title: 'Layout/SearchBar',
7 | component: SearchBar,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/LinkRoomTabPanel.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import LinkRoomTabPanel from '@link-tab/LinkRoomTabPanel';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/LinkRoomTabPanel',
7 | component: LinkRoomTabPanel,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {};
14 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/document/Document.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance.document;
2 |
3 | import io.restassured.specification.RequestSpecification;
4 |
5 | public abstract class Document {
6 |
7 | private final RequestSpecification spec;
8 |
9 | protected Document(final RequestSpecification spec) {
10 | this.spec = spec;
11 | }
12 |
13 | public RequestSpecification spec() {
14 | return spec;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/ReviewRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.request;
2 |
3 | import javax.validation.constraints.NotBlank;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @NoArgsConstructor
9 | @AllArgsConstructor
10 | @Getter
11 | public class ReviewRequest {
12 |
13 | @NotBlank(message = "내용을 입력해 주세요.")
14 | private String content;
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/down-arrow-icon/DownArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | const DownArrowIcon: React.FC = () => (
2 |
15 | );
16 |
17 | export default DownArrowIcon;
18 |
--------------------------------------------------------------------------------
/frontend/src/pages/error-page/ErrorPage.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { ErrorPage } from '@pages';
4 |
5 | import PageWrapper from '@shared/page-wrapper/PageWrapper';
6 |
7 | export default {
8 | title: 'Pages/ErrorPage',
9 | component: ErrorPage,
10 | };
11 |
12 | const Template: Story = () => (
13 |
14 |
15 |
16 | );
17 |
18 | export const Default = Template.bind({});
19 | Default.args = {};
20 |
--------------------------------------------------------------------------------
/frontend/src/utils/arrayOfAll.ts:
--------------------------------------------------------------------------------
1 | /* How to use:
2 | * type BreakPoints = 'xs' | 'sm' | 'md';
3 | * const arrayOfAllBreakPoints = arrayOfAll();
4 | * const wrongBreakPoints = arrayOfAllBreakPoints(['xs', 'sm']); // Error
5 | * const breakPoints = arrayOfAllBreakPoints(['xs', 'sm', 'md']); // Ok
6 | */
7 |
8 | const arrayOfAll =
9 | () =>
10 | (array: U & ([T] extends [U[number]] ? unknown : 'Invalid')) =>
11 | array;
12 |
13 | export default arrayOfAll;
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/alarm/SlackUserProfile.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.alarm;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 | import lombok.ToString;
7 |
8 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
9 | @Getter
10 | @ToString
11 | public class SlackUserProfile {
12 |
13 | private String email;
14 |
15 | public SlackUserProfile(final String email) {
16 | this.email = email;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/request/EditingCommentRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.request;
2 |
3 | import javax.validation.constraints.NotBlank;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @NoArgsConstructor
9 | @AllArgsConstructor
10 | @Getter
11 | public class EditingCommentRequest {
12 |
13 | @NotBlank(message = "내용을 입력해 주세요.")
14 | private String content;
15 | }
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/ArticleNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 |
5 | public class ArticleNotFoundException extends NotFoundException {
6 |
7 | public ArticleNotFoundException(final Long articleId, final String typeName) {
8 | super(String.format("%d의 식별자를 가진 %s 게시글이 존재하지 않습니다.", articleId, typeName));
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/query/response/TagSummaryData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.query.response;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.ToString;
8 |
9 | @NoArgsConstructor
10 | @AllArgsConstructor
11 | @Getter
12 | @EqualsAndHashCode
13 | @ToString
14 | public class TagSummaryData {
15 |
16 | private Long id;
17 | private String name;
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/bookmark-icon/BookmarkIcon.tsx:
--------------------------------------------------------------------------------
1 | const BookmarkIcon: React.FC = () => (
2 |
13 | );
14 |
15 | export default BookmarkIcon;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/page-title/PageTitle.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import PageTitle, { type PageTitleProps } from '@shared/page-title/PageTitle';
4 |
5 | export default {
6 | title: 'Components/PageTitle',
7 | component: PageTitle,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | children: '페이지 제목',
15 | align: 'center',
16 | };
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudySummaryData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.query.data;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @NoArgsConstructor
8 | @AllArgsConstructor
9 | @Getter
10 | public class StudySummaryData {
11 |
12 | private Long id;
13 | private String title;
14 | private String excerpt;
15 | private String thumbnail;
16 | private String recruitmentStatus;
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/meatball-menu-icon/MeatballMenuIcon.tsx:
--------------------------------------------------------------------------------
1 | const MeatballMenuIcon: React.FC = () => (
2 |
13 | );
14 |
15 | export default MeatballMenuIcon;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/meta-box/MetaBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import MetaBox from '@shared/meta-box/MetaBox';
4 |
5 | export default {
6 | title: 'Components/MetaBox',
7 | component: MetaBox,
8 | };
9 |
10 | const Template: Story = () => (
11 |
12 | 스터디 인원
13 | Content입니다
14 |
15 | );
16 |
17 | export const Default = Template.bind({});
18 | Default.args = {};
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/route-with-condition/RouteWithCondition.tsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Outlet } from 'react-router-dom';
2 |
3 | import { PATH } from '@constants';
4 |
5 | export type RouteWithConditionProps = {
6 | routingCondition: boolean;
7 | };
8 |
9 | const RouteWithCondition: React.FC = ({ routingCondition }) => {
10 | if (!routingCondition) return ;
11 | return ;
12 | };
13 |
14 | export default RouteWithCondition;
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/domain/repository/MemberRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.domain.repository;
2 |
3 | import com.woowacourse.moamoa.member.domain.Member;
4 | import java.util.List;
5 | import java.util.Optional;
6 |
7 | public interface MemberRepository {
8 |
9 | Member save(Member member);
10 |
11 | Optional findByGithubId(Long githubId);
12 |
13 | List findAllById(Iterable ids);
14 |
15 | Optional findById(Long id);
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/.babelrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | [
5 | "@babel/preset-react",
6 | {
7 | "runtime": "automatic",
8 | "importSource": "@emotion/react"
9 | }
10 | ],
11 | "@babel/preset-typescript"
12 | ],
13 | "plugins": [
14 | [
15 | "@emotion",
16 | {
17 | "sourceMap": true,
18 | "autoLabel": "dev-only",
19 | "labelFormat": "[local]",
20 | "cssPropOptimization": true
21 | }
22 | ]
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/CreatedArticleIdResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
8 | @Getter
9 | public class CreatedArticleIdResponse {
10 |
11 | private Long articleId;
12 |
13 | public CreatedArticleIdResponse(final Long articleId) {
14 | this.articleId = articleId;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # FE 모아모아(MOAMOA)
2 |
3 | 병민([@airman5573](https://github.com/airman5573)), 태태([@nan-noo](https://github.com/nan-noo))
4 |
5 | ## Getting Started (Local)
6 |
7 | ```bash
8 | # 1. clone this repository 'woowacourse-teams/2022-moamoa'
9 |
10 | # 2. go to frontend folder
11 | cd frontend
12 |
13 | # 3. install node_modules
14 | npm install
15 |
16 | # 4. start
17 | npm run start
18 |
19 | # .env.local file -> you can use your server
20 | API_URL="" # e.g) http://localhost:8080
21 | ```
22 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/search-icon/SearchIcon.tsx:
--------------------------------------------------------------------------------
1 | const SearchIcon: React.FC = () => (
2 |
16 | );
17 |
18 | export default SearchIcon;
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/letter-counter/LetterCounter.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import LetterCounter, { type LetterCounterProps } from '@shared/letter-counter/LetterCounter';
4 |
5 | export default {
6 | title: 'Components/LetterCounter',
7 | component: LetterCounter,
8 | argTypes: {},
9 | };
10 |
11 | const Template: Story = props => ;
12 |
13 | export const Default = Template.bind({});
14 | Default.args = { count: 0, maxCount: 10 };
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/response/MyStudiesResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.response;
2 |
3 | import java.util.List;
4 | import lombok.AllArgsConstructor;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.ToString;
9 |
10 | @AllArgsConstructor
11 | @NoArgsConstructor
12 | @EqualsAndHashCode
13 | @Getter
14 | @ToString
15 | public class MyStudiesResponse {
16 |
17 | private List studies;
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/section-title/SectionTitle.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import SectionTitle, { type SectionTitleProps } from '@shared/section-title/SectionTitle';
4 |
5 | export default {
6 | title: 'Components/SectionTitle',
7 | component: SectionTitle,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | children: '섹션 제목',
15 | align: 'center',
16 | };
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/exception/StudyNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 |
5 | public class StudyNotFoundException extends NotFoundException {
6 |
7 | public StudyNotFoundException() {
8 | super("스터디가 존재하지 않습니다.");
9 | }
10 |
11 | public StudyNotFoundException(final Long studyId) {
12 | super(String.format("스터디[Id: %d]는 존재하지 않습니다.", studyId));
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/components/study-chip/StudyChip.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import StudyChip, { type StudyChipProps } from '@components/study-chip/StudyChip';
4 |
5 | export default {
6 | title: 'Components/StudyChip',
7 | component: StudyChip,
8 | argTypes: {
9 | isOpen: { controls: 'boolean' },
10 | },
11 | };
12 |
13 | const Template: Story = props => ;
14 |
15 | export const Default = Template.bind({});
16 | Default.args = {
17 | isOpen: true,
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/login-icon/LoginIcon.tsx:
--------------------------------------------------------------------------------
1 | const LoginIcon: React.FC = () => (
2 |
14 | );
15 |
16 | export default LoginIcon;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/plus-icon/PlusIcon.tsx:
--------------------------------------------------------------------------------
1 | const PlusIcon: React.FC = () => (
2 |
17 | );
18 |
19 | export default PlusIcon;
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/alarm/request/SlackMessageRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.alarm.request;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import com.slack.api.model.Attachment;
5 | import java.util.List;
6 | import lombok.Getter;
7 | import lombok.RequiredArgsConstructor;
8 |
9 | @RequiredArgsConstructor
10 | @Getter
11 | public class SlackMessageRequest {
12 |
13 | @JsonProperty("channel")
14 | private final String userChannel;
15 |
16 | private final List attachments;
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/UnViewableException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.UnauthorizedException;
4 | import com.woowacourse.moamoa.studyroom.domain.Accessor;
5 |
6 | public class UnViewableException extends UnauthorizedException {
7 |
8 | public UnViewableException(final Long articleId, final Accessor accessor) {
9 | super(String.format("임시글[%d]를 접근자[%s]가 조회할 수 없습니다.", articleId, accessor));
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/logout-icon/LogoutIcon.tsx:
--------------------------------------------------------------------------------
1 | const LogoutIcon: React.FC = () => (
2 |
14 | );
15 |
16 | export default LogoutIcon;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/right-up-arrow-icon/RightUpArrowIcon.tsx:
--------------------------------------------------------------------------------
1 | const RightUpArrowIcon: React.FC = () => (
2 |
16 | );
17 |
18 | export default RightUpArrowIcon;
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/request/CommentRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.request;
2 |
3 | import javax.validation.constraints.NotBlank;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import org.hibernate.validator.constraints.Length;
8 |
9 | @Getter
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class CommentRequest {
13 |
14 | @NotBlank(message = "내용을 입력해 주세요.")
15 | @Length(max = 5000)
16 | private String content;
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/hooks/usePositiveNumberInput.ts:
--------------------------------------------------------------------------------
1 | const nums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
2 | const arrows = ['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'];
3 |
4 | const usePositiveNumberInput = () => {
5 | const handleKeyDown = (e: React.KeyboardEvent) => {
6 | const { key } = e;
7 | if (key !== 'Backspace' && !nums.includes(key) && !arrows.includes(key)) {
8 | e.preventDefault();
9 | return;
10 | }
11 | };
12 | return { handleKeyDown };
13 | };
14 |
15 | export default usePositiveNumberInput;
16 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/temp/CreatedTempArticleIdResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response.temp;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
8 | @Getter
9 | public class CreatedTempArticleIdResponse {
10 |
11 | private long draftArticleId;
12 |
13 | public CreatedTempArticleIdResponse(final long draftArticleId) {
14 | this.draftArticleId = draftArticleId;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/flex/Flex.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Flex, { type FlexBoxProps } from '@shared/flex/Flex';
4 |
5 | export default {
6 | title: 'Components/Flex',
7 | component: Flex,
8 | };
9 |
10 | const Template: Story = props => (
11 |
12 | div1
13 | div2
14 | div3
15 | div4
16 | flex-item
17 |
18 | );
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {};
22 |
--------------------------------------------------------------------------------
/frontend/src/layout/header/components/nav-button/NavButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import NavButton, { type NavButtonProps } from '@layout/header/components/nav-button/NavButton';
4 |
5 | export default {
6 | title: 'Layout/NavButton',
7 | component: NavButton,
8 | argTypes: {
9 | children: { controls: 'text' },
10 | },
11 | };
12 |
13 | const Template: Story = props => ;
14 |
15 | export const Default = Template.bind({});
16 | Default.args = {
17 | children: '➕ hihi',
18 | };
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/exception/TempArticleNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.NotFoundException;
4 | import com.woowacourse.moamoa.studyroom.domain.article.ArticleType;
5 |
6 | public class TempArticleNotFoundException extends NotFoundException {
7 |
8 | public TempArticleNotFoundException(final Long articleId, final ArticleType type) {
9 | super(String.format("%d의 식별자를 가진 임시글[%S]이 존재하지 않습니다.", articleId, type));
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/kebab-menu-icon/KebabMenuIcon.tsx:
--------------------------------------------------------------------------------
1 | const KebabMenuIcon: React.FC = () => (
2 |
16 | );
17 |
18 | export default KebabMenuIcon;
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/exception/UnwritableException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 | import com.woowacourse.moamoa.studyroom.domain.Accessor;
5 |
6 | public class UnwritableException extends BadRequestException {
7 |
8 | public UnwritableException(final Long studyId, final Accessor accessor, final String typeName) {
9 | super(String.format("스터디[%d]에 접근자[%s]가 %s의 게시글을 추가할 수 없습니다.", studyId, accessor, typeName));
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/checkbox/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Checkbox, { type CheckboxProps } from '@shared/checkbox/Checkbox';
4 |
5 | export default {
6 | title: 'Components/Checkbox',
7 | component: Checkbox,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | children: '주제',
15 | checked: true,
16 | dataTagId: 1,
17 | };
18 | Default.parameters = { controls: { exclude: ['dataTagId', 'onChange'] } };
19 |
--------------------------------------------------------------------------------
/frontend/src/layout/main/Main.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { mqDown } from '@styles/responsive';
4 |
5 | export type MainProps = {
6 | children: React.ReactNode;
7 | };
8 |
9 | const Main: React.FC = ({ children }) => {children};
10 |
11 | export const Self = styled.main`
12 | padding: 120px 0 80px;
13 | min-height: calc(100vh - 80px);
14 | height: auto;
15 |
16 | ${mqDown('md')} {
17 | padding-top: 90px;
18 | }
19 |
20 | ${mqDown('sm')} {
21 | padding-top: 80px;
22 | }
23 | `;
24 |
25 | export default Main;
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyOwnerAndTagsData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import com.woowacourse.moamoa.tag.query.response.TagSummaryData;
5 | import java.util.List;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 |
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | @Getter
13 | public class StudyOwnerAndTagsData {
14 |
15 | private MemberData owner;
16 | private List tags;
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/TestConfig.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance;
2 |
3 | import org.springframework.boot.test.context.TestConfiguration;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.test.web.client.MockRestServiceServer;
6 | import org.springframework.web.client.RestTemplate;
7 |
8 | @TestConfiguration
9 | public class TestConfig {
10 |
11 | @Bean
12 | public MockRestServiceServer mockServer(RestTemplate restTemplate) {
13 | return MockRestServiceServer.createServer(restTemplate);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/left-direction-icon/LeftDirectionIcon.tsx:
--------------------------------------------------------------------------------
1 | const LeftDirectionIcon: React.FC = () => (
2 |
16 | );
17 |
18 | export default LeftDirectionIcon;
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/pencil-icon/PencilIcon.tsx:
--------------------------------------------------------------------------------
1 | const PencilIcon: React.FC = () => (
2 |
18 | );
19 |
20 | export default PencilIcon;
21 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/right-direction-icon/RightDirectionIcon.tsx:
--------------------------------------------------------------------------------
1 | const RightDirectionIcon: React.FC = () => (
2 |
16 | );
17 |
18 | export default RightDirectionIcon;
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/x-mark-icon/XMarkIcon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import XMarkIcon, { type XMarkIconProps } from '@shared/icons/x-mark-icon/XMarkIcon';
4 |
5 | export default {
6 | title: 'Materials/Icons/XMarkIcon',
7 | component: XMarkIcon,
8 | argTypes: {
9 | size: {
10 | options: ['sm', 'base'],
11 | control: { type: 'radio' },
12 | },
13 | },
14 | };
15 |
16 | const Template: Story = ({ size }) => ;
17 |
18 | export const Default = Template.bind({});
19 | Default.args = {};
20 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Input, { type InputProps } from '@shared/input/Input';
4 |
5 | export default {
6 | title: 'Components/Input',
7 | component: Input,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | placeholder: '인풋입니다.',
15 | fontSize: 'md',
16 | disabled: false,
17 | invalid: false,
18 | fluid: false,
19 | };
20 | Default.parameters = { controls: { exclude: ['onChange', 'id'] } };
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ErrorPage } from '@error-page/ErrorPage';
2 | export { default as LoginRedirectPage } from '@login-redirect-page/LoginRedirectPage';
3 | export { default as MainPage } from '@main-page/MainPage';
4 | export { default as DetailPage } from '@detail-page/DetailPage';
5 | export { default as CreateStudyPage } from '@create-study-page/CreateStudyPage';
6 | export { default as EditStudyPage } from '@edit-study-page/EditStudyPage';
7 | export { default as MyStudyPage } from '@my-study-page/MyStudyPage';
8 | export { default as StudyRoomPage } from '@study-room-page/StudyRoomPage';
9 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/components/tab-button/TabButton.tsx:
--------------------------------------------------------------------------------
1 | import { ToggleButton } from '@shared/button';
2 |
3 | export type TabButtonProps = {
4 | children: string;
5 | isSelected: boolean;
6 | onClick: React.MouseEventHandler;
7 | };
8 |
9 | const TabButton: React.FC = ({ children, isSelected, onClick: handleClick }) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
17 | export default TabButton;
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/MoamoaApplication.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.scheduling.annotation.EnableAsync;
6 | import org.springframework.scheduling.annotation.EnableScheduling;
7 |
8 | @SpringBootApplication
9 | @EnableScheduling
10 | @EnableAsync
11 | public class MoamoaApplication {
12 |
13 | public static void main(final String[] args) {
14 | SpringApplication.run(MoamoaApplication.class, args);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/divider/Divider.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import tw from '@utils/tw';
4 |
5 | import Divider, { type DividerProps } from '@shared/divider/Divider';
6 |
7 | export default {
8 | title: 'Components/Divider',
9 | component: Divider,
10 | };
11 |
12 | const Template: Story = props => (
13 |
16 | );
17 |
18 | export const Default = Template.bind({});
19 | Default.args = {
20 | orientation: 'vertical',
21 | color: '#000051',
22 | space: '10px',
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/box-button/BoxButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { BoxButton, type BoxButtonProps } from '@shared/button';
4 |
5 | export default {
6 | title: 'Components/BoxButton',
7 | component: BoxButton,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | variant: 'primary',
15 | children: '스터디방 가입하기',
16 | fluid: false,
17 | disabled: false,
18 | };
19 | Default.parameters = { controls: { exclude: ['onClick', 'type'] } };
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/domain/Author.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.domain;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Embeddable;
5 | import lombok.AccessLevel;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 |
11 | @Getter
12 | @Embeddable
13 | @NoArgsConstructor(access = AccessLevel.PROTECTED)
14 | @EqualsAndHashCode
15 | @AllArgsConstructor
16 | public class Author {
17 |
18 | @Column(name = "author_id", nullable = false)
19 | private Long authorId;
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/folder-icon/FolderIcon.tsx:
--------------------------------------------------------------------------------
1 | const FolderIcon: React.FC = () => (
2 |
13 | );
14 |
15 | export default FolderIcon;
16 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/more-button/MoreButton.tsx:
--------------------------------------------------------------------------------
1 | import { TextButton } from '@shared/button';
2 |
3 | export type MoreButtonProps = {
4 | status: 'fold' | 'unfold';
5 | foldText: string;
6 | unfoldText: string;
7 | onClick: React.MouseEventHandler;
8 | };
9 |
10 | const MoreButton: React.FC = ({ status, foldText, unfoldText, onClick: handleClick }) => {
11 | return (
12 |
13 | {status === 'fold' ? unfoldText : foldText}
14 |
15 | );
16 | };
17 |
18 | export default MoreButton;
19 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/trashcan-icon/TrashcanIcon.tsx:
--------------------------------------------------------------------------------
1 | const TrashcanIcon: React.FC = () => (
2 |
18 | );
19 |
20 | export default TrashcanIcon;
21 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/AttachedTag.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain;
2 |
3 | import static lombok.AccessLevel.PROTECTED;
4 |
5 | import javax.persistence.Column;
6 | import javax.persistence.Embeddable;
7 | import lombok.AllArgsConstructor;
8 | import lombok.EqualsAndHashCode;
9 | import lombok.Getter;
10 | import lombok.NoArgsConstructor;
11 |
12 | @Embeddable
13 | @NoArgsConstructor(access = PROTECTED)
14 | @AllArgsConstructor
15 | @Getter
16 | @EqualsAndHashCode
17 | public class AttachedTag {
18 |
19 | @Column(name = "tag_id", nullable = false)
20 | private Long tagId;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/textarea/Textarea.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Textarea, { type TextareaProps } from '@shared/textarea/Textarea';
4 |
5 | export default {
6 | title: 'Components/Textarea',
7 | component: Textarea,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | placeholder: '인풋입니다.',
15 | fontSize: 'md',
16 | disabled: false,
17 | fluid: true,
18 | invalid: false,
19 | };
20 | Default.parameters = { controls: { exclude: ['onChange', 'id'] } };
21 |
--------------------------------------------------------------------------------
/backend/src/test/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | sql:
3 | init:
4 | mode: always
5 | jpa:
6 | hibernate:
7 | ddl-auto: validate
8 | properties:
9 | hibernate:
10 | dialect: org.hibernate.dialect.H2Dialect
11 | show_sql: true
12 | format_sql: true
13 |
14 | datasource:
15 | driver-class-name: org.h2.Driver
16 | username: sa
17 | password:
18 | url: jdbc:h2:mem:~/moamoa;MODE=MYSQL
19 | profiles:
20 | include: security
21 |
22 | logging:
23 | level:
24 | org:
25 | hibernate:
26 | type:
27 | descriptor:
28 | sql: trace
29 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/avatar/Avatar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Avatar, { type AvatarProps } from '@shared/avatar/Avatar';
4 |
5 | export default {
6 | title: 'Components/Avatar',
7 | component: Avatar,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80',
15 | name: '프로필 이미지',
16 | size: 'sm',
17 | };
18 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/query/data/MemberData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.query.data;
2 |
3 | import lombok.AccessLevel;
4 | import lombok.AllArgsConstructor;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.ToString;
9 |
10 | @Getter
11 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
12 | @AllArgsConstructor
13 | @EqualsAndHashCode
14 | @ToString
15 | public class MemberData {
16 |
17 | private Long id;
18 |
19 | private String username;
20 |
21 | private String imageUrl;
22 |
23 | private String profileUrl;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/temp/StudyResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response.temp;
2 |
3 | import com.woowacourse.moamoa.studyroom.query.data.StudyData;
4 | import lombok.AccessLevel;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
9 | @Getter
10 | public class StudyResponse {
11 |
12 | private long id;
13 |
14 | private String title;
15 |
16 | public StudyResponse(final StudyData data) {
17 | this.id = data.getId();
18 | this.title = data.getTitle();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/text-button/TextButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { TextButton, type TextButtonProps } from '@shared/button';
4 |
5 | export default {
6 | title: 'Components/TextButton',
7 | component: TextButton,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | children: '내 스터디',
15 | variant: 'primary',
16 | fluid: false,
17 | custom: {
18 | fontSize: 'md',
19 | },
20 | };
21 | Default.parameters = { controls: { exclude: ['onClick'] } };
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/SizeRequestConverter.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.controller.converter;
2 |
3 | import com.woowacourse.moamoa.studyroom.service.request.SizeRequest;
4 | import org.springframework.core.convert.converter.Converter;
5 | import org.springframework.stereotype.Component;
6 |
7 | @Component
8 | public class SizeRequestConverter implements Converter {
9 |
10 | @Override
11 | public SizeRequest convert(final String source) {
12 | return source.isBlank() ? SizeRequest.empty() : new SizeRequest(Integer.parseInt(source));
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/advice/response/ErrorResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.advice.response;
2 |
3 | public class ErrorResponse {
4 |
5 | private final String message;
6 | private int code;
7 |
8 | public ErrorResponse(final String message) {
9 | this.message = message;
10 | }
11 |
12 | public ErrorResponse(final String message, final int code) {
13 | this.message = message;
14 | this.code = code;
15 | }
16 |
17 | public String getMessage() {
18 | return message;
19 | }
20 |
21 | public int getCode() {
22 | return code;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/controller/converter/CategoryIdConverter.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.controller.converter;
2 |
3 | import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest;
4 | import org.springframework.core.convert.converter.Converter;
5 | import org.springframework.stereotype.Component;
6 |
7 | @Component
8 | public class CategoryIdConverter implements Converter {
9 |
10 | @Override
11 | public CategoryIdRequest convert(final String source) {
12 | return source.isBlank() ? CategoryIdRequest.empty() : new CategoryIdRequest(Long.parseLong(source));
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/service/request/AccessTokenRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.service.request;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.RequiredArgsConstructor;
6 |
7 | @RequiredArgsConstructor
8 | @EqualsAndHashCode
9 | public class AccessTokenRequest {
10 |
11 | @JsonProperty("client_id")
12 | private final String clientId;
13 |
14 | @JsonProperty("client_secret")
15 | private final String clientSecret;
16 |
17 | private final String code;
18 |
19 | public String getCode() {
20 | return code;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import CreateNewStudyButton, {
4 | type CreateNewStudyButtonProps,
5 | } from '@main-page/components/create-new-study-button/CreateNewStudyButton';
6 |
7 | export default {
8 | title: 'Pages/MainPage/CreateNewStudyButton',
9 | component: CreateNewStudyButton,
10 | };
11 |
12 | const Template: Story = props => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {};
16 | Default.parameters = { controls: { exclude: ['onClick'] } };
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/user-description/UserDescription.tsx:
--------------------------------------------------------------------------------
1 | import type { Member } from '@custom-types';
2 |
3 | import UserInfoItem from '@shared/user-info-item/UserInfoItem';
4 |
5 | export type UserDescriptionProps = {
6 | author: Member;
7 | description: string;
8 | };
9 |
10 | const UserDescription: React.FC = ({ author, description }) => {
11 | return (
12 |
13 | {description}
14 |
15 | );
16 | };
17 |
18 | export default UserDescription;
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/domain/AssociatedCommunity.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.domain;
2 |
3 | import static lombok.AccessLevel.PROTECTED;
4 |
5 | import javax.persistence.Column;
6 | import javax.persistence.Embeddable;
7 | import lombok.AllArgsConstructor;
8 | import lombok.EqualsAndHashCode;
9 | import lombok.Getter;
10 | import lombok.NoArgsConstructor;
11 |
12 | @Embeddable
13 | @NoArgsConstructor(access = PROTECTED)
14 | @AllArgsConstructor
15 | @Getter
16 | @EqualsAndHashCode
17 | public class AssociatedCommunity {
18 |
19 | @Column(name = "community_id", nullable = false)
20 | private Long communityId;
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/query/request/CategoryIdRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.query.request;
2 |
3 | public class CategoryIdRequest {
4 |
5 | private final Long value;
6 |
7 | public CategoryIdRequest(final long value) {
8 | this.value = value;
9 | }
10 |
11 | private CategoryIdRequest() {
12 | value = null;
13 | }
14 |
15 | public Long getValue() {
16 | return value;
17 | }
18 |
19 | public boolean isEmpty() {
20 | return value == null;
21 | }
22 |
23 | public static CategoryIdRequest empty() {
24 | return new CategoryIdRequest();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/domain/Category.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.domain;
2 |
3 | import static javax.persistence.EnumType.STRING;
4 | import static lombok.AccessLevel.PROTECTED;
5 |
6 | import javax.persistence.Entity;
7 | import javax.persistence.Enumerated;
8 | import javax.persistence.Id;
9 | import lombok.AllArgsConstructor;
10 | import lombok.Getter;
11 | import lombok.NoArgsConstructor;
12 |
13 | @Entity
14 | @Getter
15 | @AllArgsConstructor
16 | @NoArgsConstructor(access = PROTECTED)
17 | public class Category {
18 |
19 | @Id
20 | private Long id;
21 |
22 | @Enumerated(STRING)
23 | private CategoryName name;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/moamoa/fixtures/TagFixtures.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.fixtures;
2 |
3 | public class TagFixtures {
4 |
5 | public static final Long 자바_태그_아이디 = 1L;
6 | public static final String 자바_태그명 = "Java";
7 |
8 | public static final Long 우테코4기_태그_아이디 = 2L;
9 | public static final String 우테코4기_태그명 = "4기";
10 |
11 | public static final Long BE_태그_아이디 = 3L;
12 | public static final String BE_태그명 = "BE";
13 |
14 | public static final Long FE_태그_아이디 = 4L;
15 | public static final String FE_태그명 = "FE";
16 |
17 | public static final Long 리액트_태그_아이디 = 5L;
18 | public static final String 리액트_태그명 = "React";
19 | }
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/alarm/response/SlackUsersResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.alarm.response;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import java.util.List;
5 | import lombok.AccessLevel;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.ToString;
9 |
10 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
11 | @Getter
12 | @ToString
13 | public class SlackUsersResponse {
14 |
15 | @JsonProperty("members")
16 | private List responses;
17 |
18 | public SlackUsersResponse(final List responses) {
19 | this.responses = responses;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-page/components/description-tab/DescriptionTab.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 | import DescriptionTab from '@study-page/components/description-tab/DescriptionTab';
3 |
4 | import { FormProvider, useForm } from '@hooks/useForm';
5 |
6 | export default {
7 | title: 'Pages/CreateStudyPage/DescriptionTab',
8 | component: DescriptionTab,
9 | };
10 |
11 | const Template: Story = props => {
12 | const formMethods = useForm();
13 | return (
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {};
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/controller/interceptor/PathRequestMatcher.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.controller.interceptor;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import org.springframework.http.HttpMethod;
6 | import org.springframework.util.PathMatcher;
7 |
8 | @AllArgsConstructor
9 | @Getter
10 | public class PathRequestMatcher {
11 |
12 | private final String path;
13 | private final HttpMethod method;
14 |
15 | public boolean match(final PathMatcher pathMatcher, final String targetPath, final String pathMethod) {
16 | return pathMatcher.match(path, targetPath) && this.method.matches(pathMethod);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/service/response/MemberResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.service.response;
2 |
3 | import com.woowacourse.moamoa.member.domain.Member;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 |
8 | @NoArgsConstructor
9 | @AllArgsConstructor
10 | @Getter
11 | public class MemberResponse {
12 |
13 | private Long id;
14 | private String username;
15 | private String profileUrl;
16 | private String imageUrl;
17 |
18 | public MemberResponse(Member member) {
19 | this(member.getId(), member.getUsername(), member.getProfileUrl(), member.getImageUrl());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/test/cors/CorsAcceptanceTest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance.test.cors;
2 |
3 | import com.woowacourse.acceptance.AcceptanceTest;
4 | import io.restassured.RestAssured;
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Test;
7 |
8 | class CorsAcceptanceTest extends AcceptanceTest {
9 |
10 | @DisplayName("cors 적용 여부 확인")
11 | @Test
12 | void corsTest() {
13 | RestAssured.given()
14 | .header("Access-Control-Request-Method", "GET")
15 | .when()
16 | .options("/api/studies")
17 | .then()
18 | .statusCode(200);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/api/my-studies/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { checkType, hasOwnProperty, isArray, isObject } from '@utils';
4 |
5 | import { type ApiMyStudies } from '@api/my-studies';
6 | import { checkMyStudy } from '@api/my-study/typeChecker';
7 |
8 | export const checkMyStudies = (data: unknown): ApiMyStudies['get']['responseData'] => {
9 | if (!isObject(data)) throw new AxiosError(`MyStudies does not have correct type: object`);
10 |
11 | if (!hasOwnProperty(data, 'studies')) throw new AxiosError('MyStudies does not have some properties');
12 |
13 | return {
14 | studies: checkType(data.studies, isArray).map(study => checkMyStudy(study)),
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/image/Image.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import Image, { type ImageProps } from '@shared/image/Image';
4 |
5 | export default {
6 | title: 'Components/Image',
7 | component: Image,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | shape: 'rectangular',
15 | src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80',
16 | alt: '프로필 이미지',
17 | custom: {
18 | width: '5rem',
19 | height: '5rem',
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/query/data/MyStudySummaryData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.query.data;
2 |
3 | import com.woowacourse.moamoa.study.domain.StudyStatus;
4 | import lombok.AllArgsConstructor;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 |
9 | @NoArgsConstructor
10 | @AllArgsConstructor
11 | @EqualsAndHashCode
12 | @Getter
13 | public class MyStudySummaryData {
14 |
15 | private Long id;
16 | private String title;
17 | private StudyStatus studyStatus;
18 | private Integer currentMemberCount;
19 | private Integer maxMemberCount;
20 | private String startDate;
21 | private String endDate;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/api/tags/index.ts:
--------------------------------------------------------------------------------
1 | import { type AxiosError } from 'axios';
2 | import { useQuery } from 'react-query';
3 |
4 | import type { Tag } from '@custom-types';
5 |
6 | import axiosInstance from '@api/axiosInstance';
7 | import { checkTags } from '@api/tags/typeChecker';
8 |
9 | export type ApiTags = {
10 | get: {
11 | responseData: {
12 | tags: Array;
13 | };
14 | };
15 | };
16 |
17 | export const getTags = async () => {
18 | const response = await axiosInstance.get(`/api/tags`);
19 | return checkTags(response.data);
20 | };
21 |
22 | export const useGetTags = () => {
23 | return useQuery('filters', getTags);
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import AccessTokenController from '@auth/accessTokenController';
4 |
5 | import { LoginContext } from '@context/login/LoginProvider';
6 |
7 | export const useAuth = () => {
8 | const { isLoggedIn, setIsLoggedIn } = useContext(LoginContext);
9 |
10 | const login = (accesssToken: string, expiredTime: number) => {
11 | AccessTokenController.save(accesssToken, expiredTime);
12 | setIsLoggedIn(true);
13 | };
14 |
15 | const logout = () => {
16 | AccessTokenController.clear();
17 | setIsLoggedIn(false);
18 | alert('로그아웃 되었습니다.');
19 | window.location.reload();
20 | };
21 |
22 | return { isLoggedIn, login, logout };
23 | };
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/query/data/CommentData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import java.time.LocalDate;
5 | import lombok.AccessLevel;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 |
11 | @Getter
12 | @AllArgsConstructor
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | @EqualsAndHashCode
15 | public class CommentData {
16 |
17 | private Long id;
18 | private MemberData member;
19 | private LocalDate createdDate;
20 | private LocalDate lastModifiedDate;
21 | private String content;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/x-mark-icon/XMarkIcon.tsx:
--------------------------------------------------------------------------------
1 | export type XMarkIconProps = {
2 | size?: 'sm' | 'base';
3 | };
4 |
5 | const XMarkIcon: React.FC = () => {
6 | return (
7 |
10 | );
11 | };
12 |
13 | export default XMarkIcon;
14 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/study-member-card/StudyMemberCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import StudyMemberCard, { type StudyMemberCardProps } from '@detail-page/components/study-member-card/StudyMemberCard';
4 |
5 | export default {
6 | title: 'Pages/DetailPage/StudyMemberCard',
7 | component: StudyMemberCard,
8 | };
9 |
10 | const Template: Story = props => (
11 |
12 |
13 |
14 | );
15 |
16 | export const Default = Template.bind({});
17 | Default.args = {
18 | imageUrl: 'https://picsum.photos/id/186/200/200',
19 | username: 'airman5573',
20 | };
21 |
--------------------------------------------------------------------------------
/frontend/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const webpack = require('webpack');
3 | const { join } = require('path');
4 | const { merge } = require('webpack-merge');
5 |
6 | require('dotenv').config({ path: join(__dirname, '../env/.env.prod') });
7 |
8 | const common = require('./webpack.common');
9 |
10 | module.exports = merge(common, {
11 | mode: 'production',
12 | plugins: [
13 | new webpack.DefinePlugin({
14 | 'process.env.API_URL': JSON.stringify(process.env.API_URL),
15 | 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID),
16 | 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL),
17 | }),
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/ReviewData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import java.time.LocalDate;
5 | import lombok.AccessLevel;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 |
11 | @Getter
12 | @AllArgsConstructor
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | @EqualsAndHashCode
15 | public class ReviewData {
16 |
17 | private Long id;
18 | private MemberData member;
19 | private LocalDate createdDate;
20 | private LocalDate lastModifiedDate;
21 | private String content;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/icon-button/IconButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { IconButton, type IconButtonProps } from '@shared/button';
4 | import { PlusIcon } from '@shared/icons';
5 |
6 | export default {
7 | title: 'Components/IconButton',
8 | component: IconButton,
9 | };
10 |
11 | const Template: Story = props => ;
12 |
13 | export const Default = Template.bind({});
14 | Default.args = {
15 | children: ,
16 | ariaLabel: '더하기',
17 | variant: 'primary',
18 | custom: {
19 | width: '30px',
20 | height: '30px',
21 | },
22 | };
23 | Default.parameters = { controls: { exclude: ['children', 'onClick'] } };
24 |
--------------------------------------------------------------------------------
/frontend/src/layout/footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | import { type CustomCSS } from '@styles/custom-css';
5 |
6 | export type FooterProps = {
7 | children?: React.ReactNode;
8 | custom?: CustomCSS<'marginBottom'>;
9 | };
10 |
11 | const Footer: React.FC = ({ children = '그린론 디우 베루스 병민 짱구 태태', custom }) => {
12 | return {children};
13 | };
14 |
15 | export default Footer;
16 |
17 | const Self = styled.footer`
18 | ${({ theme }) => css`
19 | padding: 24px 0;
20 |
21 | text-align: center;
22 | color: ${theme.colors.secondary.dark};
23 | border-top: 1px solid ${theme.colors.secondary.dark};
24 | `}
25 | `;
26 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/steps/SetRequiredDataToCreatingStudySteps.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance.steps;
2 |
3 | import com.woowacourse.moamoa.study.service.request.StudyRequestBuilder;
4 | import java.time.LocalDate;
5 |
6 | public class SetRequiredDataToCreatingStudySteps {
7 |
8 | private final String token;
9 | private final StudyRequestBuilder builder;
10 |
11 | SetRequiredDataToCreatingStudySteps(final String token, final StudyRequestBuilder builder) {
12 | this.token = token;
13 | this.builder = builder;
14 | }
15 |
16 | public CreatingStudyStep 시작일자는(LocalDate date) {
17 | builder.startDate(date);
18 | return new CreatingStudyStep(token, builder);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const webpack = require('webpack');
3 | const { join } = require('path');
4 | const { merge } = require('webpack-merge');
5 |
6 | require('dotenv').config({ path: join(__dirname, '../env/.env.dev') });
7 |
8 | const common = require('./webpack.common');
9 |
10 | module.exports = merge(common, {
11 | mode: 'production',
12 | devtool: 'source-map',
13 | plugins: [
14 | new webpack.DefinePlugin({
15 | 'process.env.API_URL': JSON.stringify(process.env.API_URL),
16 | 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID),
17 | 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL),
18 | }),
19 | ],
20 | });
21 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/review/Reviewer.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.review;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Embeddable;
5 | import lombok.AccessLevel;
6 | import lombok.EqualsAndHashCode;
7 | import lombok.NoArgsConstructor;
8 |
9 | @Embeddable
10 | @NoArgsConstructor(access = AccessLevel.PROTECTED)
11 | @EqualsAndHashCode
12 | public class Reviewer {
13 |
14 | @Column(name = "member_id", nullable = false)
15 | private Long memberId;
16 |
17 | public Reviewer(final Long memberId) {
18 | this.memberId = memberId;
19 | }
20 |
21 | boolean isSameMemberId(final Long memberId) {
22 | return this.memberId.equals(memberId);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 | // Import commands.js using ES2015 syntax:
16 | import './commands';
17 |
18 | // Alternatively you can use CommonJS syntax:
19 | // require('./commands')
20 |
--------------------------------------------------------------------------------
/frontend/src/context/search/SearchProvider.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, createContext, useState } from 'react';
2 |
3 | import { noop } from '@utils';
4 |
5 | type SearchProviderProps = {
6 | children: ReactNode;
7 | };
8 |
9 | type KeywordType = string;
10 |
11 | type ContextType = {
12 | keyword: KeywordType;
13 | setKeyword: React.Dispatch>;
14 | };
15 |
16 | export const SearchContext = createContext({
17 | keyword: '',
18 | setKeyword: noop,
19 | });
20 |
21 | export const SearchProvider = ({ children }: SearchProviderProps) => {
22 | const [keyword, setKeyword] = useState('');
23 | return {children};
24 | };
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/query/data/OwnerData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.query.data;
2 |
3 | import static lombok.AccessLevel.PRIVATE;
4 |
5 | import java.time.LocalDate;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 | import lombok.ToString;
11 |
12 | @Getter
13 | @NoArgsConstructor(access = PRIVATE)
14 | @AllArgsConstructor
15 | @EqualsAndHashCode
16 | @ToString
17 | public class OwnerData {
18 |
19 | private Long id;
20 |
21 | private String username;
22 |
23 | private String imageUrl;
24 |
25 | private String profileUrl;
26 |
27 | private LocalDate participationDate;
28 |
29 | private int numberOfStudy;
30 | }
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/Accessor.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain;
2 |
3 | public class Accessor {
4 |
5 | private final Long memberId;
6 | private final Long studyId;
7 |
8 | public Accessor(final Long memberId, final Long studyId) {
9 | this.memberId = memberId;
10 | this.studyId = studyId;
11 | }
12 |
13 | public Long getStudyId() {
14 | return studyId;
15 | }
16 |
17 | public Long getMemberId() {
18 | return memberId;
19 | }
20 |
21 | @Override
22 | public String toString() {
23 | return "Accessor{" +
24 | "memberId=" + memberId +
25 | ", studyId=" + studyId +
26 | '}';
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/filter-slide-button/FilterSlideButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import FilterSlideButton, {
4 | type FilterSlideButtonProps,
5 | } from '@main-page/components/filter-slide-button/FilterSlideButton';
6 |
7 | export default {
8 | title: 'Pages/MainPage/FilterSlideButton',
9 | component: FilterSlideButton,
10 | argTypes: {
11 | direction: { controls: 'text' },
12 | },
13 | };
14 |
15 | const Template: Story = props => (
16 |
17 |
18 |
19 | );
20 |
21 | export const Default = Template.bind({});
22 | Default.args = {
23 | direction: 'right',
24 | };
25 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/page-title/PageTitle.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | export type PageTitleProps = {
5 | children: React.ReactNode;
6 | align?: 'left' | 'right' | 'center';
7 | };
8 |
9 | const PageTitle: React.FC = ({ children, align = 'left' }) => {
10 | return {children};
11 | };
12 |
13 | export default PageTitle;
14 |
15 | type StyledPageTitleProps = Required>;
16 |
17 | export const Self = styled.h1`
18 | ${({ theme, align }) => css`
19 | padding: 20px 0;
20 |
21 | font-size: ${theme.fontSize.xxl};
22 | font-weight: ${theme.fontWeight.bold};
23 | text-align: ${align};
24 | `}
25 | `;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/page-wrapper/PageWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | import type { CssLength } from '@custom-types';
5 |
6 | export type PageWrapperProps = {
7 | children: React.ReactNode;
8 | space?: CssLength;
9 | };
10 |
11 | const PageWrapper: React.FC = ({ children, space = '20px' }) => {
12 | return {children};
13 | };
14 |
15 | export default PageWrapper;
16 |
17 | type StyledPageWrapperProps = Required>;
18 |
19 | const Self = styled.div`
20 | ${({ space }) => css`
21 | width: 100%;
22 | max-width: 1280px;
23 | margin: 0 auto;
24 | padding: 0 ${space};
25 | `}
26 | `;
27 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/review/AssociatedStudy.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.review;
2 |
3 | import static lombok.AccessLevel.PROTECTED;
4 |
5 | import javax.persistence.Column;
6 | import javax.persistence.Embeddable;
7 | import lombok.AllArgsConstructor;
8 | import lombok.EqualsAndHashCode;
9 | import lombok.Getter;
10 | import lombok.NoArgsConstructor;
11 |
12 | @Embeddable
13 | @NoArgsConstructor(access = PROTECTED)
14 | @AllArgsConstructor
15 | @Getter
16 | @EqualsAndHashCode
17 | public class AssociatedStudy {
18 |
19 | @Column(name = "study_id", nullable = false)
20 | private Long studyId;
21 |
22 | boolean isSameStudyId(final Long studyId) {
23 | return this.studyId.equals(studyId);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/LinkArticleData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import java.time.LocalDate;
5 | import lombok.AccessLevel;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 |
11 | @NoArgsConstructor(access = AccessLevel.PROTECTED)
12 | @AllArgsConstructor
13 | @Getter
14 | @EqualsAndHashCode
15 | public class LinkArticleData {
16 |
17 | private Long id;
18 | private MemberData memberData;
19 | private String linkUrl;
20 | private String description;
21 | private LocalDate createdDate;
22 | private LocalDate lastModifiedDate;
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/list-item/ListItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import ListItem, { type ListItemProps } from '@shared/list-item/ListItem';
4 |
5 | export default {
6 | title: 'Components/ListItem',
7 | component: ListItem,
8 | argTypes: {
9 | isOpen: { controls: 'boolean' },
10 | },
11 | };
12 |
13 | const Template: Story = props => ;
14 |
15 | export const Default = Template.bind({});
16 | Default.args = {
17 | title: '게시글 제목입니다.',
18 | userInfo: {
19 | src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80',
20 | username: 'person',
21 | },
22 | subInfo: '2022-10-10',
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/study-float-box/StudyFloatBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import StudyFloatBox, { type StudyFloatBoxProps } from '@detail-page/components/study-float-box/StudyFloatBox';
6 |
7 | export default {
8 | title: 'Pages/DetailPage/StudyFloatBox',
9 | component: StudyFloatBox,
10 | };
11 |
12 | const Template: Story = props => (
13 |
14 | noop} />
15 |
16 | );
17 |
18 | export const Default = Template.bind({});
19 | Default.args = {
20 | enrollmentEndDate: '2022-07-28',
21 | currentMemberCount: 8,
22 | maxMemberCount: 14,
23 | ownerName: 'airman5573',
24 | };
25 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | .gradle
3 | build/
4 | !gradle/wrapper/gradle-wrapper.jar
5 | !**/src/main/**/build/
6 | !**/src/test/**/build/
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 | bin/
17 | !**/src/main/**/bin/
18 | !**/src/test/**/bin/
19 |
20 | ### IntelliJ IDEA ###
21 | .idea
22 | *.iws
23 | *.iml
24 | *.ipr
25 | out/
26 | !**/src/main/**/out/
27 | !**/src/test/**/out/
28 |
29 | ### NetBeans ###
30 | /nbproject/private/
31 | /nbbuild/
32 | /dist/
33 | /nbdist/
34 | /.nb-gradle/
35 |
36 | ### VS Code ###
37 | .vscode/
38 |
39 | /src/main/resources/application-security.yml
40 | /src/test/resources/application-security.yml
41 | /src/main/resources/static/docs/index.html
42 | /src/docs/asciidoc/index.html
43 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/section-title/SectionTitle.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | export type SectionTitleProps = {
5 | children: React.ReactNode;
6 | align?: 'left' | 'right' | 'center';
7 | };
8 |
9 | const SectionTitle: React.FC = ({ children, align = 'left' }) => {
10 | return {children};
11 | };
12 |
13 | export default SectionTitle;
14 |
15 | type StyledSectionTitleProps = Required>;
16 |
17 | export const Self = styled.h2`
18 | ${({ theme, align }) => css`
19 | padding: 20px 0;
20 |
21 | font-size: ${theme.fontSize.xl};
22 | font-weight: ${theme.fontWeight.bold};
23 | text-align: ${align};
24 | `}
25 | `;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/label/Label.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | export type LabelProps = {
5 | children?: React.ReactNode;
6 | htmlFor?: string;
7 | hidden?: boolean;
8 | };
9 |
10 | const Label: React.FC = ({ children, htmlFor, hidden }) => {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default Label;
19 |
20 | type StyledLabelProps = Pick;
21 |
22 | export const Self = styled.label`
23 | ${({ hidden }) => css`
24 | ${hidden &&
25 | css`
26 | display: block;
27 |
28 | height: 0;
29 | width: 0;
30 |
31 | visibility: hidden;
32 | `}
33 | `}
34 | `;
35 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/linked-button/LinkedButton.tsx:
--------------------------------------------------------------------------------
1 | // LinkButton의 필요성
2 | // Link만 사용했을 경우 내부에 있는 버튼 핸들러가 작동하기 전에 페이지 이동이 됩니다.
3 | import { useNavigate } from 'react-router-dom';
4 |
5 | import styled from '@emotion/styled';
6 |
7 | export type LinkedButtonProps = {
8 | children: React.ReactNode;
9 | to: string;
10 | };
11 |
12 | const LinkedButton: React.FC = ({ children, to }) => {
13 | const navigate = useNavigate();
14 |
15 | const handleClick = () => {
16 | navigate(to);
17 | };
18 |
19 | return {children};
20 | };
21 |
22 | export default LinkedButton;
23 |
24 | const Self = styled.button`
25 | width: 100%;
26 | height: fit-content;
27 | background: transparent;
28 | border: none;
29 | text-align: unset;
30 | `;
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/domain/repository/StudyRepository.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.domain.repository;
2 |
3 | import com.woowacourse.moamoa.study.domain.Study;
4 | import java.util.List;
5 | import java.util.Optional;
6 | import javax.persistence.LockModeType;
7 | import org.springframework.data.jpa.repository.Lock;
8 | import org.springframework.data.jpa.repository.Query;
9 | import org.springframework.data.repository.query.Param;
10 |
11 | public interface StudyRepository {
12 |
13 | Study save(Study study);
14 |
15 | Optional findById(Long id);
16 |
17 | @Lock(LockModeType.PESSIMISTIC_WRITE)
18 | @Query("select s from Study s where s.id = :id")
19 | Optional findByIdUpdateFor(@Param("id") Long id);
20 |
21 | List findAll();
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 모아모아
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-page/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { mqDown } from '@styles/responsive';
4 |
5 | const sidebarWidth = 280;
6 | const mainGabSidebar = 40;
7 |
8 | export const Container = styled.div`
9 | display: flex;
10 | column-gap: 40px;
11 |
12 | ${mqDown('md')} {
13 | flex-direction: column;
14 | column-gap: 0;
15 | }
16 | `;
17 |
18 | export const Main = styled.div`
19 | flex-grow: 1;
20 | max-width: calc(100% - ${mainGabSidebar}px - ${sidebarWidth}px);
21 |
22 | ${mqDown('md')} {
23 | max-width: 100%;
24 | margin-bottom: 15px;
25 | }
26 | `;
27 |
28 | export const Sidebar = styled.ul`
29 | width: 280px;
30 |
31 | & > li {
32 | margin-bottom: 15px;
33 | }
34 |
35 | ${mqDown('md')} {
36 | min-width: 100%;
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/controller/converter/ArticleTypeConverter.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.controller.converter;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.article.ArticleType;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.core.convert.converter.Converter;
7 | import org.springframework.stereotype.Component;
8 |
9 | @Component
10 | public class ArticleTypeConverter implements Converter {
11 |
12 | Logger logger = LoggerFactory.getLogger(ArticleTypeConverter.class);
13 |
14 | @Override
15 | public ArticleType convert(final String source) {
16 | logger.info("{}", ArticleType.valueOf(source.toUpperCase()));
17 | return ArticleType.valueOf(source.toUpperCase());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/letter-counter/LetterCounter.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 | import styled from '@emotion/styled';
3 |
4 | export type LetterCounterProps = {
5 | count: number;
6 | maxCount: number;
7 | };
8 |
9 | const LetterCounter: React.FC = ({ count, maxCount }) => {
10 | return (
11 |
12 | {Math.min(count, maxCount)}/{maxCount}
13 |
14 | );
15 | };
16 |
17 | export default LetterCounter;
18 |
19 | const Self = styled.div`
20 | ${({ theme }) => css`
21 | font-size: ${theme.fontSize.sm};
22 | color: ${theme.colors.secondary.dark};
23 | & > span {
24 | color: ${theme.colors.secondary.dark};
25 | }
26 | & > span:first-of-type {
27 | color: ${theme.colors.black};
28 | }
29 | `}
30 | `;
31 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/create-new-study-button/CreateNewStudyButton.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | import { IconButton } from '@shared/button';
4 | import { PencilIcon } from '@shared/icons';
5 |
6 | export type CreateNewStudyButtonProps = {
7 | onClick?: React.MouseEventHandler;
8 | };
9 |
10 | const CreateNewStudyButton = ({ onClick: handleClick }: CreateNewStudyButtonProps) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default CreateNewStudyButton;
21 |
22 | const Self = styled.div`
23 | position: fixed;
24 | right: 60px;
25 | bottom: 50px;
26 | z-index: 3;
27 | `;
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/AuthorResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import lombok.AllArgsConstructor;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.ToString;
9 |
10 | @AllArgsConstructor
11 | @Getter
12 | @EqualsAndHashCode
13 | @NoArgsConstructor
14 | @ToString
15 | public class AuthorResponse {
16 |
17 | private Long id;
18 | private String username;
19 | private String imageUrl;
20 | private String profileUrl;
21 |
22 | public AuthorResponse(MemberData memberData) {
23 | this(memberData.getId(), memberData.getUsername(), memberData.getImageUrl(), memberData.getProfileUrl());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/moamoa/auth/config/AuthenticationExtractorTest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.config;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.junit.jupiter.api.DisplayName;
6 | import org.junit.jupiter.api.Test;
7 | import org.springframework.mock.web.MockHttpServletRequest;
8 |
9 | class AuthenticationExtractorTest {
10 |
11 | @DisplayName("jwt-token을 받았을 때 payload를 반환한다.")
12 | @Test
13 | void getPayload() {
14 | final MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
15 | mockHttpServletRequest.addHeader("Authorization", "Bearer token");
16 |
17 | final String expected = AuthenticationExtractor.extract(mockHttpServletRequest);
18 |
19 | assertThat(expected).isEqualTo("token");
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/api/my-studies/index.ts:
--------------------------------------------------------------------------------
1 | import { type AxiosError } from 'axios';
2 | import { useQuery } from 'react-query';
3 |
4 | import type { MyStudy } from '@custom-types';
5 |
6 | import axiosInstance from '@api/axiosInstance';
7 | import { checkMyStudies } from '@api/my-studies/typeChecker';
8 |
9 | export const QK_MY_STUDIES = 'my-studies';
10 |
11 | export type ApiMyStudies = {
12 | get: {
13 | responseData: {
14 | studies: Array;
15 | };
16 | };
17 | };
18 |
19 | export const getMyStudies = async () => {
20 | const response = await axiosInstance.get(`/api/my/studies`);
21 | return checkMyStudies(response.data);
22 | };
23 |
24 | export const useGetMyStudies = () => {
25 | return useQuery(QK_MY_STUDIES, getMyStudies);
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/toggle-button/ToggleButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 | import { useState } from 'react';
3 |
4 | import { ToggleButton, type ToggleButtonProps } from '@shared/button';
5 |
6 | export default {
7 | title: 'Components/ToggleButton',
8 | component: ToggleButton,
9 | argTypes: {
10 | children: { controls: 'text' },
11 | },
12 | };
13 |
14 | const Template: Story = props => {
15 | const [isChecked, setIsChecked] = useState(false);
16 |
17 | return setIsChecked(prev => !prev)} />;
18 | };
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {
22 | children: '➕ hihi',
23 | fluid: false,
24 | };
25 | Default.parameters = { controls: { exclude: ['checked', 'onClick'] } };
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/alarm/response/SlackUserResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.alarm.response;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import com.woowacourse.moamoa.alarm.SlackUserProfile;
5 | import lombok.AccessLevel;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 | import lombok.ToString;
9 |
10 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
11 | @Getter
12 | @ToString
13 | public class SlackUserResponse {
14 |
15 | @JsonProperty("id")
16 | private String channel;
17 |
18 | @JsonProperty("profile")
19 | private SlackUserProfile slackUserProfile;
20 |
21 | public SlackUserResponse(final String channel, final SlackUserProfile slackUserProfile) {
22 | this.channel = channel;
23 | this.slackUserProfile = slackUserProfile;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/query/data/ParticipatingMemberData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.query.data;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import java.time.LocalDate;
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 | @Getter
13 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
14 | @AllArgsConstructor
15 | @EqualsAndHashCode
16 | @ToString
17 | public class ParticipatingMemberData {
18 |
19 | private Long id;
20 |
21 | private String username;
22 |
23 | private String imageUrl;
24 |
25 | private String profileUrl;
26 |
27 | private LocalDate participationDate;
28 |
29 | private int numberOfStudy;
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/devploy-frontend-prod.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Frontend Prod
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | defaults:
7 | run:
8 | working-directory: frontend
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [16.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 |
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | cache: 'npm'
27 | cache-dependency-path: "./frontend/package-lock.json"
28 |
29 | - name: Install
30 | run: npm install
31 |
32 | - name: Build
33 | run: npm run build
34 |
35 | - name: Deploy
36 | run: curl ${{ secrets.FE_PROD_DEPLOY_REQUEST_URL }}
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/study-wide-float-box/StudyWideFloatBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import StudyWideFloatBox, {
6 | type StudyWideFloatBoxProps,
7 | } from '@detail-page/components/study-wide-float-box/StudyWideFloatBox';
8 |
9 | export default {
10 | title: 'Pages/DetailPage/StudyWideFloatBox',
11 | component: StudyWideFloatBox,
12 | };
13 |
14 | const Template: Story = props => (
15 |
20 | noop} />
21 |
22 | );
23 |
24 | export const Default = Template.bind({});
25 | Default.args = {
26 | enrollmentEndDate: '2022-07-28',
27 | currentMemberCount: 8,
28 | maxMemberCount: 14,
29 | };
30 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/review-tab-panel/components/reivew-form/ReviewForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import ReviewForm, { type ReviewFormProps } from '@review-tab/components/reivew-form/ReviewForm';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/ReviewForm',
7 | component: ReviewForm,
8 | argTypes: {},
9 | };
10 |
11 | const Template: Story = props => ;
12 |
13 | export const Default = Template.bind({});
14 | Default.args = {
15 | studyId: 1,
16 | author: {
17 | id: 1,
18 | username: 'your-name',
19 | profileUrl: '/',
20 | imageUrl:
21 | 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60',
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/alarm/AsyncConfig.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.alarm;
2 |
3 | import java.util.concurrent.Executor;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
6 | import org.springframework.scheduling.annotation.EnableAsync;
7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8 |
9 | @Configuration
10 | @EnableAsync
11 | public class AsyncConfig extends AsyncConfigurerSupport {
12 |
13 | @Override
14 | public Executor getAsyncExecutor() {
15 | ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
16 | taskExecutor.setCorePoolSize(1);
17 | taskExecutor.setQueueCapacity(10);
18 | taskExecutor.initialize();
19 | return taskExecutor;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/pages/my-study-page/components/my-study-card/MyStudyCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import MyStudyCard, { type MyStudyCardProps } from '@my-study-page/components/my-study-card/MyStudyCard';
4 |
5 | export default {
6 | title: 'Pages/MyStudyPage/MyStudyCard',
7 | component: MyStudyCard,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | title: '2022 모아모아 스터디',
15 | ownerName: 'airman5573',
16 | tags: [
17 | {
18 | id: 1,
19 | name: 'Java',
20 | },
21 | {
22 | id: 2,
23 | name: 'JS',
24 | },
25 | {
26 | id: 3,
27 | name: 'FE',
28 | },
29 | ],
30 | startDate: '2022-08-13',
31 | endDate: '2022-08-20',
32 | done: false,
33 | };
34 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-page/components/publish/Publish.tsx:
--------------------------------------------------------------------------------
1 | import { BoxButton } from '@shared/button';
2 | import MetaBox from '@shared/meta-box/MetaBox';
3 |
4 | type PublishProps = {
5 | title?: string;
6 | buttonText?: string;
7 | };
8 |
9 | const Publish = ({ title = '스터디 개설', buttonText = '개설하기' }: PublishProps) => {
10 | return (
11 |
12 | {title}
13 |
14 | {buttonText}
15 |
16 |
17 | );
18 | };
19 |
20 | export default Publish;
21 |
22 | type PublishButtonProps = {
23 | children: string;
24 | };
25 | const PublishButton: React.FC = ({ children }) => (
26 |
27 | {children}
28 |
29 | );
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/config/SchedulerConfig.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.config;
2 |
3 | import java.util.List;
4 | import lombok.RequiredArgsConstructor;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.scheduling.annotation.SchedulingConfigurer;
7 | import org.springframework.scheduling.config.ScheduledTaskRegistrar;
8 | import org.springframework.scheduling.config.TriggerTask;
9 |
10 | @Configuration
11 | @RequiredArgsConstructor
12 | public class SchedulerConfig implements SchedulingConfigurer {
13 |
14 | private final List tasks;
15 |
16 | @Override
17 | public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) {
18 | for (TriggerTask task : tasks) {
19 | taskRegistrar.addTriggerTask(task);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/entity/BaseEntity.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.entity;
2 |
3 | import java.time.LocalDateTime;
4 | import javax.persistence.Column;
5 | import javax.persistence.EntityListeners;
6 | import javax.persistence.MappedSuperclass;
7 | import lombok.Getter;
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 | @EntityListeners(AuditingEntityListener.class)
13 | @MappedSuperclass
14 | @Getter
15 | public class BaseEntity {
16 |
17 | @CreatedDate
18 | @Column(updatable = false, nullable = false)
19 | private LocalDateTime createdDate;
20 |
21 | @LastModifiedDate
22 | @Column(nullable = false)
23 | private LocalDateTime lastModifiedDate;
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/SizeRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.request;
2 |
3 | public class SizeRequest {
4 |
5 | private final Integer value;
6 |
7 | private SizeRequest() {
8 | value = null;
9 | }
10 |
11 | public SizeRequest(final int value) {
12 | this.value = value;
13 | }
14 |
15 | public Integer getValue() {
16 | return value;
17 | }
18 |
19 | public boolean isEmpty() {
20 | return value == null;
21 | }
22 |
23 | public boolean isMoreThan(int value) {
24 | if (isEmpty()) {
25 | throw new IllegalStateException("SizeRequest에 value가 null 입니다.");
26 | }
27 | return getValue() > value;
28 | }
29 |
30 | public static SizeRequest empty() {
31 | return new SizeRequest();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/mocks/links.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": [
3 | {
4 | "id": 1,
5 | "author": {
6 | "username": "nan-noo",
7 | "imageUrl": "https://picsum.photos/id/50/200/300",
8 | "profileUrl": "github.com/greenlawn",
9 | "id": 2
10 | },
11 | "linkUrl": "https://naver.com",
12 | "description": "네이버 홈",
13 | "createdDate": "2022-08-13",
14 | "lastModifiedDate": "2022-08-13"
15 | },
16 | {
17 | "id": 2,
18 | "author": {
19 | "username": "nan-noo",
20 | "imageUrl": "https://picsum.photos/id/50/200/300",
21 | "profileUrl": "github.com/greenlawn",
22 | "id": 2
23 | },
24 | "linkUrl": "https://www.npmjs.com/package/he",
25 | "description": "이것도 테스트해보자!",
26 | "createdDate": "2022-08-13",
27 | "lastModifiedDate": "2022-08-13"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/study-review-card/StudyReviewCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import StudyReviewCard, { type StudyReviewCardProps } from '@detail-page/components/study-review-card/StudyReviewCard';
4 |
5 | export default {
6 | title: 'Pages/DetailPage/StudyReviewCard',
7 | component: StudyReviewCard,
8 | };
9 |
10 | const Template: Story = props => (
11 |
12 |
13 |
14 | );
15 |
16 | export const Default = Template.bind({});
17 | Default.args = {
18 | imageUrl:
19 | 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80',
20 | username: 'moamoa',
21 | reviewDate: '2020-07-19',
22 | review: '너무 좋아요~~ 짱짱',
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/user-info-item/UserInfoItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import UserInfoItem, { type UserInfoItemProps } from '@shared/user-info-item/UserInfoItem';
4 |
5 | export default {
6 | title: 'Components/UserInfoItem',
7 | component: UserInfoItem,
8 | };
9 |
10 | const Template: Story = props => (
11 |
12 | {props.children}
13 | {props.name}
14 |
15 | );
16 |
17 | export const Default = Template.bind({});
18 | Default.args = {
19 | src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80',
20 | children: '제목',
21 | name: 'person',
22 | size: 'lg',
23 | };
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/LinkArticleRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.request;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.link.LinkContent;
4 | import javax.validation.constraints.NotBlank;
5 | import javax.validation.constraints.Size;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 |
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | @Getter
13 | public class LinkArticleRequest {
14 |
15 | @NotBlank(message = "공유할 링크 URL을 입력해 주세요.")
16 | @Size(max = 500, message = "링크 URL은 500자를 초과할 수 없습니다.")
17 | private String linkUrl;
18 |
19 | @Size(max = 40, message = "설명은 40자를 초과할 수 없습니다.")
20 | private String description;
21 |
22 | public LinkContent createContent() {
23 | return new LinkContent(linkUrl, description);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/context/userInfo/UserInfoProvider.tsx:
--------------------------------------------------------------------------------
1 | import { type ReactNode, createContext, useState } from 'react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import type { Member } from '@custom-types';
6 |
7 | type UserInfoProviderProps = {
8 | children: ReactNode;
9 | };
10 |
11 | type ContextType = {
12 | userInfo: Member;
13 | setUserInfo: React.Dispatch>;
14 | };
15 |
16 | const initialValue = { id: -1, username: '', imageUrl: '', profileUrl: '' };
17 |
18 | export const UserInfoContext = createContext({
19 | userInfo: initialValue,
20 | setUserInfo: noop,
21 | });
22 |
23 | export const UserInfoProvider = ({ children }: UserInfoProviderProps) => {
24 | const [userInfo, setUserInfo] = useState(initialValue);
25 | return {children};
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/multi-tag-select/MultiTagSelect.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import MultiTagSelect, { type MultiTagSelectProps } from '@shared/multi-tag-select/MultiTagSelect';
4 |
5 | export default {
6 | title: 'Components/MultiTagSelect',
7 | component: MultiTagSelect,
8 | argTypes: {},
9 | };
10 |
11 | const Template: Story = props => ;
12 |
13 | export const Default = Template.bind({});
14 | Default.args = {
15 | options: [
16 | {
17 | value: 1,
18 | label: 'apple',
19 | },
20 | {
21 | value: 2,
22 | label: 'orange',
23 | },
24 | {
25 | value: 3,
26 | label: 'banana',
27 | },
28 | {
29 | value: 4,
30 | label: 'tomato',
31 | },
32 | {
33 | value: 5,
34 | label: 'bread',
35 | },
36 | ],
37 | };
38 |
--------------------------------------------------------------------------------
/frontend/src/context/login/LoginProvider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from 'react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import AccessTokenController from '@auth/accessTokenController';
6 |
7 | type LoginProviderProps = {
8 | children: React.ReactNode;
9 | };
10 |
11 | type ContextType = {
12 | isLoggedIn: boolean;
13 | setIsLoggedIn: React.Dispatch>;
14 | };
15 |
16 | export const LoginContext = createContext({
17 | isLoggedIn: false,
18 | setIsLoggedIn: noop,
19 | });
20 |
21 | export const LoginProvider = ({ children }: LoginProviderProps) => {
22 | const { hasAccessToken, hasTokenDateTime } = AccessTokenController;
23 | const [isLoggedIn, setIsLoggedIn] = useState(hasAccessToken && hasTokenDateTime);
24 |
25 | return {children};
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-item/LinkItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import LinkItem, { type LinkItemProps } from '@link-tab/components/link-item/LinkItem';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/LinkItem',
7 | component: LinkItem,
8 | };
9 |
10 | const Template: Story = props => ;
11 |
12 | export const Default = Template.bind({});
13 | Default.args = {
14 | linkUrl: 'https://www.naver.com/',
15 | author: {
16 | id: 1,
17 | username: 'your-name',
18 | profileUrl: '/',
19 | imageUrl:
20 | 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60',
21 | },
22 | description: '안녕하세요 글자는 약 40자 정도로 하면 좋을 것 같네요. 가나다라마바사아',
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/detail-page/components/more-button/MoreButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 | import { useState } from 'react';
3 |
4 | import MoreButton, { type MoreButtonProps } from '@detail-page/components/more-button/MoreButton';
5 |
6 | export default {
7 | title: 'Pages/DetailPage/MoreButton',
8 | component: MoreButton,
9 | argTypes: {
10 | status: 'fold',
11 | foldText: '- 접기',
12 | unfoldText: '+ 더보기',
13 | },
14 | };
15 |
16 | const Template: Story = props => {
17 | const [show, setShow] = useState(false);
18 | return (
19 |
20 | setShow(prev => !prev)} />
21 |
22 | );
23 | };
24 | export const Default = Template.bind({});
25 | Default.args = {
26 | foldText: '+ 더보기',
27 | unfoldText: '- 접기',
28 | };
29 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/WriterResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import lombok.AccessLevel;
5 | import lombok.AllArgsConstructor;
6 | import lombok.EqualsAndHashCode;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 | import lombok.ToString;
10 |
11 | @Getter
12 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
13 | @AllArgsConstructor
14 | @EqualsAndHashCode
15 | @ToString
16 | public class WriterResponse {
17 |
18 | private Long id;
19 |
20 | private String username;
21 |
22 | private String imageUrl;
23 |
24 | private String profileUrl;
25 |
26 | public WriterResponse(MemberData memberData) {
27 | this(memberData.getId(), memberData.getUsername(), memberData.getImageUrl(), memberData.getProfileUrl());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/api/links/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { arrayOfAll, checkType, hasOwnProperties, isArray, isBoolean, isObject } from '@utils';
4 |
5 | import { checkLink } from '@api/link/typeChecker';
6 | import { type ApiLinks } from '@api/links';
7 |
8 | type LinksKeys = keyof ApiLinks['get']['responseData'];
9 |
10 | const arrayOfAllLinksKeys = arrayOfAll();
11 |
12 | export const checkLinks = (data: unknown): ApiLinks['get']['responseData'] => {
13 | if (!isObject(data)) throw new AxiosError(`Links does not have correct type: object`);
14 |
15 | const keys = arrayOfAllLinksKeys(['links', 'hasNext']);
16 | if (!hasOwnProperties(data, keys)) throw new AxiosError('Links does not have some properties');
17 |
18 | return {
19 | links: checkType(data.links, isArray).map(link => checkLink(link)),
20 | hasNext: checkType(data.hasNext, isBoolean),
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button/index.tsx:
--------------------------------------------------------------------------------
1 | export * from '@shared/button/box-button/BoxButton';
2 | export * from '@shared/button/icon-button/IconButton';
3 | export * from '@shared/button/linked-button/LinkedButton';
4 | export * from '@shared/button/text-button/TextButton';
5 | export * from '@shared/button/toggle-button/ToggleButton';
6 | export * from '@shared/button/unstyled-button/UnstyledButton';
7 |
8 | export { default as BoxButton } from '@shared/button/box-button/BoxButton';
9 | export { default as IconButton } from '@shared/button/icon-button/IconButton';
10 | export { default as LinkedButton } from '@shared/button/linked-button/LinkedButton';
11 | export { default as TextButton } from '@shared/button/text-button/TextButton';
12 | export { default as ToggleButton } from '@shared/button/toggle-button/ToggleButton';
13 | export { default as UnstyledButton } from '@shared/button/unstyled-button/UnstyledButton';
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/response/AuthorResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.response;
2 |
3 | import static lombok.AccessLevel.PRIVATE;
4 |
5 | import com.woowacourse.moamoa.member.query.data.MemberData;
6 | import lombok.AllArgsConstructor;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 | import lombok.ToString;
11 |
12 | @AllArgsConstructor(access = PRIVATE)
13 | @Getter
14 | @EqualsAndHashCode
15 | @NoArgsConstructor(access = PRIVATE)
16 | @ToString
17 | public class AuthorResponse {
18 |
19 | private Long id;
20 | private String username;
21 | private String imageUrl;
22 | private String profileUrl;
23 |
24 | public AuthorResponse(MemberData memberData) {
25 | this(memberData.getId(), memberData.getUsername(), memberData.getImageUrl(), memberData.getProfileUrl());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/request/ArticleRequest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.request;
2 |
3 | import com.woowacourse.moamoa.studyroom.domain.article.Content;
4 | import javax.validation.constraints.NotBlank;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import org.hibernate.validator.constraints.Length;
8 |
9 | @Getter
10 | @NoArgsConstructor
11 | public class ArticleRequest {
12 |
13 | @NotBlank(message = "내용을 입력해 주세요.")
14 | @Length(max = 30)
15 | private String title;
16 |
17 | @NotBlank(message = "내용을 입력해 주세요.")
18 | @Length(max = 5000)
19 | private String content;
20 |
21 | public ArticleRequest(final String title, final String content) {
22 | this.title = title;
23 | this.content = content;
24 | }
25 |
26 | public Content createContent() {
27 | return new Content(title, content);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/fixture/TagFixtures.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance.fixture;
2 |
3 | public class TagFixtures {
4 |
5 | public static final Long 자바_태그_ID = 1L;
6 | public static final String 자바_태그명 = "Java";
7 | public static final String 자바_태그_설명 = "자바";
8 |
9 | public static final Long 우테코4기_태그_ID = 2L;
10 | public static final String 우테코4기_태그명 = "4기";
11 | public static final String 우테코4기_태그_설명 = "우테코4기";
12 |
13 | public static final Long BE_태그_ID = 3L;
14 | public static final String BE_태그명 = "BE";
15 | public static final String BE_태그_설명 = "백엔드";
16 |
17 | public static final Long FE_태그_ID = 4L;
18 | public static final String FE_태그명 = "FE";
19 | public static final String FE_태그_설명 = "프론트엔드";
20 |
21 | public static final Long 리액트_태그_ID = 5L;
22 | public static final String 리액트_태그명 = "React";
23 | public static final String 리액트_태그_설명 = "리액트";
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/markdown-render/MarkdownRender.tsx:
--------------------------------------------------------------------------------
1 | import DOMPurify from 'dompurify';
2 | import { marked } from 'marked';
3 | import { useEffect, useRef } from 'react';
4 |
5 | import { css } from '@emotion/react';
6 |
7 | import markdown from '@styles/markdown';
8 |
9 | type MarkdownRenderProps = {
10 | markdownContent: string;
11 | };
12 |
13 | const MarkdownRender = ({ markdownContent }: MarkdownRenderProps) => {
14 | const contentRef = useRef(null);
15 |
16 | useEffect(() => {
17 | if (!contentRef.current) return;
18 | const cleanHtml = DOMPurify.sanitize(marked.parse(markdownContent));
19 | contentRef.current.innerHTML = cleanHtml;
20 | }, [contentRef, markdownContent]);
21 |
22 | return (
23 |
30 | );
31 | };
32 |
33 | export default MarkdownRender;
34 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useUserInfo.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from 'react';
2 |
3 | import { useGetUserInformation } from '@api/member';
4 |
5 | import { useAuth } from '@hooks/useAuth';
6 |
7 | import { UserInfoContext } from '@context/userInfo/UserInfoProvider';
8 |
9 | export const useUserInfo = () => {
10 | const { userInfo, setUserInfo } = useContext(UserInfoContext);
11 |
12 | const { isLoggedIn } = useAuth();
13 |
14 | const {
15 | data,
16 | refetch: fetchUserInfo,
17 | isFetching,
18 | isError,
19 | isSuccess,
20 | } = useGetUserInformation({
21 | options: {
22 | enabled: isLoggedIn,
23 | },
24 | });
25 |
26 | // TODO: 이 훅을 사용하는 페이지에서 새로고침했을 때 setUserInfo가 반영되지 않아서 이전 userRole 값을 사용하게 됨
27 | useEffect(() => {
28 | if (isFetching || isError || !isSuccess) return;
29 | setUserInfo(data);
30 | }, [isFetching]);
31 |
32 | return {
33 | fetchUserInfo,
34 | userInfo,
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleSummariesResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import java.util.List;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.Getter;
6 | import lombok.NoArgsConstructor;
7 | import lombok.ToString;
8 |
9 | @NoArgsConstructor
10 | @Getter
11 | @EqualsAndHashCode
12 | @ToString
13 | public class ArticleSummariesResponse {
14 |
15 | private List articles;
16 | private long currentPage;
17 | private long lastPage;
18 | private long totalCount;
19 |
20 | public ArticleSummariesResponse(
21 | final List articles, final long currentPage, final long lastPage,
22 | final long totalCount) {
23 | this.articles = articles;
24 | this.currentPage = currentPage;
25 | this.lastPage = lastPage;
26 | this.totalCount = totalCount;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/button-group/ButtonGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { BoxButton } from '@shared/button';
4 | import ButtonGroup, { type ButtonGroupProps } from '@shared/button-group/ButtonGroup';
5 |
6 | export default {
7 | title: 'Components/ButtonGroup',
8 | component: ButtonGroup,
9 | };
10 |
11 | const Template: Story = props => (
12 |
13 | hi
14 |
15 | hi
16 |
17 |
18 | hi
19 |
20 |
21 | );
22 |
23 | export const Default = Template.bind({});
24 | Default.args = {
25 | orientation: 'vertical',
26 | gap: '8px',
27 | custom: {
28 | width: '200px',
29 | height: '100px',
30 | },
31 | };
32 | Default.parameters = { controls: { exclude: ['children'] } };
33 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/filter-button/FilterButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 | import { useState } from 'react';
3 |
4 | import FilterButton, { type FilterButtonProps } from '@main-page/components/filter-button/FilterButton';
5 |
6 | export default {
7 | title: 'Pages/MainPage/FilterButton',
8 | component: FilterButton,
9 | argTypes: {
10 | name: { controls: 'text' },
11 | description: { controls: 'text' },
12 | isChecked: { controls: 'boolean' },
13 | },
14 | };
15 |
16 | const Template: Story = props => {
17 | const [isChecked, setIsChecked] = useState(false);
18 | return setIsChecked(prev => !prev)} />;
19 | };
20 |
21 | export const Default = Template.bind({});
22 | Default.args = {
23 | name: 'JS',
24 | description: '자바스크립트',
25 | };
26 | Default.parameters = { controls: { exclude: ['onClick', 'isChecked'] } };
27 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-form/LinkForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import LinkForm, { type LinkFormProps } from '@link-tab/components/link-form/LinkForm';
6 |
7 | export default {
8 | title: 'Pages/StudyRoomPage/LinkForm',
9 | component: LinkForm,
10 | };
11 |
12 | const Template: Story = props => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {
16 | author: {
17 | id: 20,
18 | username: 'tco0427',
19 | imageUrl:
20 | 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
21 | profileUrl: 'github.com',
22 | },
23 | };
24 | Default.parameters = { controls: { exclude: ['onPostSuccess', 'onPostError'] } };
25 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useUserRole.ts:
--------------------------------------------------------------------------------
1 | import { USER_ROLE } from '@constants';
2 |
3 | import { StudyId } from '@custom-types';
4 |
5 | import { useGetUserRole } from '@api/member';
6 |
7 | import { useAuth } from '@hooks/useAuth';
8 |
9 | export const useUserRole = ({ studyId }: { studyId: StudyId }) => {
10 | const { isLoggedIn } = useAuth();
11 |
12 | const {
13 | data,
14 | refetch: fetchUserRole,
15 | isFetching,
16 | isError,
17 | isSuccess,
18 | } = useGetUserRole({
19 | studyId,
20 | options: {
21 | enabled: isLoggedIn,
22 | },
23 | });
24 |
25 | const userRole = data?.role;
26 |
27 | return {
28 | fetchUserRole,
29 | userRole,
30 | isFetching,
31 | isError,
32 | isSuccess,
33 | isOwner: userRole === USER_ROLE.OWNER,
34 | isMember: userRole === USER_ROLE.MEMBER,
35 | isNonMember: userRole === USER_ROLE.NON_MEMBER,
36 | isOwnerOrMember: userRole !== USER_ROLE.NON_MEMBER,
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/components/tab-button/TabButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 | import { useState } from 'react';
3 |
4 | import TabButton, { type TabButtonProps } from '@study-room-page/components/tab-button/TabButton';
5 |
6 | export default {
7 | title: 'Pages/StudyRoomPage/TabButton',
8 | component: TabButton,
9 | };
10 |
11 | const Template: Story = props => {
12 | const [isSelected, setIsSelected] = useState(false);
13 |
14 | const handleTabButtonClick = () => {
15 | setIsSelected(prev => !prev);
16 | };
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export const Default = Template.bind({});
26 | Default.args = {
27 | children: '공지사항',
28 | isSelected: false,
29 | };
30 | Default.parameters = { controls: { exclude: ['onClick'] } };
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/auth/service/oauthclient/response/GithubProfileResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.auth.service.oauthclient.response;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import com.woowacourse.moamoa.member.domain.Member;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.NoArgsConstructor;
8 |
9 | @Getter
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class GithubProfileResponse {
13 |
14 | @JsonProperty("id")
15 | private Long githubId;
16 |
17 | @JsonProperty("login")
18 | private String username;
19 |
20 | @JsonProperty("email")
21 | private String email;
22 |
23 | @JsonProperty("avatar_url")
24 | private String imageUrl;
25 |
26 | @JsonProperty("html_url")
27 | private String profileUrl;
28 |
29 | public Member toMember() {
30 | return new Member(githubId, username, email, imageUrl, profileUrl);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/api/reviews/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { arrayOfAll, checkType, hasOwnProperties, isArray, isNumber, isObject } from '@utils';
4 |
5 | import { checkStudyReview } from '@api/review/typeChecker';
6 | import { type ApiReviews } from '@api/reviews';
7 |
8 | type StudyReviewsKeys = keyof ApiReviews['get']['responseData'];
9 |
10 | const arrayOfAllStudyReviewsKeys = arrayOfAll();
11 | export const checkStudyReviews = (data: unknown): ApiReviews['get']['responseData'] => {
12 | if (!isObject(data)) throw new AxiosError(`StudyReviews does not have correct type: object`);
13 |
14 | const keys = arrayOfAllStudyReviewsKeys(['reviews', 'totalCount']);
15 | if (!hasOwnProperties(data, keys)) throw new AxiosError('StudyReviews does not have some properties');
16 |
17 | return {
18 | reviews: checkType(data.reviews, isArray).map(review => checkStudyReview(review)),
19 | totalCount: checkType(data.totalCount, isNumber),
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/study-card/StudyCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import StudyCard, { type StudyCardProps } from '@main-page/components/study-card/StudyCard';
4 |
5 | export default {
6 | title: 'Pages/MainPage/StudyCard',
7 | component: StudyCard,
8 | };
9 |
10 | const Template: Story = props => (
11 |
12 |
13 |
14 | );
15 |
16 | export const Default = Template.bind({});
17 | Default.args = {
18 | thumbnailUrl:
19 | 'https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1673&q=80',
20 | thumbnailAlt: '스터디 이미지 Alt',
21 | title: '자바스크립트 스터디',
22 | excerpt: '자바스크립트 스터디입니다',
23 | tags: [
24 | {
25 | id: 1,
26 | name: 'FE',
27 | },
28 | { id: 2, name: '4기' },
29 | { id: 3, name: 'JS' },
30 | ],
31 | isOpen: true,
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/webpack/webpack.local.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const webpack = require('webpack');
3 | const { join } = require('path');
4 | const { merge } = require('webpack-merge');
5 |
6 | require('dotenv').config({ path: join(__dirname, '../env/.env.local') });
7 |
8 | const common = require('./webpack.common');
9 |
10 | module.exports = merge(common, {
11 | mode: 'development',
12 | devtool: 'eval-source-map',
13 | devServer: {
14 | open: true,
15 | port: 3000,
16 | compress: true,
17 | client: {
18 | overlay: {
19 | errors: true,
20 | warnings: true,
21 | },
22 | },
23 | historyApiFallback: true,
24 | },
25 | plugins: [
26 | new webpack.DefinePlugin({
27 | 'process.env.API_URL': JSON.stringify(process.env.API_URL),
28 | 'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID),
29 | 'process.env.LINK_PREVIEW_API_URL': JSON.stringify(process.env.LINK_PREVIEW_API_URL),
30 | }),
31 | ],
32 | });
33 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/TempArticleData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.query.data;
2 |
3 | import java.time.LocalDate;
4 | import lombok.Getter;
5 |
6 | @Getter
7 | public class TempArticleData {
8 |
9 | private final Long id;
10 | private final StudyData studyData;
11 | private final String title;
12 | private final String content;
13 | private final LocalDate createdDate;
14 | private final LocalDate lastModifiedDate;
15 |
16 | public TempArticleData(final Long id, final StudyData studyData,
17 | final String title, final String content,
18 | final LocalDate createdDate,
19 | final LocalDate lastModifiedDate) {
20 | this.id = id;
21 | this.studyData = studyData;
22 | this.title = title;
23 | this.content = content;
24 | this.createdDate = createdDate;
25 | this.lastModifiedDate = lastModifiedDate;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/pages/main-page/components/filter-slide-button/FilterSlideButton.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@shared/button';
2 | import { LeftDirectionIcon, RightDirectionIcon } from '@shared/icons';
3 |
4 | export type FilterSlideButtonProps = {
5 | direction: 'right' | 'left';
6 | ariaLabel: string;
7 | onClick: React.MouseEventHandler;
8 | };
9 |
10 | const FilterSlideButton: React.FC = ({ direction, ariaLabel, onClick: handleClick }) => {
11 | return (
12 |
24 | {direction === 'right' ? : }
25 |
26 | );
27 | };
28 |
29 | export default FilterSlideButton;
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/member/controller/MemberController.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.member.controller;
2 |
3 | import com.woowacourse.moamoa.auth.config.AuthenticatedMemberId;
4 | import com.woowacourse.moamoa.member.service.MemberService;
5 | import com.woowacourse.moamoa.member.service.response.MemberResponse;
6 | import lombok.RequiredArgsConstructor;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RestController;
10 |
11 | @RestController
12 | @RequiredArgsConstructor
13 | public class MemberController {
14 |
15 | private final MemberService memberService;
16 |
17 | @GetMapping("/api/members/me")
18 | public ResponseEntity getCurrentMember(
19 | @AuthenticatedMemberId Long memberId
20 | ) {
21 | MemberResponse response = memberService.getByMemberId(memberId);
22 | return ResponseEntity.ok().body(response);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/schedule/AutoCloseEnrollmentTask.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.schedule;
2 |
3 | import com.woowacourse.moamoa.study.service.StudyService;
4 | import java.time.LocalDateTime;
5 | import java.time.ZoneId;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.scheduling.config.TriggerTask;
8 | import org.springframework.scheduling.support.CronTrigger;
9 | import org.springframework.stereotype.Component;
10 |
11 | @Component
12 | @Slf4j
13 | public class AutoCloseEnrollmentTask extends TriggerTask {
14 |
15 | public AutoCloseEnrollmentTask(final StudyService studyService) {
16 | super(runnable(studyService), new CronTrigger("@daily", ZoneId.of("Asia/Seoul")));
17 | }
18 |
19 | private static Runnable runnable(final StudyService studyService) {
20 | return () -> {
21 | log.debug("{} : start moamoa scheduled task!", LocalDateTime.now());
22 | studyService.autoUpdateStatus();
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/tag/service/SearchingTagService.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.tag.service;
2 |
3 | import com.woowacourse.moamoa.tag.query.TagDao;
4 | import com.woowacourse.moamoa.tag.query.request.CategoryIdRequest;
5 | import com.woowacourse.moamoa.tag.query.response.TagData;
6 | import com.woowacourse.moamoa.tag.service.response.TagsResponse;
7 | import java.util.List;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | @Service
12 | @Transactional(readOnly = true)
13 | public class SearchingTagService {
14 |
15 | private final TagDao tagDao;
16 |
17 | public SearchingTagService(final TagDao tagDao) {
18 | this.tagDao = tagDao;
19 | }
20 |
21 | public TagsResponse getBy(String shortName, CategoryIdRequest categoryId) {
22 | final List tagsResponse = tagDao.searchByShortNameAndCategoryId(shortName.trim(), categoryId);
23 | return new TagsResponse(tagsResponse);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/mocks/handlers/memberHandlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw';
2 |
3 | export const user = {
4 | id: 20,
5 | username: 'tco0427',
6 | imageUrl:
7 | 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
8 | profileUrl: 'github.com',
9 | participationDate: '2022-08-12',
10 | numberOfStudy: 5,
11 | };
12 |
13 | export const memberHandlers = [
14 | rest.get('/api/members/me', (req, res, ctx) => {
15 | return res(ctx.status(200), ctx.json(user));
16 | }),
17 | rest.get('/api/members/me/role', (req, res, ctx) => {
18 | // const studyId = req.url.searchParams.get('study-id');
19 |
20 | // const roles = ['OWNER', 'MEMBER', 'NON_MEMBER'];
21 | // const selectedRole = roles[Math.floor(Math.random() * roles.length)];
22 | return res(
23 | ctx.status(200),
24 | ctx.json({
25 | role: 'OWNER',
26 | // role: selectedRole,
27 | }),
28 | );
29 | }),
30 | ];
31 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/user-description/UserDescription.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import UserDescription, { type UserDescriptionProps } from '@link-tab/components/user-description/UserDescription';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/UserDescription',
7 | component: UserDescription,
8 | };
9 |
10 | const Template: Story = props => (
11 |
16 |
17 |
18 | );
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {
22 | author: {
23 | id: 1,
24 | username: 'your-name',
25 | profileUrl: '/',
26 | imageUrl:
27 | 'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=800&q=60',
28 | },
29 | description: '안녕하세요 글자는 약 40자 정도로 하면 좋을 것 같네요. 가나다라마바사아',
30 | };
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/response/CommentResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.response;
2 |
3 | import com.woowacourse.moamoa.comment.query.data.CommentData;
4 | import java.time.LocalDate;
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 | @Getter
13 | @AllArgsConstructor
14 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
15 | @EqualsAndHashCode
16 | @ToString
17 | public class CommentResponse {
18 |
19 | private Long id;
20 | private AuthorResponse author;
21 | private LocalDate createdDate;
22 | private LocalDate lastModifiedDate;
23 | private String content;
24 |
25 | public CommentResponse(CommentData comment) {
26 | this(comment.getId(), new AuthorResponse(comment.getMember()), comment.getCreatedDate(),
27 | comment.getLastModifiedDate(), comment.getContent());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/query/data/ArticleData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.MemberData;
4 | import java.time.LocalDate;
5 | import lombok.Getter;
6 |
7 | @Getter
8 | public class ArticleData {
9 |
10 | private final Long id;
11 | private final MemberData memberData;
12 | private final String title;
13 | private final String content;
14 | private final LocalDate createdDate;
15 | private final LocalDate lastModifiedDate;
16 |
17 | public ArticleData(final Long id, final MemberData memberData, final String title, final String content,
18 | final LocalDate createdDate,
19 | final LocalDate lastModifiedDate) {
20 | this.id = id;
21 | this.memberData = memberData;
22 | this.title = title;
23 | this.content = content;
24 | this.createdDate = createdDate;
25 | this.lastModifiedDate = lastModifiedDate;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/LinksResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.studyroom.query.data.LinkArticleData;
4 | import java.util.List;
5 | import java.util.stream.Collectors;
6 | import lombok.AccessLevel;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 |
10 | @NoArgsConstructor(access = AccessLevel.PROTECTED)
11 | @Getter
12 | public class LinksResponse {
13 |
14 | private List links;
15 |
16 | private boolean hasNext;
17 |
18 | public LinksResponse(final List linkArticleData, final boolean hasNext) {
19 | this.links = getLinkResponses(linkArticleData);
20 | this.hasNext = hasNext;
21 | }
22 |
23 | private List getLinkResponses(final List linkArticleData) {
24 | return linkArticleData.stream()
25 | .map(LinkResponse::new)
26 | .collect(Collectors.toList());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/components/@shared/icons/crown-icon/CrownIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '@emotion/react';
2 |
3 | import { type CustomCSS, resolveCustomCSS } from '@styles/custom-css';
4 |
5 | type CrownIconProps = {
6 | custom?: CustomCSS<'position' | 'top' | 'left' | 'zIndex'>;
7 | };
8 | const CrownIcon: React.FC = ({ custom }) => {
9 | const theme = useTheme();
10 | return (
11 |
12 |
27 |
28 | );
29 | };
30 |
31 | export default CrownIcon;
32 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-preview/LinkPreview.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import LinkPreview, { type LinkPreviewProps } from '@link-tab/components/link-preview/LinkPreview';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/LinkPreview',
7 | component: LinkPreview,
8 | };
9 |
10 | const Template: Story = props => (
11 |
17 |
18 |
19 | );
20 |
21 | export const Default = Template.bind({});
22 | Default.args = {
23 | previewResult: {
24 | title: '합성 컴포넌트 어쩌구 저쩌구 쏼라쏼라',
25 | description: '카카오 엔터테인먼트 FE 기술 블로그',
26 | imageUrl:
27 | 'https://images.unsplash.com/photo-1572059002053-8cc5ad2f4a38?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTV8fGdvb2dsZXxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60',
28 | domainName: 'naver.com',
29 | },
30 | linkUrl: 'https://naver.com',
31 | };
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/domain/exception/UneditableException.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.domain.exception;
2 |
3 | import com.woowacourse.moamoa.common.exception.BadRequestException;
4 | import com.woowacourse.moamoa.studyroom.domain.Accessor;
5 |
6 | public class UneditableException extends BadRequestException {
7 |
8 | public UneditableException(final String message) {
9 | super(message);
10 | }
11 |
12 | public static UneditableException forArticle(final Long studyId, final Accessor accessor, final String typeName) {
13 | final String message = String.format("스터디[%d]에 접근자[%s]가 %s의 게시글을 수정/삭제할 수 없습니다.", studyId, accessor, typeName);
14 | return new UneditableException(message);
15 | }
16 |
17 | public static UneditableException forTempArticle(final Long articleId, final Accessor accessor) {
18 | final String message = String.format("임시 게시글[%d]에 접근자[%s]가 수정/삭제할 수 없습니다.", articleId, accessor);
19 | return new UneditableException(message);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ArticleResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.studyroom.query.data.ArticleData;
4 | import java.time.LocalDate;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Builder;
7 | import lombok.EqualsAndHashCode;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 | import lombok.ToString;
11 |
12 | @Builder
13 | @EqualsAndHashCode
14 | @AllArgsConstructor
15 | @NoArgsConstructor
16 | @Getter
17 | @ToString
18 | public class ArticleResponse {
19 |
20 | private Long id;
21 | private AuthorResponse author;
22 | private String title;
23 | private String content;
24 | private LocalDate createdDate;
25 | private LocalDate lastModifiedDate;
26 |
27 | public ArticleResponse(ArticleData data) {
28 | this(data.getId(), new AuthorResponse(data.getMemberData()), data.getTitle(), data.getContent(),
29 | data.getCreatedDate(), data.getLastModifiedDate());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ReviewResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.studyroom.query.data.ReviewData;
4 | import java.time.LocalDate;
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 | @Getter
13 | @AllArgsConstructor
14 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
15 | @EqualsAndHashCode
16 | @ToString
17 | public class ReviewResponse {
18 |
19 | private Long id;
20 | private WriterResponse member;
21 | private LocalDate createdDate;
22 | private LocalDate lastModifiedDate;
23 | private String content;
24 |
25 | public ReviewResponse(final ReviewData reviewData) {
26 | this(reviewData.getId(), new WriterResponse(reviewData.getMember()), reviewData.getCreatedDate(),
27 | reviewData.getLastModifiedDate(), reviewData.getContent());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/api/link-preview/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { arrayOfAll, checkType, hasOwnProperties, isObject, isString } from '@utils';
4 |
5 | import { type ApiLinkPreview } from '@api/link-preview';
6 |
7 | type LinkPreviewKeys = keyof ApiLinkPreview['get']['responseData'];
8 |
9 | const arrayOfAllLinkPreviewKeys = arrayOfAll();
10 |
11 | export const checkLinkPreview = (data: unknown): ApiLinkPreview['get']['responseData'] => {
12 | if (!isObject(data)) throw new AxiosError(`LinkPreview does not have correct type: object`);
13 |
14 | const keys = arrayOfAllLinkPreviewKeys(['title', 'description', 'imageUrl', 'domainName']);
15 | if (!hasOwnProperties(data, keys)) throw new AxiosError('LinkPreview does not have some properties');
16 |
17 | return {
18 | title: checkType(data.title, isString, true),
19 | description: checkType(data.description, isString, true),
20 | imageUrl: checkType(data.imageUrl, isString, true),
21 | domainName: checkType(data.domainName, isString, true),
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/frontend/src/pages/error-page/ErrorPage.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import sthWentWrongImage from '@assets/images/sth-went-wrong.png';
4 |
5 | import { PATH } from '@constants';
6 |
7 | import { BoxButton } from '@shared/button';
8 | import Flex from '@shared/flex/Flex';
9 | import Image from '@shared/image/Image';
10 |
11 | const ErrorPage: React.FC = () => {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default ErrorPage;
22 |
23 | const ErrorImage = () => (
24 |
25 | );
26 |
27 | const ErrorMessage = () => 잘못된 접근입니다.
;
28 |
29 | const HomeLink: React.FC = () => (
30 |
31 |
32 | 홈으로 이동
33 |
34 |
35 | );
36 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/review-tab-panel/components/review-comment/ReviewComment.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import ReviewComment, { type ReviewCommentProps } from '@review-tab/components/review-comment/ReviewComment';
4 |
5 | export default {
6 | title: 'Pages/StudyRoomPage/ReviewComment',
7 | component: ReviewComment,
8 | argTypes: {},
9 | };
10 |
11 | const Template: Story = props => {
12 | return ;
13 | };
14 |
15 | export const Default = Template.bind({});
16 | Default.args = {
17 | id: 123,
18 | studyId: 1,
19 | author: {
20 | id: 1,
21 | username: 'nan-noo',
22 | imageUrl: 'https://avatars.githubusercontent.com/u/54002105?v=4',
23 | profileUrl: 'https://github.com/nan-noo',
24 | },
25 | date: '2022-07-31',
26 | content:
27 | '후기후기 라라라랄라ㅏ abcedfsf rksk가나다라 마바사ㅏ 아자ㅏ 아런아렁날 ㅓㅇㄴㄹㄴ아렁나러ㅏㄴ어라먼아러낭ㄹㅇㄴㄹㄴㅇ 낭런아럼;닌아럼;니아런;이ㅏ런일아아아앙 나나나나나나 ㅏㅇㄹㅇㄹ알ㅇㄹㅇㄹㅇ ㄴㄹ날가나다라가나다라 가낟 ㄴ안ㅇ란ㅇㄹ ㅇ ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹ ㅇㄴㄹㅁㅇㄴㄴㅇㄹㅇㄴㄹ ㄹㅇㄴㄹㅁㄴㅇㄹㄴㅇㄹㄴㅇㄹ ㄹㅇㄴㅁㄴㅇㄹㅁㄴㅇㄹㄴㅇㄹㄴ ㅇㄹㅇ',
28 | };
29 |
--------------------------------------------------------------------------------
/.github/workflows/frontend.yml:
--------------------------------------------------------------------------------
1 | name: frontend
2 |
3 | on:
4 | pull_request:
5 | branches: ["main", "develop"]
6 | paths:
7 | - 'frontend/**'
8 |
9 | defaults:
10 | run:
11 | working-directory: frontend
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [16.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 |
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: "npm"
29 | cache-dependency-path: "./frontend/package-lock.json"
30 |
31 | - name: Install
32 | run: npm install
33 |
34 | - name: Build
35 | run: npm run build
36 |
37 | - name: Test
38 | uses: cypress-io/github-action@v4.2.0
39 | with:
40 | start: npm run start
41 | wait-on: "http://localhost:3000"
42 | working-directory: frontend
43 | config-file: cypress.config.ts
44 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/moamoa/study/webmvc/UnauthorizedMyStudyWebMvcTest.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.webmvc;
2 |
3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6 |
7 | import org.junit.jupiter.api.DisplayName;
8 | import org.junit.jupiter.params.ParameterizedTest;
9 | import org.junit.jupiter.params.provider.CsvSource;
10 |
11 | import com.woowacourse.moamoa.common.WebMVCTest;
12 |
13 | class UnauthorizedMyStudyWebMvcTest extends WebMVCTest {
14 |
15 | @DisplayName("헤더에 Authorization 코드가 없이, 내 스터디를 조회할 경우 401 에러가 발생한다.")
16 | @ParameterizedTest
17 | @CsvSource({"/api/my/studies", "/api/members/me/role"})
18 | void getMyStudiesWithoutAuthorization(String path) throws Exception {
19 | mockMvc.perform(get(path))
20 | .andExpect(status().isUnauthorized())
21 | .andDo(print());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/service/response/StudyResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.service.response;
2 |
3 | import com.woowacourse.moamoa.study.query.data.StudySummaryData;
4 | import com.woowacourse.moamoa.tag.query.response.TagSummaryData;
5 | import java.util.List;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 |
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | @Getter
13 | public class StudyResponse {
14 |
15 | private Long id;
16 | private String title;
17 | private String excerpt;
18 | private String thumbnail;
19 | private String recruitmentStatus;
20 | private List tags;
21 |
22 | public StudyResponse(
23 | final StudySummaryData studySummaryData,
24 | final List studyTags
25 | ) {
26 | this(studySummaryData.getId(), studySummaryData.getTitle(), studySummaryData.getExcerpt(),
27 | studySummaryData.getThumbnail(), studySummaryData.getRecruitmentStatus(), studyTags);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/study/query/data/StudyDetailsData.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.study.query.data;
2 |
3 | import com.woowacourse.moamoa.member.query.data.OwnerData;
4 | import java.time.LocalDate;
5 | import lombok.AccessLevel;
6 | import lombok.AllArgsConstructor;
7 | import lombok.Getter;
8 |
9 | @AllArgsConstructor(access = AccessLevel.PACKAGE)
10 | @Getter
11 | public class StudyDetailsData {
12 |
13 | private final Long id;
14 | private final String title;
15 | private final String excerpt;
16 | private final String thumbnail;
17 | private final String recruitmentStatus;
18 | private final String description;
19 | private final LocalDate createdDate;
20 | private final OwnerData owner;
21 | private final Integer currentMemberCount;
22 | private final Integer maxMemberCount;
23 | private final LocalDate enrollmentEndDate;
24 | private final LocalDate startDate;
25 | private final LocalDate endDate;
26 |
27 | public static StudyDetailsDataBuilder builder() {
28 | return new StudyDetailsDataBuilder();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/comment/service/response/CommentsResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.comment.service.response;
2 |
3 | import static java.util.stream.Collectors.toList;
4 |
5 | import com.woowacourse.moamoa.comment.query.data.CommentData;
6 | import java.util.List;
7 | import lombok.AllArgsConstructor;
8 | import lombok.EqualsAndHashCode;
9 | import lombok.Getter;
10 | import lombok.NoArgsConstructor;
11 | import lombok.ToString;
12 |
13 | @AllArgsConstructor
14 | @NoArgsConstructor
15 | @EqualsAndHashCode
16 | @Getter
17 | @ToString
18 | public class CommentsResponse {
19 |
20 | private List comments;
21 | private long totalCount;
22 | private boolean hasNext;
23 |
24 | public static CommentsResponse from(final List comments, final boolean hasNext, final long totalCount) {
25 | final List commentResponses = comments.stream()
26 | .map(CommentResponse::new)
27 | .collect(toList());
28 |
29 | return new CommentsResponse(commentResponses, totalCount, hasNext);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/studyroom/service/response/ReviewsResponse.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.studyroom.service.response;
2 |
3 | import com.woowacourse.moamoa.studyroom.query.data.ReviewData;
4 | import java.util.List;
5 | import java.util.stream.Collectors;
6 | import lombok.AccessLevel;
7 | import lombok.Getter;
8 | import lombok.NoArgsConstructor;
9 |
10 | @Getter
11 | @NoArgsConstructor(access = AccessLevel.PRIVATE)
12 | public class ReviewsResponse {
13 |
14 | private List reviews;
15 | private Integer totalCount;
16 |
17 | public ReviewsResponse(final List reviews, final Integer totalCount) {
18 | this.reviews = reviews.stream()
19 | .map(ReviewResponse::new)
20 | .collect(Collectors.toList());
21 | this.totalCount = totalCount;
22 | }
23 |
24 | public ReviewsResponse(final List reviews) {
25 | this.reviews = reviews.stream()
26 | .map(ReviewResponse::new)
27 | .collect(Collectors.toList());
28 | totalCount = reviews.size();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/api/review/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { arrayOfAll, checkType, hasOwnProperties, isDateYMD, isNumber, isObject, isString } from '@utils';
4 |
5 | import type { StudyReview } from '@custom-types';
6 |
7 | import { checkMember } from '@api/member/typeChecker';
8 |
9 | type StudyReviewKeys = keyof StudyReview;
10 |
11 | const arrayOfAllStudyReviewKeys = arrayOfAll();
12 |
13 | export const checkStudyReview = (data: unknown): StudyReview => {
14 | if (!isObject(data)) throw new AxiosError(`StudyReview does not have correct type: object`);
15 |
16 | const keys = arrayOfAllStudyReviewKeys(['id', 'member', 'createdDate', 'lastModifiedDate', 'content']);
17 | if (!hasOwnProperties(data, keys)) throw new AxiosError('StudyReview does not have some properties');
18 |
19 | return {
20 | id: checkType(data.id, isNumber),
21 | member: checkMember(data.member),
22 | content: checkType(data.content, isString),
23 | createdDate: checkType(data.createdDate, isDateYMD),
24 | lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD),
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/frontend/src/pages/study-room-page/tabs/link-room-tab-panel/components/link-edit-form/LinkEditForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { type Story } from '@storybook/react';
2 |
3 | import { noop } from '@utils';
4 |
5 | import LinkEditForm, { type LinkEditFormProps } from '@link-tab/components/link-edit-form/LinkEditForm';
6 |
7 | export default {
8 | title: 'Pages/StudyRoomPage/LinkEditForm',
9 | component: LinkEditForm,
10 | };
11 |
12 | const Template: Story = props => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {
16 | linkId: 1,
17 | author: {
18 | id: 20,
19 | username: 'tco0427',
20 | imageUrl:
21 | 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
22 | profileUrl: 'github.com',
23 | },
24 | originalContent: {
25 | linkUrl: 'https://naver.com',
26 | description: '네이버 홈',
27 | },
28 | };
29 | Default.parameters = { controls: { exclude: ['onPostSuccess', 'onPostError'] } };
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/com/woowacourse/moamoa/common/entity/ReadOnlyCollectionPersister.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.moamoa.common.entity;
2 |
3 | import org.hibernate.MappingException;
4 | import org.hibernate.cache.CacheException;
5 | import org.hibernate.cache.spi.access.CollectionDataAccess;
6 | import org.hibernate.mapping.Collection;
7 | import org.hibernate.persister.collection.BasicCollectionPersister;
8 | import org.hibernate.persister.spi.PersisterCreationContext;
9 |
10 | public class ReadOnlyCollectionPersister extends BasicCollectionPersister {
11 |
12 | public ReadOnlyCollectionPersister(final Collection collectionBinding,
13 | final CollectionDataAccess cacheAccessStrategy,
14 | final PersisterCreationContext creationContext)
15 | throws MappingException, CacheException {
16 | super(asInverse(collectionBinding), cacheAccessStrategy, creationContext);
17 | }
18 |
19 | private static Collection asInverse(Collection collection) {
20 | collection.setInverse(true);
21 | return collection;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/api/link/typeChecker.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from 'axios';
2 |
3 | import { arrayOfAll, checkType, hasOwnProperties, isDateYMD, isNumber, isObject, isString } from '@utils';
4 |
5 | import type { Link } from '@custom-types';
6 |
7 | import { checkMember } from '@api/member/typeChecker';
8 |
9 | type LinkKeys = keyof Link;
10 |
11 | const arrayOfAllLinkKeys = arrayOfAll();
12 |
13 | export const checkLink = (data: unknown): Link => {
14 | if (!isObject(data)) throw new AxiosError(`Link does not have correct type: object`);
15 |
16 | const keys = arrayOfAllLinkKeys(['id', 'author', 'linkUrl', 'description', 'createdDate', 'lastModifiedDate']);
17 | if (!hasOwnProperties(data, keys)) throw new AxiosError('Link does not have some properties');
18 |
19 | return {
20 | id: checkType(data.id, isNumber),
21 | author: checkMember(data.author),
22 | linkUrl: checkType(data.linkUrl, isString),
23 | description: checkType(data.description, isString),
24 | createdDate: checkType(data.createdDate, isDateYMD),
25 | lastModifiedDate: checkType(data.lastModifiedDate, isDateYMD),
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/frontend/src/pages/my-study-page/hooks/useMyStudyPage.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import { STUDY_STATUS } from '@constants';
4 |
5 | import type { MyStudy, StudyStatus } from '@custom-types';
6 |
7 | import { useGetMyStudies } from '@api/my-studies';
8 |
9 | export type StudyType = 'prepare' | 'inProgress' | 'done';
10 |
11 | const filterStudiesByStatus = (studies: Array, status: StudyStatus) => {
12 | return studies.filter(({ studyStatus }) => studyStatus === status);
13 | };
14 |
15 | export const useMyStudyPage = () => {
16 | const myStudyQueryResult = useGetMyStudies();
17 |
18 | const filteredStudies: Record> = useMemo(() => {
19 | const studies = myStudyQueryResult.data?.studies ?? [];
20 | return {
21 | prepare: filterStudiesByStatus(studies, STUDY_STATUS.PREPARE),
22 | inProgress: filterStudiesByStatus(studies, STUDY_STATUS.IN_PROGRESS),
23 | done: filterStudiesByStatus(studies, STUDY_STATUS.DONE),
24 | };
25 | }, [myStudyQueryResult.data]);
26 |
27 | return {
28 | myStudyQueryResult,
29 | studies: filteredStudies,
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/backend/src/test/java/com/woowacourse/acceptance/document/StudyDocument.java:
--------------------------------------------------------------------------------
1 | package com.woowacourse.acceptance.document;
2 |
3 | import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
4 | import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
5 | import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document;
6 |
7 | import io.restassured.RestAssured;
8 | import io.restassured.specification.RequestSpecification;
9 |
10 | public class StudyDocument extends Document {
11 |
12 | private StudyDocument(final RequestSpecification spec) {
13 | super(spec);
14 | }
15 |
16 | public static StudyDocument 스터디_참가_문서(RequestSpecification spec) {
17 | RequestSpecification documentSpec = RestAssured.given(spec).filter(
18 | document("studies/participant",
19 | requestHeaders(
20 | headerWithName("Authorization").description("JWT Token")
21 | )
22 | )
23 | );
24 |
25 | return new StudyDocument(documentSpec);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------