Skip to content

Commit 56e7fd3

Browse files
authored
Merge pull request #315 from woowacourse-teams/develop
공지사항 및 동시성 문제 해결
2 parents 64f8490 + 439263b commit 56e7fd3

52 files changed

Lines changed: 32332 additions & 724 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/main/java/com/woowacourse/moamoa/common/advice/CommonControllerAdvice.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
import com.woowacourse.moamoa.common.exception.UnauthorizedException;
1313
import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
1414
import com.woowacourse.moamoa.study.service.exception.FailureParticipationException;
15+
import com.woowacourse.moamoa.study.service.exception.InvalidUpdatingException;
1516
import io.jsonwebtoken.JwtException;
16-
import java.io.PrintStream;
17-
import java.io.PrintWriter;
1817
import lombok.extern.slf4j.Slf4j;
1918
import org.springframework.http.ResponseEntity;
2019
import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -35,6 +34,7 @@ public ResponseEntity<ErrorResponse> handleBadRequest() {
3534
}
3635

3736
@ExceptionHandler({
37+
InvalidUpdatingException.class,
3838
InvalidFormatException.class,
3939
InvalidPeriodException.class,
4040
BadRequestException.class,

backend/src/main/java/com/woowacourse/moamoa/study/domain/Study.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import static javax.persistence.GenerationType.IDENTITY;
44
import static lombok.AccessLevel.PROTECTED;
55

6-
import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException;
76
import com.woowacourse.moamoa.common.exception.UnauthorizedException;
7+
import com.woowacourse.moamoa.referenceroom.service.exception.NotParticipatedMemberException;
88
import com.woowacourse.moamoa.study.domain.exception.InvalidPeriodException;
99
import com.woowacourse.moamoa.study.service.exception.FailureParticipationException;
10+
import com.woowacourse.moamoa.study.service.exception.InvalidUpdatingException;
1011
import com.woowacourse.moamoa.study.service.exception.OwnerCanNotLeaveException;
1112
import java.time.LocalDate;
1213
import java.time.LocalDateTime;
@@ -152,6 +153,19 @@ public MemberRole getRole(final Long memberId) {
152153
public void update(Long memberId, Content content, RecruitPlanner recruitPlanner, AttachedTags attachedTags,
153154
StudyPlanner studyPlanner
154155
) {
156+
if (isRecruitingAfterEndStudy(recruitPlanner, studyPlanner) ||
157+
isRecruitedOrStartStudyBeforeCreatedAt(recruitPlanner, studyPlanner, createdAt)) {
158+
throw new InvalidUpdatingException();
159+
}
160+
161+
if (studyPlanner.isInappropriateCondition(createdAt.toLocalDate())) {
162+
throw new InvalidUpdatingException();
163+
}
164+
165+
if ((recruitPlanner.getMax() != null && recruitPlanner.getMax() < participants.getSize())) {
166+
throw new InvalidUpdatingException();
167+
}
168+
155169
checkOwner(memberId);
156170
this.content = content;
157171
this.recruitPlanner = recruitPlanner;

backend/src/main/java/com/woowacourse/moamoa/study/service/StudyParticipantService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class StudyParticipantService {
1818
private final MemberRepository memberRepository;
1919
private final StudyRepository studyRepository;
2020

21-
public void participateStudy(final Long memberId, final Long studyId) {
21+
public synchronized void participateStudy(final Long memberId, final Long studyId) {
2222
memberRepository.findById(memberId)
2323
.orElseThrow(MemberNotFoundException::new);
2424
final Study study = studyRepository.findById(studyId)

backend/src/main/java/com/woowacourse/moamoa/study/service/StudyService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ public void updateStudy(Long memberId, Long studyId, StudyRequest request) {
7474
Study study = studyRepository.findById(studyId)
7575
.orElseThrow(StudyNotFoundException::new);
7676

77-
study.update(memberId, request.mapToContent(), request.mapToRecruitPlan(), request.mapToAttachedTags(),
78-
request.mapToStudyPlanner(LocalDate.now()));
77+
final Content content = request.mapToContent();
78+
final RecruitPlanner recruitPlanner = request.mapToRecruitPlan();
79+
final StudyPlanner studyPlanner = request.mapToStudyPlanner(LocalDate.now());
80+
81+
study.update(memberId, content, recruitPlanner, request.mapToAttachedTags(), studyPlanner);
7982
}
8083
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
}

backend/src/test/java/com/woowacourse/acceptance/test/study/UpdatingStudyAcceptanceTest.java

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.woowacourse.acceptance.fixture.TagFixtures.BE_태그_ID;
44
import static com.woowacourse.acceptance.fixture.TagFixtures.우테코4기_태그_ID;
55
import static com.woowacourse.acceptance.fixture.TagFixtures.자바_태그_ID;
6+
import static com.woowacourse.acceptance.steps.LoginSteps.디우가;
67
import static com.woowacourse.acceptance.steps.LoginSteps.짱구가;
78
import static org.springframework.http.HttpHeaders.ACCEPT;
89
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
@@ -22,11 +23,11 @@
2223
import org.springframework.http.HttpStatus;
2324
import org.springframework.http.MediaType;
2425

25-
public class UpdatingStudyAcceptanceTest extends AcceptanceTest {
26+
class UpdatingStudyAcceptanceTest extends AcceptanceTest {
2627

2728
@DisplayName("스터디 내용을 수정할 수 있다.")
2829
@Test
29-
public void updateStudy() {
30+
void updateStudy() {
3031
final LocalDate 지금 = LocalDate.now();
3132
final long studyId = 짱구가().로그인하고().자바_스터디를()
3233
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
@@ -55,4 +56,70 @@ public void updateStudy() {
5556
.put("/api/studies/{study-id}")
5657
.then().statusCode(HttpStatus.OK.value());
5758
}
59+
60+
@DisplayName("이전 날짜로 스터디 모집 기간을 변경할 수 없다.")
61+
@Test
62+
void updateStudyWithBeforeDay() {
63+
final LocalDate 지금 = LocalDate.now();
64+
final long studyId = 짱구가().로그인하고().자바_스터디를()
65+
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
66+
.생성한다();
67+
final String accessToken = 짱구가().로그인한다();
68+
69+
final StudyRequest request = new StudyRequestBuilder().title("변경된 제목")
70+
.description("변경된 설명")
71+
.excerpt("변경된 한 줄 설명")
72+
.thumbnail("변경된 썸네일")
73+
.startDate(LocalDate.now())
74+
.endDate(LocalDate.now().plusMonths(1))
75+
.enrollmentEndDate(LocalDate.now().minusDays(1))
76+
.tagIds(List.of(자바_태그_ID, 우테코4기_태그_ID))
77+
.build();
78+
79+
RestAssured.given(spec).log().all()
80+
.filter(document("studies/update",
81+
requestHeaders(headerWithName("Authorization").description("Bearer Token"))))
82+
.header(AUTHORIZATION, accessToken)
83+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
84+
.header(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
85+
.pathParam("study-id", studyId)
86+
.body(request)
87+
.when().log().all()
88+
.put("/api/studies/{study-id}")
89+
.then().statusCode(HttpStatus.BAD_REQUEST.value());
90+
}
91+
92+
@DisplayName("스터디 모집 인원을 현재 인원보다 적게 변경할 수 없다.")
93+
@Test
94+
void updateStudyWithLessThanCurrentMember() {
95+
final LocalDate 지금 = LocalDate.now();
96+
final long studyId = 짱구가().로그인하고().자바_스터디를()
97+
.시작일자는(지금).태그는(자바_태그_ID, 우테코4기_태그_ID, BE_태그_ID)
98+
.생성한다();
99+
디우가().로그인하고().스터디에(studyId).참여한다();
100+
final String accessToken = 짱구가().로그인한다();
101+
102+
final StudyRequest request = new StudyRequestBuilder().title("변경된 제목")
103+
.description("변경된 설명")
104+
.excerpt("변경된 한 줄 설명")
105+
.thumbnail("변경된 썸네일")
106+
.startDate(LocalDate.now())
107+
.endDate(LocalDate.now().plusMonths(1))
108+
.enrollmentEndDate(LocalDate.now().plusDays(5))
109+
.maxMemberCount(1)
110+
.tagIds(List.of(자바_태그_ID, 우테코4기_태그_ID))
111+
.build();
112+
113+
RestAssured.given(spec).log().all()
114+
.filter(document("studies/update",
115+
requestHeaders(headerWithName("Authorization").description("Bearer Token"))))
116+
.header(AUTHORIZATION, accessToken)
117+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
118+
.header(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
119+
.pathParam("study-id", studyId)
120+
.body(request)
121+
.when().log().all()
122+
.put("/api/studies/{study-id}")
123+
.then().statusCode(HttpStatus.BAD_REQUEST.value());
124+
}
58125
}

frontend/.storybook/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'@edit-study-page': resolve(__dirname, '../src/pages/edit-study-page'),
3030
'@my-study-page': resolve(__dirname, '../src/pages/my-study-page'),
3131
'@community-tab': resolve(__dirname, '../src/pages/study-room-page/tabs/community-tab-panel'),
32+
'@notice-tab': resolve(__dirname, '../src/pages/study-room-page/tabs/notice-tab-panel'),
3233
'@study-room-page': resolve(__dirname, '../src/pages/study-room-page'),
3334
'@login-redirect-page': resolve(__dirname, '../src/pages/login-redirect-page'),
3435
'@error-page': resolve(__dirname, '../src/pages/error-page'),

frontend/src/App.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ const App = () => {
5757
path={`${PATH.COMMUNITY_EDIT()}`}
5858
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
5959
/>
60+
<Route
61+
path={`${PATH.NOTICE_ARTICLE()}`}
62+
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
63+
/>
64+
<Route
65+
path={`${PATH.NOTICE_PUBLISH()}`}
66+
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
67+
/>
68+
<Route
69+
path={`${PATH.NOTICE_EDIT()}`}
70+
element={isLoggedIn ? <StudyRoomPage /> : <Navigate to={PATH.MAIN} replace={true} />}
71+
/>
6072
<Route
6173
path={PATH.EDIT_STUDY()}
6274
element={isLoggedIn ? <EditStudyPage /> : <Navigate to={PATH.MAIN} replace={true} />}

frontend/src/api/notice/index.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { AxiosError, AxiosResponse } from 'axios';
2+
import { useMutation, useQuery } from 'react-query';
3+
4+
import type { NoticeArticle } from '@custom-types';
5+
6+
import axiosInstance from '@api/axiosInstance';
7+
8+
export type GetNoticeArticlesResponseData = {
9+
articles: Array<NoticeArticle>;
10+
currentPage: number;
11+
lastPage: number;
12+
totalCount: number;
13+
};
14+
15+
export type GetNoticeArticleResponseData = NoticeArticle;
16+
17+
export type GetNoticeArticlesParams = {
18+
studyId: number;
19+
page?: number;
20+
size?: number;
21+
};
22+
23+
export type GetNoticeArticleParams = {
24+
studyId: number;
25+
articleId: number;
26+
};
27+
28+
export type PostNoticeArticleRequestParams = {
29+
studyId: number;
30+
};
31+
export type PostNoticeArticleRequestBody = {
32+
title: string;
33+
content: string;
34+
};
35+
export type PostNoticeArticleRequestVariables = PostNoticeArticleRequestParams & PostNoticeArticleRequestBody;
36+
export type PostNoticeArticleResponseData = {
37+
studyId: number;
38+
title: string;
39+
content: string;
40+
};
41+
42+
export type PutNoticeArticleRequestParams = {
43+
studyId: number;
44+
articleId: number;
45+
};
46+
export type PutNoticeArticleRequestBody = {
47+
title: string;
48+
content: string;
49+
};
50+
export type PutNoticeArticleRequestVariables = PutNoticeArticleRequestParams & PutNoticeArticleRequestBody;
51+
52+
export type DeleteNoticeArticleRequestParams = {
53+
studyId: number;
54+
articleId: number;
55+
};
56+
57+
const getNoticeArticles = async ({ studyId, page = 1, size = 8 }: GetNoticeArticlesParams) => {
58+
// 서버쪽에서는 page를 0번부터 계산하기 때문에 page - 1을 해줘야 한다
59+
const response = await axiosInstance.get<GetNoticeArticlesResponseData>(
60+
`/api/studies/${studyId}/notice/articles?page=${page - 1}&size=${size}`,
61+
);
62+
const { totalCount, currentPage, lastPage } = response.data;
63+
64+
response.data = {
65+
...response.data,
66+
totalCount: Number(totalCount),
67+
currentPage: Number(currentPage) + 1, // page를 하나 늘려준다 서버에서 0으로 오기 때문이다
68+
lastPage: Number(lastPage),
69+
};
70+
71+
return response.data;
72+
};
73+
74+
const getNoticeArticle = async ({ studyId, articleId }: GetNoticeArticleParams) => {
75+
// 서버쪽에서는 page를 0번부터 계산하기 때문에 page - 1을 해줘야 한다
76+
const response = await axiosInstance.get<GetNoticeArticleResponseData>(
77+
`/api/studies/${studyId}/notice/articles/${articleId}`,
78+
);
79+
return response.data;
80+
};
81+
82+
const postNoticeArticle = async ({ studyId, title, content }: PostNoticeArticleRequestVariables) => {
83+
const response = await axiosInstance.post<null, AxiosResponse<null>, PostNoticeArticleRequestBody>(
84+
`/api/studies/${studyId}/notice/articles`,
85+
{
86+
title,
87+
content,
88+
},
89+
);
90+
91+
return response.data;
92+
};
93+
94+
const putNoticeArticle = async ({ studyId, title, content, articleId }: PutNoticeArticleRequestVariables) => {
95+
const response = await axiosInstance.put<null, AxiosResponse<null>, PutNoticeArticleRequestBody>(
96+
`/api/studies/${studyId}/notice/articles/${articleId}`,
97+
{
98+
title,
99+
content,
100+
},
101+
);
102+
103+
return response.data;
104+
};
105+
106+
const deleteNoticeArticle = async ({ studyId, articleId }: DeleteNoticeArticleRequestParams) => {
107+
const response = await axiosInstance.delete<null, AxiosResponse<null>>(
108+
`/api/studies/${studyId}/notice/articles/${articleId}`,
109+
);
110+
111+
return response.data;
112+
};
113+
114+
export const useGetNoticeArticles = (studyId: number, page: number) => {
115+
return useQuery(['get-notice-articles', studyId, page], () => getNoticeArticles({ studyId, page }));
116+
};
117+
118+
export const useGetNoticeArticle = (studyId: number, articleId: number) => {
119+
return useQuery(['get-notice-article', studyId, articleId], () => getNoticeArticle({ studyId, articleId }));
120+
};
121+
122+
export const usePostNoticeArticle = () => {
123+
return useMutation<null, AxiosError, PostNoticeArticleRequestVariables>(postNoticeArticle);
124+
};
125+
126+
export const usePutNoticeArticle = () => {
127+
return useMutation<null, AxiosError, PutNoticeArticleRequestVariables>(putNoticeArticle);
128+
};
129+
130+
export const useDeleteNoticeArticle = () => {
131+
return useMutation<null, AxiosError, DeleteNoticeArticleRequestParams>(deleteNoticeArticle);
132+
};

frontend/src/constants.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ export const PATH = {
77
STUDY_ROOM: (studyId: ':studyId' | number = ':studyId') => `/studyroom/${studyId}`,
88
LOGIN: '/login',
99
REVIEW: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/reviews`,
10-
COMMUNITY: (studyId: ':studyId' | number = ':studyId') => `/studyroom/${studyId}/community`,
10+
11+
COMMUNITY: (studyId: ':studyId' | string | number = ':studyId') => `/studyroom/${studyId}/community`,
1112
COMMUNITY_ARTICLE: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
1213
`/studyroom/${studyId}/community/article/${articleId}`,
1314
COMMUNITY_PUBLISH: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/community/article/publish`,
1415
COMMUNITY_EDIT: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
1516
`/studyroom/${studyId}/community/article/${articleId}/edit`,
17+
18+
NOTICE: (studyId: ':studyId' | string | number = ':studyId') => `/studyroom/${studyId}/notice`,
19+
NOTICE_ARTICLE: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
20+
`/studyroom/${studyId}/notice/article/${articleId}`,
21+
NOTICE_PUBLISH: (studyId: string | number = ':studyId') => `/studyroom/${studyId}/notice/article/publish`,
22+
NOTICE_EDIT: (studyId: string | number = ':studyId', articleId: string | number = ':articleId') =>
23+
`/studyroom/${studyId}/notice/article/${articleId}/edit`,
1624
};
1725

1826
export const API_ERROR = {

0 commit comments

Comments
 (0)