Skip to content

Commit

Permalink
Merge pull request #315 from woowacourse-teams/develop
Browse files Browse the repository at this point in the history
공지사항 및 동시성 문제 해결
  • Loading branch information
verus-j authored Aug 30, 2022
2 parents 64f8490 + 439263b commit 56e7fd3
Show file tree
Hide file tree
Showing 52 changed files with 32,332 additions and 724 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
import com.woowacourse.moamoa.common.exception.UnauthorizedException;
import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
import com.woowacourse.moamoa.study.service.exception.FailureParticipationException;
import com.woowacourse.moamoa.study.service.exception.InvalidUpdatingException;
import io.jsonwebtoken.JwtException;
import java.io.PrintStream;
import java.io.PrintWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand All @@ -35,6 +34,7 @@ public ResponseEntity<ErrorResponse> handleBadRequest() {
}

@ExceptionHandler({
InvalidUpdatingException.class,
InvalidFormatException.class,
InvalidPeriodException.class,
BadRequestException.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import static javax.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException;
import com.woowacourse.moamoa.common.exception.UnauthorizedException;
import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException;
import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
import com.woowacourse.moamoa.study.service.exception.FailureParticipationException;
import com.woowacourse.moamoa.study.service.exception.InvalidUpdatingException;
import com.woowacourse.moamoa.study.service.exception.OwnerCanNotLeaveException;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -152,6 +153,19 @@ public MemberRole getRole(final Long memberId) {
public void update(Long memberId, Content content, RecruitPlanner recruitPlanner, AttachedTags attachedTags,
StudyPlanner studyPlanner
) {
if (isRecruitingAfterEndStudy(recruitPlanner, studyPlanner) ||
isRecruitedOrStartStudyBeforeCreatedAt(recruitPlanner, studyPlanner, createdAt)) {
throw new InvalidUpdatingException();
}

if (studyPlanner.isInappropriateCondition(createdAt.toLocalDate())) {
throw new InvalidUpdatingException();
}

if ((recruitPlanner.getMax() != null && recruitPlanner.getMax() < participants.getSize())) {
throw new InvalidUpdatingException();
}

checkOwner(memberId);
this.content = content;
this.recruitPlanner = recruitPlanner;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class StudyParticipantService {
private final MemberRepository memberRepository;
private final StudyRepository studyRepository;

public void participateStudy(final Long memberId, final Long studyId) {
public synchronized void participateStudy(final Long memberId, final Long studyId) {
memberRepository.findById(memberId)
.orElseThrow(MemberNotFoundException::new);
final Study study = studyRepository.findById(studyId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ public void updateStudy(Long memberId, Long studyId, StudyRequest request) {
Study study = studyRepository.findById(studyId)
.orElseThrow(StudyNotFoundException::new);

study.update(memberId, request.mapToContent(), request.mapToRecruitPlan(), request.mapToAttachedTags(),
request.mapToStudyPlanner(LocalDate.now()));
final Content content = request.mapToContent();
final RecruitPlanner recruitPlanner = request.mapToRecruitPlan();
final StudyPlanner studyPlanner = request.mapToStudyPlanner(LocalDate.now());

study.update(memberId, content, recruitPlanner, request.mapToAttachedTags(), studyPlanner);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.woowacourse.moamoa.study.service.exception;

import com.woowacourse.moamoa.common.exception.BadRequestException;

public class InvalidUpdatingException extends BadRequestException {

public InvalidUpdatingException() {
super("스터디 수정이 불가능합니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID;
import static com.woowacourse.acceptance.fixture.TagFixtures.우테코4기_태그_ID;
import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID;
import static com.woowacourse.acceptance.steps.LoginSteps.디우가;
import static com.woowacourse.acceptance.steps.LoginSteps.짱구가;
import static org.springframework.http.HttpHeaders.ACCEPT;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
Expand All @@ -22,11 +23,11 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

public class UpdatingStudyAcceptanceTest extends AcceptanceTest {
class UpdatingStudyAcceptanceTest extends AcceptanceTest {

@DisplayName("스터디 내용을 수정할 수 있다.")
@Test
public void updateStudy() {
void updateStudy() {
final LocalDate 지금 = LocalDate.now();
final long studyId = 짱구가().로그인하고().자바_스터디를()
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
Expand Down Expand Up @@ -55,4 +56,70 @@ public void updateStudy() {
.put("/api/studies/{study-id}")
.then().statusCode(HttpStatus.OK.value());
}

@DisplayName("이전 날짜로 스터디 모집 기간을 변경할 수 없다.")
@Test
void updateStudyWithBeforeDay() {
final LocalDate 지금 = LocalDate.now();
final long studyId = 짱구가().로그인하고().자바_스터디를()
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
.생성한다();
final String accessToken = 짱구가().로그인한다();

final StudyRequest request = new StudyRequestBuilder().title("변경된 제목")
.description("변경된 설명")
.excerpt("변경된 한 줄 설명")
.thumbnail("변경된 썸네일")
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusMonths(1))
.enrollmentEndDate(LocalDate.now().minusDays(1))
.tagIds(List.of(자바_태그_ID, 우테코4기_태그_ID))
.build();

RestAssured.given(spec).log().all()
.filter(document("studies/update",
requestHeaders(headerWithName("Authorization").description("Bearer Token"))))
.header(AUTHORIZATION, accessToken)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.pathParam("study-id", studyId)
.body(request)
.when().log().all()
.put("/api/studies/{study-id}")
.then().statusCode(HttpStatus.BAD_REQUEST.value());
}

@DisplayName("스터디 모집 인원을 현재 인원보다 적게 변경할 수 없다.")
@Test
void updateStudyWithLessThanCurrentMember() {
final LocalDate 지금 = LocalDate.now();
final long studyId = 짱구가().로그인하고().자바_스터디를()
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
.생성한다();
디우가().로그인하고().스터디에(studyId).참여한다();
final String accessToken = 짱구가().로그인한다();

final StudyRequest request = new StudyRequestBuilder().title("변경된 제목")
.description("변경된 설명")
.excerpt("변경된 한 줄 설명")
.thumbnail("변경된 썸네일")
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusMonths(1))
.enrollmentEndDate(LocalDate.now().plusDays(5))
.maxMemberCount(1)
.tagIds(List.of(자바_태그_ID, 우테코4기_태그_ID))
.build();

RestAssured.given(spec).log().all()
.filter(document("studies/update",
requestHeaders(headerWithName("Authorization").description("Bearer Token"))))
.header(AUTHORIZATION, accessToken)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.pathParam("study-id", studyId)
.body(request)
.when().log().all()
.put("/api/studies/{study-id}")
.then().statusCode(HttpStatus.BAD_REQUEST.value());
}
}
1 change: 1 addition & 0 deletions frontend/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'@edit-study-page': resolve(__dirname, '../src/pages/edit-study-page'),
'@my-study-page': resolve(__dirname, '../src/pages/my-study-page'),
'@community-tab': resolve(__dirname, '../src/pages/study-room-page/tabs/community-tab-panel'),
'@notice-tab': resolve(__dirname, '../src/pages/study-room-page/tabs/notice-tab-panel'),
'@study-room-page': resolve(__dirname, '../src/pages/study-room-page'),
'@login-redirect-page': resolve(__dirname, '../src/pages/login-redirect-page'),
'@error-page': resolve(__dirname, '../src/pages/error-page'),
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ const App = () => {
path={`${PATH.COMMUNITY_EDIT()}`}
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
/>
<Route
path={`${PATH.NOTICE_ARTICLE()}`}
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
/>
<Route
path={`${PATH.NOTICE_PUBLISH()}`}
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
/>
<Route
path={`${PATH.NOTICE_EDIT()}`}
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
/>
<Route
path={PATH.EDIT_STUDY()}
element={isLoggedIn ? <EditStudyPage /> : <Navigate to={PATH.MAIN} replace={true} />}
Expand Down
132 changes: 132 additions & 0 deletions frontend/src/api/notice/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { AxiosError, AxiosResponse } from 'axios';
import { useMutation, useQuery } from 'react-query';

import type { NoticeArticle } from '@custom-types';

import axiosInstance from '@api/axiosInstance';

export type GetNoticeArticlesResponseData = {
articles: Array<NoticeArticle>;
currentPage: number;
lastPage: number;
totalCount: number;
};

export type GetNoticeArticleResponseData = NoticeArticle;

export type GetNoticeArticlesParams = {
studyId: number;
page?: number;
size?: number;
};

export type GetNoticeArticleParams = {
studyId: number;
articleId: number;
};

export type PostNoticeArticleRequestParams = {
studyId: number;
};
export type PostNoticeArticleRequestBody = {
title: string;
content: string;
};
export type PostNoticeArticleRequestVariables = PostNoticeArticleRequestParams & PostNoticeArticleRequestBody;
export type PostNoticeArticleResponseData = {
studyId: number;
title: string;
content: string;
};

export type PutNoticeArticleRequestParams = {
studyId: number;
articleId: number;
};
export type PutNoticeArticleRequestBody = {
title: string;
content: string;
};
export type PutNoticeArticleRequestVariables = PutNoticeArticleRequestParams & PutNoticeArticleRequestBody;

export type DeleteNoticeArticleRequestParams = {
studyId: number;
articleId: number;
};

const getNoticeArticles = async ({ studyId, page = 1, size = 8 }: GetNoticeArticlesParams) => {
// 서버쪽에서는 page를 0번부터 계산하기 때문에 page - 1을 해줘야 한다
const response = await axiosInstance.get<GetNoticeArticlesResponseData>(
`/api/studies/${studyId}/notice/articles?page=${page - 1}&size=${size}`,
);
const { totalCount, currentPage, lastPage } = response.data;

response.data = {
...response.data,
totalCount: Number(totalCount),
currentPage: Number(currentPage) + 1, // page를 하나 늘려준다 서버에서 0으로 오기 때문이다
lastPage: Number(lastPage),
};

return response.data;
};

const getNoticeArticle = async ({ studyId, articleId }: GetNoticeArticleParams) => {
// 서버쪽에서는 page를 0번부터 계산하기 때문에 page - 1을 해줘야 한다
const response = await axiosInstance.get<GetNoticeArticleResponseData>(
`/api/studies/${studyId}/notice/articles/${articleId}`,
);
return response.data;
};

const postNoticeArticle = async ({ studyId, title, content }: PostNoticeArticleRequestVariables) => {
const response = await axiosInstance.post<null, AxiosResponse<null>, PostNoticeArticleRequestBody>(
`/api/studies/${studyId}/notice/articles`,
{
title,
content,
},
);

return response.data;
};

const putNoticeArticle = async ({ studyId, title, content, articleId }: PutNoticeArticleRequestVariables) => {
const response = await axiosInstance.put<null, AxiosResponse<null>, PutNoticeArticleRequestBody>(
`/api/studies/${studyId}/notice/articles/${articleId}`,
{
title,
content,
},
);

return response.data;
};

const deleteNoticeArticle = async ({ studyId, articleId }: DeleteNoticeArticleRequestParams) => {
const response = await axiosInstance.delete<null, AxiosResponse<null>>(
`/api/studies/${studyId}/notice/articles/${articleId}`,
);

return response.data;
};

export const useGetNoticeArticles = (studyId: number, page: number) => {
return useQuery(['get-notice-articles', studyId, page], () => getNoticeArticles({ studyId, page }));
};

export const useGetNoticeArticle = (studyId: number, articleId: number) => {
return useQuery(['get-notice-article', studyId, articleId], () => getNoticeArticle({ studyId, articleId }));
};

export const usePostNoticeArticle = () => {
return useMutation<null, AxiosError, PostNoticeArticleRequestVariables>(postNoticeArticle);
};

export const usePutNoticeArticle = () => {
return useMutation<null, AxiosError, PutNoticeArticleRequestVariables>(putNoticeArticle);
};

export const useDeleteNoticeArticle = () => {
return useMutation<null, AxiosError, DeleteNoticeArticleRequestParams>(deleteNoticeArticle);
};
10 changes: 9 additions & 1 deletion frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ export const PATH = {
STUDY_ROOM: (studyId: ':studyId' | number = ':studyId') => `/studyroom/${studyId}`,
LOGIN: '/login',
REVIEW: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/reviews`,
COMMUNITY: (studyId: ':studyId' | number = ':studyId') => `/studyroom/${studyId}/community`,

COMMUNITY: (studyId: ':studyId' | string | number = ':studyId') => `/studyroom/${studyId}/community`,
COMMUNITY_ARTICLE: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
`/studyroom/${studyId}/community/article/${articleId}`,
COMMUNITY_PUBLISH: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/community/article/publish`,
COMMUNITY_EDIT: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
`/studyroom/${studyId}/community/article/${articleId}/edit`,

NOTICE: (studyId: ':studyId' | string | number = ':studyId') => `/studyroom/${studyId}/notice`,
NOTICE_ARTICLE: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
`/studyroom/${studyId}/notice/article/${articleId}`,
NOTICE_PUBLISH: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/notice/article/publish`,
NOTICE_EDIT: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
`/studyroom/${studyId}/notice/article/${articleId}/edit`,
};

export const API_ERROR = {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/custom-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type LinkId = number;
export type Page = number;
export type Size = number;

export type SitePage = 'home' | 'studyroom';

export type Owner = {
id: MemberId;
username: string;
Expand Down Expand Up @@ -112,3 +114,14 @@ export type CommunityArticle = {
};

export type CommunityArticleMode = 'publish' | 'edit';

export type NoticeArticle = {
id: number;
author: Member;
title: string;
content: string;
createdDate: DateYMD;
lastModifiedDate: DateYMD;
};

export type NoticeArticleMode = 'publish' | 'edit';
Loading

0 comments on commit 56e7fd3

Please sign in to comment.