Skip to content

Commit

Permalink
[ALL] 하루스터디 v1.1.1 배포 👏 #426
Browse files Browse the repository at this point in the history
[ALL] 하루스터디 v1.1.1 배포 👏
  • Loading branch information
jaehee329 authored Aug 18, 2023
2 parents fc51b7d + eae33e6 commit 04dbeec
Show file tree
Hide file tree
Showing 29 changed files with 503 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "진행 관련 기능")
@RequiredArgsConstructor
Expand Down Expand Up @@ -102,4 +108,23 @@ public ResponseEntity<Void> participate(
return ResponseEntity.created(
URI.create("/api/studies/" + studyId + "/progresses/" + progressId)).build();
}

@SwaggerExceptionResponse({
RoomNotFoundException.class,
MemberNotFoundException.class,
AuthorizationException.class,
PomodoroProgressNotFoundException.class,
ProgressNotBelongToRoomException.class
})
@Operation(summary = "스터디 진행도 삭제")
@ApiResponse(responseCode = "204")
@DeleteMapping("/api/studies/{studyId}/progresses/{progressId}")
public ResponseEntity<Void> delete(
@Authenticated AuthMember authMember,
@PathVariable Long studyId,
@PathVariable Long progressId
) {
pomodoroProgressService.deleteProgress(authMember, studyId, progressId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class PomodoroProgress extends BaseTimeEntity {
@JoinColumn(name = "member_id")
private Member member;

@OneToMany(mappedBy = "pomodoroProgress", cascade = CascadeType.PERSIST)
@OneToMany(mappedBy = "pomodoroProgress", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<PomodoroContent> pomodoroContents = new ArrayList<>();

private String nickname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ public class PomodoroProgressService {
public PomodoroProgressResponse findPomodoroProgress(
AuthMember authMember, Long studyId, Long progressId
) {
Member member = memberRepository.findByIdIfExists(authMember.id());
PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId);
PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId);
validateProgressIsRelatedWith(pomodoroProgress, member, pomodoroRoom);
validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom);
return PomodoroProgressResponse.from(pomodoroProgress);
}

Expand Down Expand Up @@ -82,11 +81,10 @@ private PomodoroProgressesResponse getPomodoroProgressesResponseWithMemberFilter
}

public void proceed(AuthMember authMember, Long studyId, Long progressId) {
Member member = memberRepository.findByIdIfExists(authMember.id());
PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId);
PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId);

validateProgressIsRelatedWith(pomodoroProgress, member, pomodoroRoom);
validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom);
pomodoroProgress.proceed();
}

Expand All @@ -107,13 +105,31 @@ private void validateIsSameMemberId(AuthMember authMember, Long memberId) {
}

private void validateProgressIsRelatedWith(
PomodoroProgress pomodoroProgress, Member member, PomodoroRoom pomodoroRoom
PomodoroProgress pomodoroProgress, AuthMember authMember, PomodoroRoom pomodoroRoom
) {
validateMemberOwns(pomodoroProgress, authMember);
validateProgressIsIncludedIn(pomodoroRoom, pomodoroProgress);
}

private void validateMemberOwns(PomodoroProgress pomodoroProgress, AuthMember authMember) {
Member member = memberRepository.findByIdIfExists(authMember.id());
if (!pomodoroProgress.isOwnedBy(member)) {
throw new AuthorizationException();
}
}

private void validateProgressIsIncludedIn(PomodoroRoom pomodoroRoom,
PomodoroProgress pomodoroProgress) {
if (pomodoroProgress.isNotIncludedIn(pomodoroRoom)) {
throw new ProgressNotBelongToRoomException();
}
}

public void deleteProgress(AuthMember authMember, Long studyId, Long progressId) {
PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId);
validateEverParticipated(authMember, pomodoroRoom);
PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId);
validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom);
pomodoroProgressRepository.delete(pomodoroProgress);
}
}
5 changes: 5 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,8 @@ export const requestRegisterProgress = (nickname: string, studyId: string, membe
headers: { Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ memberId, nickname }),
});

export const requestDeleteProgress = (studyId: string, progressId: number, accessToken: string) =>
http.delete(`/api/studies/${studyId}/progresses/${progressId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
30 changes: 30 additions & 0 deletions frontend/src/assets/icons/ArrowIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Props = {
direction?: 'up' | 'down';
};

const ArrowIcon = ({ direction = 'down' }: Props) => {
return (
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_1371_134)">
<path
d={
direction === 'up'
? 'M17.7085 28.125L25.0002 21.875L32.2918 28.125'
: 'M32.2915 21.875L24.9998 28.125L17.7082 21.875'
}
stroke="black"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_1371_134">
<rect width="16.6667" height="16.6667" fill="white" transform="translate(16.6665 16.6667)" />
</clipPath>
</defs>
</svg>
);
};

export default ArrowIcon;
100 changes: 100 additions & 0 deletions frontend/src/components/board/GuideModal/GuideModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { css, styled } from 'styled-components';

import Button from '@Components/common/Button/Button';
import Typography from '@Components/common/Typography/Typography';

import color from '@Styles/color';

import { useModal } from '@Contexts/ModalProvider';

type RequiredQuestion = 'toDo' | 'completionCondition' | 'doneAsExpected';

const contents = {
title: {
toDo: '이번 사이클에서 학습할 것은 무엇인가요?',
completionCondition: '그 학습의 완료 조건은 무엇인가요?',
doneAsExpected: '실제로 학습이 어떻게 됐나요? 예상대로 잘 이루어졌나요?',
},
descriptions: {
toDo: [
'• 학습하고자 하는 분야나 주제를 명확하게 정의하세요.',
'• 큰 목표를 작은 단계로 나눠보세요.',
'• 예시 1) 자바스크립트 딥다이브 챕터15-1 독서 및 정리',
'• 예시 2) 영단어 20개 외우기',
'• 예시 3) 한국사 강의 3강(청동기 시대) 시청하기',
],
completionCondition: [
'• 학습한 내용에 대한 구체적인 결과물을 만들어 보세요.',
'• 자기 평가를 통해 얼마나 잘 이해하고 있는지 확인해보세요.',
'• 예시 1) 자바스크립트 호이스팅 개념 스터디원에게 설명하기',
'• 예시 2) 학습한 영어 단어 테스트하여 만점 받기',
'• 예시 3) 청동기 시대에 관련된 문제 5개 만들고 공유하기',
],
doneAsExpected: [],
},
} as const;

type Props = {
question: RequiredQuestion;
};

const GuideModal = ({ question }: Props) => {
const { closeModal } = useModal();

return (
<GuideLayout>
<Typography variant="h6">{contents.title[question]}</Typography>
<Separator />
<Description>
{contents.descriptions[question].map((description) => (
<Typography key={description} variant="p3">
{description}
</Typography>
))}
</Description>
<Button
variant="outlined"
$style={css`
color: ${color.blue[500]};
border: none;
&:hover {
&:enabled {
background-color: ${color.blue[50]};
}
}
`}
size="x-small"
$block={false}
onClick={closeModal}
>
확인
</Button>
</GuideLayout>
);
};

const GuideLayout = styled.div`
padding: 5px 10px;
button {
margin-top: 18px;
float: right;
}
`;

const Separator = styled.div`
width: 100%;
height: 1px;
margin: 24px 0;
background-color: ${color.neutral[200]};
`;

const Description = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`;

export default GuideModal;
77 changes: 71 additions & 6 deletions frontend/src/components/board/PlanningForm/PlanningForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { styled } from 'styled-components';

import Button from '@Components/common/Button/Button';
import QuestionTextarea from '@Components/common/QuestionTextarea/QuestionTextarea';
import Typography from '@Components/common/Typography/Typography';

import usePlanningForm from '@Hooks/board/usePlanningForm';
import useDisplay from '@Hooks/common/useDisplay';

import { PLAN_QUESTIONS } from '@Constants/study';

import { getKeys } from '@Utils/getKeys';
import { useModal } from '@Contexts/ModalProvider';

import type { Plan } from '@Types/study';
import ArrowIcon from '@Assets/icons/ArrowIcon';

import GuideModal from '../GuideModal/GuideModal';

type Props = {
onClickSubmitButton: () => Promise<void>;
Expand All @@ -24,6 +28,9 @@ const PlanningForm = ({ onClickSubmitButton, studyId, progressId }: Props) => {
onClickSubmitButton,
);

const { isShow: isOpenOptionalQuestion, toggleShow: toggleOptionalQuestion } = useDisplay();
const { openModal } = useModal();

const handleClickButton = async () => {
try {
await submitForm();
Expand All @@ -33,11 +40,48 @@ const PlanningForm = ({ onClickSubmitButton, studyId, progressId }: Props) => {
}
};

const handleClickGuideButton = (question: 'toDo' | 'completionCondition') => () => {
openModal(<GuideModal question={question} />);
};

return (
<Layout>
{getKeys<Plan>(PLAN_QUESTIONS).map((key) => (
<QuestionTextarea key={key} question={PLAN_QUESTIONS[key]} {...questionTextareaProps[key]} />
))}
<QuestionLayout>
<Typography variant="h5" fontWeight="600">
다음 항목에 답변해주세요.
</Typography>
<QuestionList>
<QuestionTextarea
question={PLAN_QUESTIONS.toDo}
onClickGuideButton={handleClickGuideButton('toDo')}
{...questionTextareaProps.toDo}
/>
<QuestionTextarea
question={PLAN_QUESTIONS.completionCondition}
onClickGuideButton={handleClickGuideButton('completionCondition')}
{...questionTextareaProps.completionCondition}
/>
</QuestionList>
<OptionalQuestionToggle onClick={toggleOptionalQuestion}>
<Typography variant="h5" fontWeight="600">
더 구체적인 목표 설정을 원한다면?
</Typography>
<ArrowIcon direction={isOpenOptionalQuestion ? 'up' : 'down'} />
</OptionalQuestionToggle>
{isOpenOptionalQuestion && (
<QuestionList>
<QuestionTextarea
question={PLAN_QUESTIONS.expectedProbability}
{...questionTextareaProps.expectedProbability}
/>
<QuestionTextarea
question={PLAN_QUESTIONS.expectedDifficulty}
{...questionTextareaProps.expectedDifficulty}
/>
<QuestionTextarea question={PLAN_QUESTIONS.whatCanYouDo} {...questionTextareaProps.whatCanYouDo} />
</QuestionList>
)}
</QuestionLayout>
<Button
variant="primary"
type="submit"
Expand All @@ -59,9 +103,30 @@ const Layout = styled.div`
display: flex;
flex-direction: column;
gap: 60px;
gap: 30px;
padding: 60px 85px;
`;

const QuestionLayout = styled.div`
width: 100%;
height: 90%;
display: flex;
flex-direction: column;
gap: 40px;
overflow-y: auto;
`;

const QuestionList = styled.ul`
display: flex;
flex-direction: column;
gap: 60px;
`;

const OptionalQuestionToggle = styled.button`
display: flex;
align-items: center;
gap: 10px;
`;
Loading

0 comments on commit 04dbeec

Please sign in to comment.