Skip to content

Commit ef6f5c3

Browse files
CEO-Nickskydreamer21keongmini
authored
fcm device token 중복 저장 수정 (#164)
#163 ### Proposed Changes - FcmDevice table에 (fcm device, memberId) unique key 등록 - 디바이스 정보 저장 시, 중복 저장 방지 로직 추가 ### Code Review Point (리뷰어가 중점적으로 보면 좋을 부분을 적어주세요.) --------- Co-authored-by: JuHyun Kim <95271588+skydreamer21@users.noreply.github.com> Co-authored-by: keongmini <kmnam09@gmail.com> Co-authored-by: skydreamer <skydreamer210@gmail.com>
1 parent 5cb25e0 commit ef6f5c3

File tree

20 files changed

+482
-3
lines changed

20 files changed

+482
-3
lines changed

src/main/kotlin/com/ssak3/timeattack/common/exception/ApplicationExceptionType.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ enum class ApplicationExceptionType(
117117

118118
// ======================== [END] TASK ========================
119119

120+
// ======================== [START] SUBTASK ========================
121+
122+
/**
123+
* - {0} : Subtask ID
124+
*/
125+
SUBTASK_NOT_FOUND_BY_ID(
126+
HttpStatus.BAD_REQUEST,
127+
"ERR_SUBTASK_001",
128+
"해당 ID로 Subtask를 찾을 수 없습니다. : {0}",
129+
),
130+
131+
// ======================== [END] SUBTASK ========================
132+
120133
/**
121134
* - {0} : BindException 에러 메시지
122135
*/

src/main/kotlin/com/ssak3/timeattack/notifications/repository/FcmDeviceRepositoryCustom.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ import com.ssak3.timeattack.notifications.repository.entity.FcmDeviceEntity
44

55
interface FcmDeviceRepositoryCustom {
66
fun findActiveByMember(memberId: Long): List<FcmDeviceEntity>
7+
8+
fun findActiveByMemberAndFcmToken(
9+
memberId: Long,
10+
fcmToken: String,
11+
): FcmDeviceEntity?
712
}

src/main/kotlin/com/ssak3/timeattack/notifications/repository/FcmDeviceRepositoryCustomImpl.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ class FcmDeviceRepositoryCustomImpl(
1818
fcmDevice.status.isTrue,
1919
).fetch()
2020
}
21+
22+
override fun findActiveByMemberAndFcmToken(
23+
memberId: Long,
24+
fcmToken: String,
25+
): FcmDeviceEntity? {
26+
return queryFactory.selectFrom(fcmDevice)
27+
.where(
28+
fcmDevice.member.id.eq(memberId),
29+
fcmDevice.fcmRegistrationToken.eq(fcmToken),
30+
fcmDevice.status.isTrue,
31+
).fetchOne()
32+
}
2133
}

src/main/kotlin/com/ssak3/timeattack/notifications/repository/entity/FcmDeviceEntity.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@ import jakarta.persistence.Id
1515
import jakarta.persistence.JoinColumn
1616
import jakarta.persistence.ManyToOne
1717
import jakarta.persistence.Table
18+
import jakarta.persistence.UniqueConstraint
1819

1920
@Entity
20-
@Table(name = "fcm_devices")
21+
@Table(
22+
name = "fcm_devices",
23+
uniqueConstraints = [
24+
UniqueConstraint(
25+
name = "uk_member_token",
26+
columnNames = ["member_id", "fcm_registration_token"],
27+
),
28+
],
29+
)
2130
class FcmDeviceEntity(
2231
@Id
2332
@GeneratedValue(strategy = GenerationType.IDENTITY)

src/main/kotlin/com/ssak3/timeattack/notifications/service/FcmDeviceService.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.ssak3.timeattack.notifications.service
22

3+
import com.ssak3.timeattack.common.utils.checkNotNull
34
import com.ssak3.timeattack.notifications.domain.FcmDevice
45
import com.ssak3.timeattack.notifications.repository.FcmDeviceRepository
56
import org.springframework.stereotype.Service
@@ -11,7 +12,15 @@ class FcmDeviceService(
1112
) {
1213
@Transactional
1314
fun save(fcmDevice: FcmDevice) {
14-
fcmDeviceRepository.save(fcmDevice.toEntity())
15+
val memberId = checkNotNull(fcmDevice.member.id)
16+
17+
// 해당 유저의 기기가 이미 등록되어 있으면 등록하지 않음
18+
fcmDeviceRepository.findActiveByMemberAndFcmToken(
19+
memberId = memberId,
20+
fcmToken = fcmDevice.fcmRegistrationToken,
21+
)
22+
?.run { return }
23+
?: fcmDeviceRepository.save(fcmDevice.toEntity())
1524
}
1625

1726
fun getDevicesByMember(memberId: Long): List<FcmDevice> =
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.ssak3.timeattack.task.controller
2+
3+
import com.ssak3.timeattack.common.config.SwaggerConfig.Companion.SECURITY_SCHEME_NAME
4+
import com.ssak3.timeattack.member.domain.Member
5+
import com.ssak3.timeattack.task.controller.dto.ImmersionResponse
6+
import com.ssak3.timeattack.task.service.ImmersionTaskService
7+
import io.swagger.v3.oas.annotations.Operation
8+
import io.swagger.v3.oas.annotations.security.SecurityRequirement
9+
import org.springframework.http.ResponseEntity
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal
11+
import org.springframework.web.bind.annotation.GetMapping
12+
import org.springframework.web.bind.annotation.RequestMapping
13+
import org.springframework.web.bind.annotation.RestController
14+
15+
@RestController
16+
@RequestMapping("/v1/immersion-tasks")
17+
class ImmersionTaskController(
18+
private val immersionTaskService: ImmersionTaskService,
19+
) {
20+
@Operation(summary = "몰입 중인 작업 목록 호출", security = [SecurityRequirement(name = SECURITY_SCHEME_NAME)])
21+
@GetMapping("/all")
22+
fun getImmersionTasks(
23+
@AuthenticationPrincipal member: Member,
24+
): ResponseEntity<ImmersionResponse> {
25+
val immersionTasks = immersionTaskService.getImmersionTasks(member)
26+
val response = ImmersionResponse(immersionTasks = immersionTasks)
27+
return ResponseEntity.ok(response)
28+
}
29+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.ssak3.timeattack.task.controller
2+
3+
import com.ssak3.timeattack.common.config.SwaggerConfig.Companion.SECURITY_SCHEME_NAME
4+
import com.ssak3.timeattack.task.controller.dto.SubTaskResponse
5+
import com.ssak3.timeattack.task.controller.dto.SubtaskUpsertRequest
6+
import com.ssak3.timeattack.task.service.SubtaskService
7+
import io.swagger.v3.oas.annotations.Operation
8+
import io.swagger.v3.oas.annotations.security.SecurityRequirement
9+
import jakarta.validation.Valid
10+
import jakarta.validation.constraints.Positive
11+
import org.springframework.http.ResponseEntity
12+
import org.springframework.web.bind.annotation.DeleteMapping
13+
import org.springframework.web.bind.annotation.PatchMapping
14+
import org.springframework.web.bind.annotation.PathVariable
15+
import org.springframework.web.bind.annotation.PostMapping
16+
import org.springframework.web.bind.annotation.RequestBody
17+
import org.springframework.web.bind.annotation.RequestMapping
18+
import org.springframework.web.bind.annotation.RestController
19+
20+
@RestController
21+
@RequestMapping("/v1/subtasks")
22+
class SubtaskController(
23+
private val subtaskService: SubtaskService,
24+
) {
25+
@Operation(summary = "세부목표 생성/수정", security = [SecurityRequirement(name = SECURITY_SCHEME_NAME)])
26+
@PostMapping
27+
fun upsert(
28+
@RequestBody @Valid request: SubtaskUpsertRequest,
29+
): ResponseEntity<SubTaskResponse> {
30+
val upsertedSubtask = subtaskService.upsert(request)
31+
return ResponseEntity.ok(SubTaskResponse.fromSubtask(upsertedSubtask))
32+
}
33+
34+
@Operation(summary = "세부작업 삭제", security = [SecurityRequirement(name = SECURITY_SCHEME_NAME)])
35+
@DeleteMapping("/{id}")
36+
fun remove(
37+
@PathVariable(required = true) @Positive id: Long,
38+
): ResponseEntity<Void> {
39+
subtaskService.delete(id)
40+
return ResponseEntity.noContent().build()
41+
}
42+
43+
@Operation(summary = "세부작업 완료 상태 업데이트", security = [SecurityRequirement(name = SECURITY_SCHEME_NAME)])
44+
@PatchMapping("/{id}")
45+
fun updateStatus(
46+
@PathVariable(required = true) @Positive id: Long,
47+
): ResponseEntity<SubTaskResponse> {
48+
val updatedSubtask = subtaskService.updateStatus(id)
49+
return ResponseEntity.ok(SubTaskResponse.fromSubtask(updatedSubtask))
50+
}
51+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.ssak3.timeattack.task.controller.dto
2+
3+
import com.ssak3.timeattack.task.domain.ImmersionTask
4+
5+
data class ImmersionResponse(
6+
val immersionTasks: List<ImmersionTask>,
7+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.ssak3.timeattack.task.controller.dto
2+
3+
import com.ssak3.timeattack.common.utils.checkNotNull
4+
import com.ssak3.timeattack.task.domain.Subtask
5+
6+
data class SubTaskResponse(
7+
val id: Long,
8+
val taskId: Long,
9+
val name: String,
10+
val isCompleted: Boolean,
11+
) {
12+
companion object {
13+
fun fromSubtask(subtask: Subtask) =
14+
SubTaskResponse(
15+
id = subtask.id,
16+
taskId = checkNotNull(subtask.task.id, "task id"),
17+
name = subtask.name,
18+
isCompleted = subtask.isCompleted,
19+
)
20+
}
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ssak3.timeattack.task.controller.dto
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode
5+
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED
6+
import jakarta.validation.constraints.Positive
7+
import jakarta.validation.constraints.Size
8+
9+
@Schema(description = "세부목표 생성/수정 요청 ")
10+
data class SubtaskUpsertRequest(
11+
@Schema(
12+
title = "세부목표 id",
13+
description = "세부목표 수정시 필요",
14+
example = "1",
15+
requiredMode = RequiredMode.NOT_REQUIRED,
16+
)
17+
@field:Positive
18+
val id: Long = 0,
19+
@Schema(
20+
title = "작업 id",
21+
description = "세부목표가 해당하는 작업의 id",
22+
example = "1",
23+
requiredMode = REQUIRED,
24+
)
25+
@field:Positive
26+
val taskId: Long,
27+
@Schema(
28+
title = "세부목표 이름",
29+
description = "1이상 40자 이하",
30+
example = "created/updated subtask name",
31+
requiredMode = RequiredMode.REQUIRED,
32+
)
33+
@field:Size(min = 1, max = 40)
34+
val name: String,
35+
)

0 commit comments

Comments
 (0)