Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mission): support markdown syntax for mission descriptions #762

Merged
merged 26 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7ce97ae
feat: add property for HTML converted description
woowabrie Jul 10, 2024
07d808d
feat: implement function to retrieve HTML-converted description
woowabrie Jul 10, 2024
b08248f
feat: add preview to check parsed description
woowabrie Jul 8, 2024
dc028b7
feat: 과제 설명의 DB 데이터 타입 변경
woowabrie Jul 8, 2024
75c8893
feat: 초기 데이터의 과제 설명 값 마크다운으로 변경
woowabrie Jul 8, 2024
371c953
refactor: edit name of dto using my missions response
woowabrie Jul 11, 2024
55d19db
feat: 참여 중인 모집의 과제 상세 조회 기능 추가
woowabrie Jul 8, 2024
6570b52
feat: 나의 미션 상세 보기 endpoint 추가
woowabrie Jul 8, 2024
c12b6c5
feat: 내 지원 현황 목록 아이템에서 미션의 설명 제거
woowabrie Jul 17, 2024
3ae10d3
refactor: rename fixture function responding to its name of data
woowabrie Jul 17, 2024
fdb29b2
refactor: 메일 관리 > 미리보기와 동일한 방식으로 미션 관리가 동작하도록 변경
woowabrie Jul 17, 2024
1861855
feat: Mission의 description 만 인자로 받아 변환하도록 변경
woowabrie Jul 17, 2024
e7174d8
refactor: Service에서 Mission description을 변환하도록 변경
woowabrie Jul 17, 2024
010b2e7
refactor: User -> Member 전환에 따른 메서드명 변경
woowabrie Jul 17, 2024
f557d2f
refactor: 사용 안하는 변수 할당 제거
woowabrie Jul 17, 2024
02213d4
feat: Mission 기간 내에만 과제 상세 보기가 가능하도록 조건 추가
woowabrie Jul 18, 2024
e2b13db
refactor: admin에서 공통으로 사용할 미리보기 dialog 추출
woowabrie Jul 18, 2024
e545e26
refactor: 초기 데이터 세팅 시 trimIndent() 대신 trimMargin() 사용
woowabrie Jul 18, 2024
e4c5038
refactor: flyway script 버전 변경
woowabrie Jul 18, 2024
75b7694
test: 나의 미션 상세 조회 ControllerTest 추가
woowabrie Jul 18, 2024
15862ce
refactor: 내 과제 조회 기능 테스트에 적합한 도메인 용어 사용
woowabrie Jul 23, 2024
4eedac4
refactor: 과제 조회 관련 API 문서 제목 변경
woowabrie Jul 23, 2024
01c8230
refactor: MyMissionResponse의 부 생성자 매개변수 순서 조정
woowabrie Jul 23, 2024
48a795d
refactor: 내 과제 조회 코드 가독성 개선
woowabrie Jul 23, 2024
31f49b2
refactor: 과제 조회 여부 확인 코드 가독성 개선
woowabrie Jul 23, 2024
f7e5b18
test(mission): refine test scenarios
woowahan-pjs Jul 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/docs/asciidoc/mission.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
= 과제 관련 API

== 내 과제 조회
== 내 과제 목록 조회

operation::mission-list-me-get[snippets='http-request,http-response']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a: #553 (comment) 의 스니펫 디렉토리 명명 규칙에 따라 잘 작성하셨네요!


== 내 과제 상세 조회
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 내 과제 제출물 조회와 같이 내 과제 조회는 어떤가요? 경로에도 '상세'라는 단어가 드러나지 않으니깐요.

Suggested change
== 내 과제 상세 조회
== 내 과제 조회


operation::mission-me-get[snippets='http-request,http-response']
26 changes: 23 additions & 3 deletions src/main/kotlin/apply/application/MissionDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,9 @@ data class MissionResponse(
)
}

data class MyMissionResponse(
data class MyMissionAndJudgementResponse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: ListMyMissionResponse라는 이름은 어떠세요?
https://en.dict.naver.com/#/entry/enko/d57a6e57bdc0458294a4f66108842840

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제안주신 클래스명으로 바꿨을 경우 리턴 값이 List<ListMyMissionResponse>> 의 형태로 나오게 되어 일단 유지했습니다. ㅋㅋ
네이밍은 같은 미션 관련 DTO인 MissionAndEvaluationResponse 을 참고했어요.

val id: Long,
val title: String,
val description: String,
val submittable: Boolean,
val submitted: Boolean,
val startDateTime: LocalDateTime,
Expand All @@ -115,7 +114,6 @@ data class MyMissionResponse(
) : this(
mission.id,
mission.title,
mission.description,
mission.submittable,
submitted,
mission.period.startDateTime,
Expand All @@ -126,6 +124,28 @@ data class MyMissionResponse(
)
}

data class MyMissionResponse(
val id: Long,
val title: String,
val description: String,
val submittable: Boolean,
val startDateTime: LocalDateTime,
val endDateTime: LocalDateTime,
val status: MissionStatus,
val submitted: Boolean,
) {
constructor(mission: Mission, submitted: Boolean, formattedDescription: String) : this(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a: 사소하지만 부 생성자의 매개 변수 순서를 주 생성자와 동일하게 설정하면 어떨까요?

Suggested change
constructor(mission: Mission, submitted: Boolean, formattedDescription: String) : this(
constructor(mission: Mission, description: String, submitted: Boolean) : this(

mission.id,
mission.title,
formattedDescription,
mission.submittable,
mission.period.startDateTime,
mission.period.endDateTime,
mission.status,
submitted
)
}

data class JudgmentItemData(
var id: Long = 0L,
var testName: String = "",
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/apply/application/MissionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import apply.domain.mission.getOrThrow
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import support.markdownToEmbeddedHtml

@Transactional
@Service
Expand Down Expand Up @@ -120,4 +121,8 @@ class MissionService(
?.let(::EvaluationItemSelectData)
?: EvaluationItemSelectData()
}

fun parseDescription(description: String): String {
return markdownToEmbeddedHtml(description)
}
}
33 changes: 27 additions & 6 deletions src/main/kotlin/apply/application/MyMissionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import apply.domain.judgmentitem.JudgmentItem
import apply.domain.judgmentitem.JudgmentItemRepository
import apply.domain.mission.Mission
import apply.domain.mission.MissionRepository
import apply.domain.mission.getOrThrow
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import support.markdownToEmbeddedHtml

@Transactional(readOnly = true)
@Service
Expand All @@ -25,12 +27,12 @@ class MyMissionService(
private val assignmentRepository: AssignmentRepository,
private val judgmentRepository: JudgmentRepository
) {
fun findAllByMemberIdAndRecruitmentId(memberId: Long, recruitmentId: Long): List<MyMissionResponse> {
fun findAllByMemberIdAndRecruitmentId(memberId: Long, recruitmentId: Long): List<MyMissionAndJudgementResponse> {
val missions = findMissions(memberId, recruitmentId)
if (missions.isEmpty()) return emptyList()

val assignments = assignmentRepository.findAllByMemberId(memberId)
if (assignments.isEmpty()) return missions.map(::MyMissionResponse)
if (assignments.isEmpty()) return missions.map(::MyMissionAndJudgementResponse)

val judgmentItems = judgmentItemRepository.findAllByMissionIdIn(missions.map { it.id })
if (judgmentItems.isEmpty()) return missions.mapBy(assignments)
Expand All @@ -46,23 +48,23 @@ class MyMissionService(
return missionRepository.findAllByEvaluationIdIn(targets.map { it.id }).filterNot { it.hidden }
}

private fun List<Mission>.mapBy(assignments: List<Assignment>): List<MyMissionResponse> {
private fun List<Mission>.mapBy(assignments: List<Assignment>): List<MyMissionAndJudgementResponse> {
return map { mission ->
val assignment = assignments.find { it.missionId == mission.id }
MyMissionResponse(mission, assignment != null)
MyMissionAndJudgementResponse(mission, assignment != null)
}
}

private fun List<Mission>.mapBy(
assignments: List<Assignment>,
judgmentItems: List<JudgmentItem>,
judgments: List<Judgment>
): List<MyMissionResponse> {
): List<MyMissionAndJudgementResponse> {
return map { mission ->
val assignment = assignments.find { it.missionId == mission.id }
val judgmentItem = judgmentItems.find { it.missionId == mission.id }
val judgment = judgments.findLastJudgment(assignment, judgmentItem)
MyMissionResponse(
MyMissionAndJudgementResponse(
mission = mission,
submitted = assignment != null,
runnable = assignment != null && judgmentItem != null,
Expand Down Expand Up @@ -94,4 +96,23 @@ class MyMissionService(
judgmentRecord = judgment?.lastRecord
)
}

fun findByMemberIdAndMissionId(memberId: Long, missionId: Long): MyMissionResponse {
val mission = missionRepository.getOrThrow(missionId)

evaluationTargetRepository.findByEvaluationIdAndMemberId(mission.evaluationId, memberId)
?: throw NoSuchElementException("과제가 존재하지 않습니다. id: $missionId")

if (!mission.isDescriptionViewable) {
throw NoSuchElementException("과제가 존재하지 않습니다. id: $missionId")
}

val assignment = assignmentRepository.findByMemberIdAndMissionId(memberId, missionId)

return MyMissionResponse(
mission = mission,
submitted = assignment != null,
markdownToEmbeddedHtml(mission.description),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 아래 코드는 어떨까요? 조건문에 중괄호를 추가하거나, 생성자를 호출할 때 인자에 줄 바꿈을 추가할 수도 있습니다.

Suggested change
val mission = missionRepository.getOrThrow(missionId)
evaluationTargetRepository.findByEvaluationIdAndMemberId(mission.evaluationId, memberId)
?: throw NoSuchElementException("과제가 존재하지 않습니다. id: $missionId")
if (!mission.isDescriptionViewable) {
throw NoSuchElementException("과제가 존재하지 않습니다. id: $missionId")
}
val assignment = assignmentRepository.findByMemberIdAndMissionId(memberId, missionId)
return MyMissionResponse(
mission = mission,
submitted = assignment != null,
markdownToEmbeddedHtml(mission.description),
)
val mission = missionRepository.getOrThrow(memberId)
val evaluationTarget = evaluationTargetRepository.findByEvaluationIdAndMemberId(mission.evaluationId, memberId)
if (!mission.isDescriptionViewable || evaluationTarget == null) throw NoSuchElementException("과제가 존재하지 않습니다. id: $missionId")
val assignment = assignmentRepository.findByMemberIdAndMissionId(memberId, missionId)
return MyMissionResponse(mission, markdownToEmbeddedHtml(mission.description), submitted = assignment != null)

}
}
20 changes: 18 additions & 2 deletions src/main/kotlin/apply/config/DatabaseInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,15 @@ class DatabaseInitializer(
val missions = listOf(
Mission(
title = "1주 차 프리코스 - 숫자 야구 게임",
description = "https://github.com/woowacourse/java-baseball-precourse",
description = """
|# 미션 - 숫자 야구 게임
|
|## 🔍 진행 방식
|
|- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
|- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
|- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.
""".trimMargin(),
evaluationId = 2L,
startDateTime = createLocalDateTime(2020, 11, 24, 15),
endDateTime = createLocalDateTime(2120, 12, 1, 0),
Expand All @@ -421,7 +429,15 @@ class DatabaseInitializer(
),
Mission(
title = "2주 차 프리코스 - 자동차 경주 게임",
description = "https://github.com/woowacourse/java-racingcar-precourse",
description = """
|# 미션 - 자동차 경주 게임
|
|## 🔍 진행 방식
|
|- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
|- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
|- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.
""".trimMargin(),
evaluationId = 3L,
startDateTime = createLocalDateTime(2020, 12, 1, 15),
endDateTime = createLocalDateTime(2120, 12, 8, 0),
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/apply/domain/mission/Mission.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.time.LocalDateTime
import javax.persistence.Column
import javax.persistence.Embedded
import javax.persistence.Entity
import javax.persistence.Lob

@SQLDelete(sql = "update mission set deleted = true where id = ?")
@Where(clause = "deleted = false")
Expand All @@ -16,6 +17,7 @@ class Mission(
val title: String,

@Column(nullable = false)
@Lob
val description: String,

@Column(nullable = false)
Expand All @@ -40,6 +42,9 @@ class Mission(
val isSubmitting: Boolean
get() = status == MissionStatus.SUBMITTING

val isDescriptionViewable: Boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c: 개인 취향이에요! readable이나 readableDescription은 어떠세요?

get() = !hidden && (status == MissionStatus.SUBMITTING || status == MissionStatus.UNSUBMITTABLE)
woowabrie marked this conversation as resolved.
Show resolved Hide resolved

constructor(
title: String,
description: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package apply.ui.admin.mail
package apply.ui.admin

import com.vaadin.flow.component.Component
import com.vaadin.flow.component.Html
Expand All @@ -11,7 +11,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout
import org.jsoup.Jsoup
import support.views.createContrastButton

class MailPreviewDialog(
class PreviewDialog(
htmlText: String
) : Dialog() {
init {
Expand All @@ -22,7 +22,7 @@ class MailPreviewDialog(
}

private fun createHeader(): VerticalLayout {
return VerticalLayout(H2("메일 미리 보기")).apply {
return VerticalLayout(H2("미리 보기")).apply {
isPadding = false
element.style["margin-bottom"] = "10px"
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/apply/ui/admin/mail/MailsFormView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import apply.application.RecruitmentService
import apply.application.mail.MailData
import apply.application.mail.MailService
import apply.ui.admin.BaseLayout
import apply.ui.admin.PreviewDialog
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.button.Button
Expand Down Expand Up @@ -82,7 +83,7 @@ class MailsFormView(

private fun createPreviewButton(): Button {
return createContrastButton("미리 보기") {
handleMailData { mail -> MailPreviewDialog(mailService.generateMailBody(mail)) }
handleMailData { mail -> PreviewDialog(mailService.generateMailBody(mail)) }
}
}

Expand Down
39 changes: 30 additions & 9 deletions src/main/kotlin/apply/ui/admin/mission/MissionsFormView.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package apply.ui.admin.mission

import apply.application.EvaluationService
import apply.application.MissionData
import apply.application.MissionService
import apply.ui.admin.BaseLayout
import apply.ui.admin.PreviewDialog
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.button.Button
Expand All @@ -22,6 +24,7 @@ import support.views.createPrimaryButton
import support.views.toDisplayName

private val MISSION_FORM_URL_PATTERN: Regex = Regex("^(\\d*)/?(\\d*)/?($NEW_VALUE|$EDIT_VALUE)$")
private const val DATA_NOT_BIND_MESSAGE: String = "모든 항목이 잘 입력되었는지 확인해 주세요."

@Route(value = "admin/missions", layout = BaseLayout::class)
class MissionsFormView(
Expand Down Expand Up @@ -54,11 +57,18 @@ class MissionsFormView(
submitButton.text = displayName
}

private fun createButtons(): Component {
return HorizontalLayout(submitButton, createCancelButton(), createPreviewButton()).apply {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
}
}

private fun createSubmitButton(): Button {
return createPrimaryButton {
missionForm.bindOrNull()?.let {
handleMissionData { mission ->
try {
missionService.save(it)
missionService.save(mission)
UI.getCurrent().navigate(MissionsView::class.java, recruitmentId)
} catch (e: Exception) {
createNotification(e.localizedMessage)
Expand All @@ -67,16 +77,27 @@ class MissionsFormView(
}
}

private fun createButtons(): Component {
return HorizontalLayout(submitButton, createCancelButton()).apply {
setSizeFull()
justifyContentMode = FlexComponent.JustifyContentMode.CENTER
}
}

private fun createCancelButton(): Button {
return createContrastButton("취소") {
UI.getCurrent().navigate(MissionsView::class.java, recruitmentId)
}
}

private fun createPreviewButton(): Button {
return createContrastButton("미리보기") {
handleMissionData { mission ->
val formattedDescription = missionService.parseDescription(mission.description)
PreviewDialog("<div>$formattedDescription</div>")
}
}
}

private fun handleMissionData(action: (MissionData) -> Unit) {
val result = missionForm.bindOrNull()
if (result == null) {
createNotification(DATA_NOT_BIND_MESSAGE)
} else {
action(result)
}
}
}
13 changes: 12 additions & 1 deletion src/main/kotlin/apply/ui/api/MissionRestController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import apply.application.MissionAndEvaluationResponse
import apply.application.MissionData
import apply.application.MissionResponse
import apply.application.MissionService
import apply.application.MyMissionAndJudgementResponse
import apply.application.MyMissionResponse
import apply.application.MyMissionService
import apply.domain.member.Member
Expand Down Expand Up @@ -58,11 +59,21 @@ class MissionRestController(
fun findMyMissionsByRecruitmentId(
@PathVariable recruitmentId: Long,
@LoginMember member: Member
): ResponseEntity<ApiResponse<List<MyMissionResponse>>> {
): ResponseEntity<ApiResponse<List<MyMissionAndJudgementResponse>>> {
val responses = missionQueryService.findAllByMemberIdAndRecruitmentId(member.id, recruitmentId)
return ResponseEntity.ok(ApiResponse.success(responses))
}

@GetMapping("/{missionId}/me")
fun findMyMission(
@PathVariable recruitmentId: Long,
@PathVariable missionId: Long,
@LoginMember member: Member
): ResponseEntity<ApiResponse<MyMissionResponse>> {
val response = missionQueryService.findByMemberIdAndMissionId(member.id, missionId)
return ResponseEntity.ok(ApiResponse.success(response))
}

@DeleteMapping("/{missionId}")
fun deleteById(
@PathVariable recruitmentId: Long,
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/db/migration/V4_5__Alter_mission_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table mission
modify description longtext not null;
Loading