Skip to content

Commit

Permalink
feat(apply): implement the mission view page
Browse files Browse the repository at this point in the history
  • Loading branch information
woowahan-cron authored Sep 12, 2024
1 parent ec25e40 commit 7ca413d
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 3 deletions.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"dependencies": {
"axios": "^0.21.1",
"classnames": "^2.3.1",
"github-markdown-css": "^5.6.1",
"highlight.js": "^11.10.0",
"react": "^18.2.0",
"react-datepicker": "^4.2.1",
"react-dom": "^18.2.0",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import RecruitmentProvider from "./provider/RecruitmentProvider";
import TokenProvider from "./provider/TokenProvider";
import MemberInfoProvider from "./provider/MemberInfoProvider";
import { ModalProvider } from "./hooks/useModalContext";
import MissionView from "./pages/MissionView/MissionView";

const App = () => {
return (
Expand All @@ -45,6 +46,7 @@ const App = () => {
<Route path={PATH.EDIT_PASSWORD} element={<PasswordEdit />} />
<Route path={PATH.MY_APPLICATION} element={<MyApplication />} />
<Route path={PATH.ASSIGNMENT} element={<AssignmentSubmit />} />
<Route path={PATH.MISSION_VIEW} element={<MissionView />} />
</Route>

<Route element={<PrivateRoute />}>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/api/recruitments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export type PostJudgmentRequest = RequestWithToken<{

export type PostJudgmentResponseData = Mission["judgment"];

export type FetchMissionRequest = RequestWithToken<{
recruitmentId: string;
missionId: number;
}>;

export const fetchRecruitmentItems = (recruitmentId: FetchRecruitmentItemsRequest) =>
axios.get<FetchRecruitmentItemsResponseData>(`/api/recruitments/${recruitmentId}/items`);

Expand Down Expand Up @@ -117,3 +122,13 @@ export const patchAssignment = ({
assignmentData,
headers({ token })
);

export const fetchMissionRequirements = ({
token,
recruitmentId,
missionId,
}: FetchAssignmentRequest) =>
axios.get<Mission>(
`/api/recruitments/${recruitmentId}/missions/${missionId}/me`,
headers({ token })
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.assignment-button {
background-color: var(--blue);
}

.refresh-button {
background-color: var(--blue-gray-500);
}
Expand All @@ -11,6 +15,7 @@
}

@media screen and (max-width: 800px) {
.assignment-button,
.judgment-button,
.apply-button,
.refresh-button {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ const MyMissionItem = ({ mission, recruitmentId }: MyMissionItemProps) => {
);
};

const routeToMissionView = () => {
navigate(
generatePath(PATH.MISSION_VIEW, {
recruitmentId,
missionId: String(mission.id),
})
);
};

return (
<div className={classNames(styles["content-box"])}>
<div className={styles["content-wrapper"]}>
Expand All @@ -49,6 +58,15 @@ const MyMissionItem = ({ mission, recruitmentId }: MyMissionItemProps) => {
{missionItem.title}
</RecruitmentDetail>
<ul className={styles["title-button-list"]}>
<li>
<Button
className={buttonStyles["assignment-button"]}
disabled={missionItem.status !== MISSION_STATUS.SUBMITTING}
onClick={routeToMissionView}
>
과제 보기
</Button>
</li>
<li>
<Button
className={buttonStyles["apply-button"]}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const PATH = {
SIGN_UP: "/sign-up",
MY_APPLICATION: "/applications/me",
ASSIGNMENT: "/assignment/:status",
MISSION_VIEW: "/assignment/:recruitmentId/mission/:missionId",
APPLICATION_FORM: "/application-forms/:status",
LOGIN: "/login",
FIND_PASSWORD: "/find",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/hooks/useMission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type MissionProps = {
recruitmentId: string;
};

const missionLabel = (submitted: boolean, missionStatus: Mission["status"]) => {
export const getMissionLabel = (submitted: boolean, missionStatus: Mission["status"]) => {
const labelMap = {
SUBMITTABLE: BUTTON_LABEL.BEFORE_SUBMIT,
SUBMITTING: submitted ? BUTTON_LABEL.EDIT : BUTTON_LABEL.SUBMIT,
Expand All @@ -22,7 +22,7 @@ const missionLabel = (submitted: boolean, missionStatus: Mission["status"]) => {
const useMission = ({ mission, recruitmentId }: MissionProps) => {
const [missionItem, setMissionItem] = useState<Mission>({ ...mission });

const applyButtonLabel = missionLabel(mission.submitted, mission.status);
const applyButtonLabel = getMissionLabel(mission.submitted, mission.status);

useEffect(() => {
setMissionItem(mission);
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/pages/MissionView/MissionView.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.buttons {
width: 100%;
margin-top: 3.5rem;
display: flex;
justify-content: space-between;
gap: 1rem;
}

.buttons li {
flex: 1;
}

.buttons li > button {
width: 100%;
}

.mission-viewer-body li {
list-style: disc;
}
118 changes: 118 additions & 0 deletions frontend/src/pages/MissionView/MissionView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { generatePath, useNavigate, useParams } from "react-router-dom";
import Button, { BUTTON_VARIANT } from "../../components/@common/Button/Button";
import Container from "../../components/@common/Container/Container";
import useTokenContext from "../../hooks/useTokenContext";
import styles from "./MissionView.module.css";
import { PARAM, PATH } from "../../constants/path";
import { fetchMissionRequirements } from "../../api";
import { useEffect, useState } from "react";
import highlighter from "highlight.js";
import "github-markdown-css/github-markdown-light.css";
import "highlight.js/styles/github.css";
import { Mission } from "../../../types/domains/recruitments";
import { getMissionLabel } from "../../hooks/useMission";
import { MISSION_STATUS } from "../../constants/recruitment";
import { AxiosError } from "axios";

const MissionView = () => {
const { token } = useTokenContext();
const navigate = useNavigate();

const { recruitmentId, missionId } = useParams<{ recruitmentId: string; missionId: string }>();
const [mission, setMission] = useState<Mission | null>(null);
const description = mission?.description ?? "";

const isMissionSubmittable =
!mission ||
mission?.submittable ||
mission?.status === MISSION_STATUS.SUBMITTABLE ||
mission?.status === MISSION_STATUS.SUBMITTING;

const missionLabel = getMissionLabel(
mission?.submitted ?? false,
mission?.status ?? MISSION_STATUS.UNSUBMITTABLE
);

const goBack = () => {
navigate(-1);
};

const routeToAssignmentSubmit = () => {
const isSubmitted = mission?.submitted;

navigate(
{
pathname: generatePath(PATH.ASSIGNMENT, {
status: isSubmitted ? PARAM.ASSIGNMENT_STATUS.EDIT : PARAM.ASSIGNMENT_STATUS.NEW,
}),
},
{
state: {
recruitmentId,
currentMission: mission,
},
}
);
};

const fetchRequirement = async () => {
if (!recruitmentId || !missionId) {
return;
}

try {
const response = await fetchMissionRequirements({
token,
recruitmentId: parseInt(recruitmentId, 10),
missionId: parseInt(missionId, 10),
});

setMission(response?.data);
} catch (error) {
alert((error as AxiosError).response?.data.message);

goBack();
}
};

useEffect(() => {
if (!recruitmentId || !missionId) {
goBack();
return;
}

fetchRequirement();
}, []);

useEffect(() => {
/*
url - https://highlightjs.org/
<pre><code> 태그 내의 코드를 자동으로 감지하여 하이라이팅합니다.
자동 감지가 실패할 경우, class 속성에 언어를 명시적으로 지정할 수 있습니다.
*/
highlighter.highlightAll();
}, [description]);

return (
<Container>
<div
className={`${styles["mission-viewer-body"]} markdown-body`}
dangerouslySetInnerHTML={{ __html: description }}
/>
<ul className={styles.buttons}>
<li>
<Button type="button" variant={BUTTON_VARIANT.OUTLINED} onClick={goBack}>
뒤로가기
</Button>
</li>
<li>
<Button type="button" onClick={routeToAssignmentSubmit} disabled={!isMissionSubmittable}>
{missionLabel}
</Button>
</li>
</ul>
</Container>
);
};

export default MissionView;
10 changes: 10 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8422,6 +8422,11 @@ giget@^1.0.0:
pathe "^1.1.0"
tar "^6.1.13"

github-markdown-css@^5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-5.6.1.tgz#8ca3d5c3d93d79ea429fddafea091347ab374f78"
integrity sha512-DItLFgHd+s7HQmk63YN4/TdvLeRqk1QP7pPKTTPrDTYoI5x7f/luJWSOZxesmuxBI2srHp8RDyoZd+9WF+WK8Q==

github-slugger@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
Expand Down Expand Up @@ -8660,6 +8665,11 @@ headers-polyfill@^3.1.0:
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.1.1.tgz#798e265f80edfc53fb8fd01e8963b356b12c2514"
integrity sha512-ifvvIC+VDeLTEkJDwxECSI7k9rJF7sJavCh/UfhGsgJ+LXMbGILRf+NXhc4znuf+JA5X2/leQdOXk6Lq6Y2/wQ==

highlight.js@^11.10.0:
version "11.10.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==

history@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/apply/application/MyMissionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class MyMissionService(
private fun findMissions(memberId: Long, recruitmentId: Long): List<Mission> {
val evaluationIds = evaluationRepository.findAllByRecruitmentId(recruitmentId).map { it.id }
val targets = evaluationTargetRepository.findAllByMemberIdAndEvaluationIdIn(memberId, evaluationIds)
return missionRepository.findAllByEvaluationIdIn(targets.map { it.id }).filterNot { it.hidden }
return missionRepository.findAllByEvaluationIdIn(targets.map { it.evaluationId }).filterNot { it.hidden }
}

private fun List<Mission>.mapBy(assignments: List<Assignment>): List<MyMissionAndJudgementResponse> {
Expand Down
42 changes: 42 additions & 0 deletions src/main/kotlin/apply/config/DatabaseInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,48 @@ class DatabaseInitializer(
|- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
|- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
|- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.
|
|---
|
|## 🎯 프로그래밍 요구 사항
|
|### 라이브러리
|
|- `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다.
| - Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다.
| - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.
|
|#### 사용 예시
|
|```java
|List<Integer> computer = new ArrayList<>();
|while (computer.size() < 3) {
| int randomNumber = Randoms.pickNumberInRange(1, 9);
| if (!computer.contains(randomNumber)) {
| computer.add(randomNumber);
| }
|}
|```
|
|```javascript
|const computer = [];
|while (computer.length < 3) {
| const number = MissionUtils.Random.pickNumberInRange(1, 9);
| if (!computer.includes(number)) {
| computer.push(number);
| }
|}
|```
|
|```kotlin
|val computer = mutableListOf()
|while (computer.size() < 3) {
| val randomNumber = Randoms.pickNumberInRange(1, 9)
| if (!computer.contains(randomNumber)) {
| computer.add(randomNumber)
| }
|}
|```
""".trimMargin(),
evaluationId = 2L,
startDateTime = createLocalDateTime(2020, 11, 24, 15),
Expand Down
24 changes: 24 additions & 0 deletions src/test/kotlin/support/MarkdownTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package support

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldContainInOrder
import io.kotest.matchers.string.shouldNotContain

class MarkdownTest : StringSpec({
Expand Down Expand Up @@ -61,4 +63,26 @@ class MarkdownTest : StringSpec({
val actual = markdownToEmbeddedHtml(markdownText)
actual shouldNotContain "<body>"
}

"코드 포맷을 사용하는 경우 <code> 태그가 추가된다" {
val markdownText = """
|`buildSrc`
""".trimMargin()
val actual = markdownToEmbeddedHtml(markdownText)
actual shouldContain "<code>buildSrc</code>"
}

"프로그래밍 언어를 지정하면 <code> 태그에 클래스 속성이 지정된다" {
val markdownText = """
|```kotlin
|class Cat {
| fun purr() {
| println("Purr purr")
| }
|}
|```
""".trimMargin()
val actual = markdownToEmbeddedHtml(markdownText)
actual.shouldContainInOrder("<pre>", "<code class=\"language-kotlin\">", "</code>", "</pre>")
}
})

0 comments on commit 7ca413d

Please sign in to comment.