Skip to content

Commit 6becd7a

Browse files
authored
Merge pull request #120 from kjoon418/kjoon418
feat: 임시저장 개선 및 엔티티 연관 구조 수정
2 parents f3dde77 + 7a3741d commit 6becd7a

16 files changed

Lines changed: 246 additions & 67 deletions

File tree

src/main/kotlin/goodspace/bllsoneshot/entity/assignment/Answer.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ package goodspace.bllsoneshot.entity.assignment
33
import goodspace.bllsoneshot.entity.BaseEntity
44
import jakarta.persistence.Column
55
import jakarta.persistence.Entity
6-
import jakarta.persistence.FetchType
7-
import jakarta.persistence.JoinColumn
8-
import jakarta.persistence.OneToOne
96

107
@Entity
118
class Answer(
12-
@OneToOne(mappedBy = "answer", fetch = FetchType.LAZY)
13-
@JoinColumn(nullable = false)
14-
val question: Comment,
9+
@Column(nullable = true)
10+
var content: String? = null,
1511

16-
@Column(nullable = false)
17-
val content: String
12+
@Column(nullable = true)
13+
var temporaryContent: String? = null
1814
) : BaseEntity()

src/main/kotlin/goodspace/bllsoneshot/entity/assignment/Comment.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
package goodspace.bllsoneshot.entity.assignment
22

33
import goodspace.bllsoneshot.entity.BaseEntity
4+
import jakarta.persistence.CascadeType
45
import jakarta.persistence.Column
56
import jakarta.persistence.Entity
67
import jakarta.persistence.EnumType
78
import jakarta.persistence.Enumerated
8-
import jakarta.persistence.CascadeType
99
import jakarta.persistence.FetchType
10-
import jakarta.persistence.JoinColumn
1110
import jakarta.persistence.ManyToOne
1211
import jakarta.persistence.OneToOne
13-
import jakarta.persistence.Table
1412
import jakarta.persistence.Transient
1513

1614
@Entity
1715
class Comment(
18-
@ManyToOne(fetch = FetchType.LAZY)
19-
@JoinColumn(nullable = false)
20-
val task: Task,
21-
2216
@ManyToOne(fetch = FetchType.LAZY)
2317
val proofShot: ProofShot,
2418
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
@@ -61,6 +55,10 @@ class Comment(
6155
val isConfirmed: Boolean
6256
get() = registerStatus == RegisterStatus.CONFIRMED
6357

58+
@get:Transient
59+
val isTemporary: Boolean
60+
get() = registerStatus == RegisterStatus.TEMPORARY
61+
6462
@get:Transient
6563
val isRead: Boolean
6664
get() = readByMentee
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package goodspace.bllsoneshot.entity.assignment
22

33
import goodspace.bllsoneshot.entity.BaseEntity
4-
import jakarta.persistence.Column
54
import jakarta.persistence.Entity
65

76
@Entity
87
class GeneralComment(
9-
@Column(nullable = false)
10-
var content: String
8+
var content: String? = null,
9+
var temporaryContent: String? = null
1110
) : BaseEntity()

src/main/kotlin/goodspace/bllsoneshot/entity/assignment/ProofShot.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ class ProofShot(
3131
get() = comments.filter { it.isFeedback && it.isConfirmed }
3232

3333
@get:Transient
34-
val allFeedbackComments: List<Comment>
35-
get() = comments.filter { it.isFeedback }
34+
val temporaryFeedbackComments: List<Comment>
35+
get() = comments.filter { it.isFeedback && it.isTemporary }
36+
37+
fun hasFeedback(): Boolean =
38+
comments.any { it.isFeedback && it.isConfirmed }
39+
40+
fun hasReadAllFeedbacks(): Boolean {
41+
if (!hasFeedback()) {
42+
return true
43+
}
44+
45+
return comments.filter { it.isFeedback && it.isConfirmed }
46+
.all { it.isRead }
47+
}
48+
49+
fun markFeedbackAsRead() {
50+
comments.forEach { it.markAsRead() }
51+
}
52+
53+
fun clearTemporaryFeedbackComments() {
54+
comments.removeIf { it.isFeedback && it.isTemporary }
55+
}
56+
57+
fun clearTemporaryAnswers() {
58+
comments.filter { it.isQuestion && it.answer != null }
59+
.forEach { question ->
60+
question.answer?.temporaryContent = null
61+
}
62+
}
3663
}

src/main/kotlin/goodspace/bllsoneshot/entity/assignment/Task.kt

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,16 @@ class Task(
4444
@OneToMany(mappedBy = "task", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
4545
val proofShots: MutableList<ProofShot> = mutableListOf()
4646

47-
@OneToMany(mappedBy = "task", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
48-
val comments: MutableList<Comment> = mutableListOf()
49-
50-
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST, CascadeType.REMOVE], orphanRemoval = true)
47+
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
5148
var generalComment: GeneralComment? = null
5249

5350
@Column(nullable = false)
5451
var completed: Boolean = false
5552

53+
@get:Transient
54+
val questions: List<Comment>
55+
get() = proofShots.flatMap { it.questComments }
56+
5657
var actualMinutes: Int? = null
5758
set(value) {
5859
if (value != null && value < MINIMUM_ACTUAL_MINUTES) {
@@ -69,19 +70,13 @@ class Task(
6970
proofShots.isNotEmpty()
7071

7172
fun hasFeedback(): Boolean =
72-
comments.any { it.isFeedback && it.isConfirmed }
73-
74-
fun hasReadAllFeedbacks(): Boolean {
75-
if (!hasFeedback()) {
76-
return true
77-
}
73+
proofShots.any { it.hasFeedback() }
7874

79-
return comments.filter { it.isFeedback && it.isConfirmed }
80-
.all { it.isRead }
81-
}
75+
fun hasReadAllFeedbacks() =
76+
proofShots.all { it.hasReadAllFeedbacks() }
8277

8378
fun markFeedbackAsRead() {
84-
comments.forEach { it.markAsRead() }
79+
proofShots.forEach { it.markFeedbackAsRead() }
8580
}
8681

8782
// TODO: 로직 이해하기
@@ -92,7 +87,14 @@ class Task(
9287
proofShots.forEach { ps ->
9388
ps.comments.removeIf { it.isFeedback }
9489
}
95-
comments.removeIf { it.isFeedback }
90+
}
91+
92+
fun clearTemporaryFeedbackComments() {
93+
proofShots.forEach { it.clearTemporaryFeedbackComments() }
94+
}
95+
96+
fun clearTemporaryAnswers() {
97+
proofShots.forEach { it.clearTemporaryAnswers() }
9698
}
9799

98100
companion object {

src/main/kotlin/goodspace/bllsoneshot/mentor/controller/MentorTaskController.kt

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,42 @@ class MentorTaskController(
2828
summary = "멘토 할 일 상세 조회",
2929
description = """
3030
멘토가 멘티의 할 일을 상세 조회합니다.
31-
인증 사진, 멘티의 질문, 멘토의 피드백(임시저장 포함)을 모두 반환합니다.
31+
인증 사진, 멘티의 질문, 멘토의 최종 저장된 피드백을 반환합니다.
32+
임시저장된 피드백을 조회하려면 GET /{taskId}/feedback/temporary API를 사용하세요.
3233
3334
응답 필드:
34-
generalComment: 멘토의 총평 (null이면 아직 작성하지 않음)
35-
hasProofShot: 학생 인증 사진 제출 여부
36-
proofShots.feedbacks: 임시저장(TEMPORARY) + 확정저장(CONFIRMED) 피드백 모두 포함
35+
generalComment: 멘토의 최종 저장된 총평 (아직 작성하지 않았을 경우 null)
36+
proofShots.feedbacks: 최종 저장된(CONFIRMED) 피드백만 포함 (임시저장 제외)
3737
"""
3838
)
3939
fun getTaskDetail(
4040
@PathVariable taskId: Long,
4141
principal: Principal
4242
): ResponseEntity<MentorTaskDetailResponse> {
4343
val result = mentorTaskService.getTaskForFeedback(principal.userId, taskId)
44+
45+
return ResponseEntity.ok(result)
46+
}
47+
48+
@GetMapping("/{taskId}/feedback/temporary")
49+
@Operation(
50+
summary = "멘토 피드백 임시저장 조회",
51+
description = """
52+
멘토가 임시저장한 피드백을 조회합니다.
53+
TEMPORARY 상태의 총평(temporaryContent) 및 피드백만 반환됩니다.
54+
최종 저장된(CONFIRMED) 피드백은 포함되지 않습니다.
55+
56+
응답 필드:
57+
generalComment: 임시저장된 총평 (temporaryContent)
58+
proofShots.feedbacks: TEMPORARY 상태의 피드백만 포함
59+
"""
60+
)
61+
fun getTemporaryFeedback(
62+
@PathVariable taskId: Long,
63+
principal: Principal
64+
): ResponseEntity<MentorTaskDetailResponse> {
65+
val result = mentorTaskService.getTemporaryFeedback(principal.userId, taskId)
66+
4467
return ResponseEntity.ok(result)
4568
}
4669

@@ -50,10 +73,12 @@ class MentorTaskController(
5073
description = """
5174
멘토가 작성 중인 피드백을 임시저장합니다.
5275
프론트에서 자동 저장(debounce) 시 이 API를 호출해 주시면 됩니다.
76+
임시저장은 기존 최종 저장된(CONFIRMED) 피드백에 영향을 주지 않습니다.
5377
5478
임시저장은 검증이 느슨합니다:
5579
- 총평이 비어 있어도 됩니다.
5680
- 상세 피드백 내용이 비어 있어도 됩니다.
81+
- 질문 답변이 비어 있어도 됩니다.
5782
- 총평 최대 200자 제한만 적용됩니다.
5883
5984
요청 필드:
@@ -67,6 +92,7 @@ class MentorTaskController(
6792
principal: Principal
6893
): ResponseEntity<Void> {
6994
mentorTaskService.saveTemporary(principal.userId, taskId, request)
95+
7096
return NO_CONTENT
7197
}
7298

@@ -77,9 +103,15 @@ class MentorTaskController(
77103
멘토가 피드백을 최종 저장합니다.
78104
멘티에게 피드백이 공개되며, 이후 수정은 이 API를 다시 호출합니다.
79105
106+
최종 저장 시:
107+
- 기존 최종 저장된(CONFIRMED) 피드백과 임시저장된(TEMPORARY) 피드백이 모두 제거됩니다.
108+
- 새로운 피드백이 CONFIRMED 상태로 저장됩니다.
109+
- 모든 임시저장된 답변(temporaryContent)이 제거되고, 새로운 답변이 content에 저장됩니다.
110+
80111
최종 저장은 검증이 엄격합니다:
81112
- 총평은 필수이며 최대 200자입니다.
82113
- 상세 피드백 내용이 비어 있으면 안 됩니다.
114+
- 질문 답변 내용이 비어 있으면 안 됩니다.
83115
84116
요청 필드:
85117
generalComment: 멘토의 총평 (필수, 최대 200자)
@@ -92,6 +124,7 @@ class MentorTaskController(
92124
principal: Principal
93125
): ResponseEntity<Void> {
94126
mentorTaskService.saveFeedback(principal.userId, taskId, request)
127+
95128
return NO_CONTENT
96129
}
97130

@@ -111,6 +144,7 @@ class MentorTaskController(
111144
principal: Principal
112145
): ResponseEntity<Void> {
113146
mentorTaskService.deleteFeedback(principal.userId, taskId)
147+
114148
return NO_CONTENT
115149
}
116150

@@ -167,6 +201,7 @@ class MentorTaskController(
167201
principal: Principal
168202
): ResponseEntity<Void> {
169203
mentorTaskService.deleteTask(principal.userId, taskId)
204+
170205
return NO_CONTENT
171206
}
172207
}

src/main/kotlin/goodspace/bllsoneshot/mentor/dto/request/MentorFeedbackRequest.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package goodspace.bllsoneshot.mentor.dto.request
22

33
data class MentorFeedbackRequest(
44
val generalComment: String?,
5-
val proofShotFeedbacks: List<ProofShotFeedbackRequest> = emptyList()
5+
val proofShotFeedbacks: List<ProofShotFeedbackRequest> = emptyList(),
6+
val questionAnswers: List<QuestionAnswerRequest> = emptyList()
67
)
78

89
data class ProofShotFeedbackRequest(
@@ -16,3 +17,8 @@ data class DetailFeedbackRequest(
1617
val percentX: Double,
1718
val percentY: Double
1819
)
20+
21+
data class QuestionAnswerRequest(
22+
val questionId: Long,
23+
val content: String
24+
)

src/main/kotlin/goodspace/bllsoneshot/mentor/dto/response/MentorTaskDetailResponse.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ data class MentorTaskDetailResponse(
1010
val menteeName: String,
1111

1212
val generalComment: String?,
13-
val hasProofShot: Boolean,
14-
val hasFeedback: Boolean,
1513

1614
val proofShots: List<ProofShotResponse>
1715
)

src/main/kotlin/goodspace/bllsoneshot/mentor/mapper/MentorTaskMapper.kt

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,54 @@ class MentorTaskMapper(
3838
subject = task.subject,
3939
menteeName = task.mentee.name,
4040
generalComment = task.generalComment?.content,
41-
hasProofShot = task.hasProofShot(),
42-
hasFeedback = task.hasFeedback(),
4341
proofShots = task.proofShots.map { mapProofShot(it) }
4442
)
4543
}
4644

45+
/**
46+
* 임시저장 피드백 조회용 매핑.
47+
* TEMPORARY 상태의 총평(temporaryContent) 및 피드백만 포함합니다.
48+
*/
49+
fun mapToTemporaryDetail(task: Task): MentorTaskDetailResponse {
50+
return MentorTaskDetailResponse(
51+
taskId = task.id!!,
52+
taskName = task.name,
53+
subject = task.subject,
54+
menteeName = task.mentee.name,
55+
generalComment = task.generalComment?.temporaryContent,
56+
proofShots = task.proofShots.map { mapTemporaryProofShot(it) }
57+
)
58+
}
59+
4760
/**
4861
* 멘토 화면용 ProofShot 매핑.
49-
* 멘티 화면과 달리 임시저장(TEMPORARY) 피드백도 포함합니다.
62+
* 최종 저장된(CONFIRMED) 피드백만 포함합니다.
5063
*/
5164
private fun mapProofShot(proofShot: ProofShot): ProofShotResponse {
5265
val questions = proofShot.questComments.sortedBy { it.annotation.number }
53-
val feedbacks = proofShot.allFeedbackComments.sortedBy { it.annotation.number }
66+
val feedbacks = proofShot.confirmedFeedbackComments.sortedBy { it.annotation.number }
67+
68+
return ProofShotResponse(
69+
proofShotId = proofShot.id!!,
70+
imageFileId = proofShot.file.id!!,
71+
questions = questions.map { questionMapper.mapConfirmed(it) },
72+
feedbacks = feedbacks.map { feedbackMapper.map(it) }
73+
)
74+
}
75+
76+
/**
77+
* 임시저장 피드백용 ProofShot 매핑.
78+
* TEMPORARY 상태의 피드백만 포함합니다.
79+
* 질문 답변은 임시저장된 답변(temporaryContent)을 반환합니다.
80+
*/
81+
private fun mapTemporaryProofShot(proofShot: ProofShot): ProofShotResponse {
82+
val questions = proofShot.questComments.sortedBy { it.annotation.number }
83+
val feedbacks = proofShot.temporaryFeedbackComments.sortedBy { it.annotation.number }
5484

5585
return ProofShotResponse(
5686
proofShotId = proofShot.id!!,
5787
imageFileId = proofShot.file.id!!,
58-
questions = questions.map { questionMapper.map(it) },
88+
questions = questions.map { questionMapper.mapTemporary(it) },
5989
feedbacks = feedbacks.map { feedbackMapper.map(it) }
6090
)
6191
}

0 commit comments

Comments
 (0)