Skip to content

Commit 5c16f07

Browse files
committed
2 parents 049b4a4 + d31a640 commit 5c16f07

File tree

14 files changed

+787
-254
lines changed

14 files changed

+787
-254
lines changed

docs/api-specification.yaml

Lines changed: 400 additions & 241 deletions
Large diffs are not rendered by default.

src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
1414
* @Valid 검증 실패 → 400
1515
* throw IllegalArgumentException("메시지") → 400
1616
* throw NoSuchElementException("메시지") → 404
17+
* throw IllegalStateException("메시지") → 409
1718
* 기타 모든 예외 → 500
1819
*/
1920
@ControllerAdvice
@@ -41,6 +42,12 @@ class GlobalExceptionHandler {
4142
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse(ex.message ?: "데이터를 찾을 수 없습니다"))
4243
}
4344

45+
@ExceptionHandler(IllegalStateException::class)
46+
fun handleIllegalState(ex: IllegalStateException): ResponseEntity<ApiResponse<Void>> {
47+
logger.warn("부적절한 상태: {}", ex.message)
48+
return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse(ex.message ?: "요청을 처리할 수 없는 상태입니다"))
49+
}
50+
4451
@ExceptionHandler(Exception::class)
4552
fun handleGenericException(
4653
ex: Exception,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.back.koreaTravelGuide.common.security
2+
3+
import org.springframework.security.core.Authentication
4+
5+
fun Authentication.getUserId(): Long {
6+
if (principal is Long) {
7+
return principal as Long
8+
}
9+
throw IllegalStateException("인증된 사용자 ID를 찾을 수 없거나 타입이 올바르지 않습니다.")
10+
}

src/main/kotlin/com/back/koreaTravelGuide/domain/auth/controller/AuthController.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.koreaTravelGuide.domain.auth.controller
22

33
import com.back.koreaTravelGuide.common.ApiResponse
4+
import com.back.koreaTravelGuide.common.security.getUserId
45
import com.back.koreaTravelGuide.domain.auth.dto.request.UserRoleUpdateRequest
56
import com.back.koreaTravelGuide.domain.auth.dto.response.AccessTokenResponse
67
import com.back.koreaTravelGuide.domain.auth.dto.response.LoginResponse
@@ -10,7 +11,7 @@ import jakarta.servlet.http.HttpServletRequest
1011
import jakarta.servlet.http.HttpServletResponse
1112
import org.springframework.beans.factory.annotation.Value
1213
import org.springframework.http.ResponseEntity
13-
import org.springframework.security.core.annotation.AuthenticationPrincipal
14+
import org.springframework.security.core.Authentication
1415
import org.springframework.web.bind.annotation.CookieValue
1516
import org.springframework.web.bind.annotation.PostMapping
1617
import org.springframework.web.bind.annotation.RequestBody
@@ -45,9 +46,10 @@ class AuthController(
4546
@Operation(summary = "신규 사용자 역할 선택")
4647
@PostMapping("/role")
4748
fun updateUserRole(
48-
@AuthenticationPrincipal userId: Long,
49+
authentication: Authentication,
4950
@RequestBody request: UserRoleUpdateRequest,
5051
): ResponseEntity<ApiResponse<LoginResponse>> {
52+
val userId = authentication.getUserId()
5153
val loginResponse = authService.updateRoleAndLogin(userId, request.role)
5254
return ResponseEntity.ok(ApiResponse("역할이 선택되었으며 로그인에 성공했습니다.", loginResponse))
5355
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.back.koreaTravelGuide.domain.rate.controller
2+
3+
import com.back.koreaTravelGuide.common.ApiResponse
4+
import com.back.koreaTravelGuide.common.security.getUserId
5+
import com.back.koreaTravelGuide.domain.rate.dto.GuideRatingSummaryResponse
6+
import com.back.koreaTravelGuide.domain.rate.dto.RateRequest
7+
import com.back.koreaTravelGuide.domain.rate.dto.RateResponse
8+
import com.back.koreaTravelGuide.domain.rate.service.RateService
9+
import io.swagger.v3.oas.annotations.Operation
10+
import org.springframework.data.domain.Page
11+
import org.springframework.data.domain.Pageable
12+
import org.springframework.http.ResponseEntity
13+
import org.springframework.security.access.prepost.PreAuthorize
14+
import org.springframework.security.core.Authentication
15+
import org.springframework.web.bind.annotation.GetMapping
16+
import org.springframework.web.bind.annotation.PathVariable
17+
import org.springframework.web.bind.annotation.PutMapping
18+
import org.springframework.web.bind.annotation.RequestBody
19+
import org.springframework.web.bind.annotation.RequestMapping
20+
import org.springframework.web.bind.annotation.RestController
21+
22+
@RestController
23+
@RequestMapping("/api/rate")
24+
class RateController(
25+
private val rateService: RateService,
26+
) {
27+
@Operation(summary = "가이드 평가 생성/수정")
28+
@PutMapping("/guides/{guideId}")
29+
fun rateGuide(
30+
authentication: Authentication,
31+
@PathVariable guideId: Long,
32+
@RequestBody request: RateRequest,
33+
): ResponseEntity<ApiResponse<RateResponse>> {
34+
val raterUserId = authentication.getUserId()
35+
val rate = rateService.rateGuide(raterUserId, guideId, request.rating, request.comment)
36+
return ResponseEntity.ok(ApiResponse("가이드 평가가 등록되었습니다.", RateResponse.from(rate)))
37+
}
38+
39+
@Operation(summary = "AI 채팅 세션 평가 생성/수정")
40+
@PutMapping("/aichat/sessions/{sessionId}")
41+
fun rateAiSession(
42+
authentication: Authentication,
43+
@PathVariable sessionId: Long,
44+
@RequestBody request: RateRequest,
45+
): ResponseEntity<ApiResponse<RateResponse>> {
46+
val raterUserId = authentication.getUserId()
47+
val rate = rateService.rateAiSession(raterUserId, sessionId, request.rating, request.comment)
48+
return ResponseEntity.ok(ApiResponse("AI 채팅 평가가 등록되었습니다.", RateResponse.from(rate)))
49+
}
50+
51+
@Operation(summary = "내가 받은 가이드 평가 조회")
52+
@GetMapping("/guides/my")
53+
@PreAuthorize("hasRole('GUIDE')")
54+
fun getMyGuideRatings(authentication: Authentication): ResponseEntity<ApiResponse<GuideRatingSummaryResponse>> {
55+
val guideId = authentication.getUserId()
56+
val summary = rateService.getMyGuideRatingSummary(guideId)
57+
return ResponseEntity.ok(ApiResponse("내 가이드 평점 정보를 조회했습니다.", summary))
58+
}
59+
60+
@Operation(summary = "관리자의 모든 AI 채팅 평가 조회")
61+
@GetMapping("/admin/aichat/sessions")
62+
@PreAuthorize("hasRole('ADMIN')")
63+
fun getAllAiSessionRatings(pageable: Pageable): ResponseEntity<ApiResponse<Page<RateResponse>>> {
64+
val ratingsPage = rateService.getAllAiSessionRatingsForAdmin(pageable)
65+
return ResponseEntity.ok(ApiResponse("모든 AI 채팅 평가 목록을 조회했습니다.", ratingsPage))
66+
}
67+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.back.koreaTravelGuide.domain.rate.dto
2+
3+
data class GuideRatingSummaryResponse(
4+
val averageRating: Double,
5+
val totalRatings: Int,
6+
val ratings: List<RateResponse>,
7+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.back.koreaTravelGuide.domain.rate.dto
2+
3+
import jakarta.validation.constraints.Max
4+
import jakarta.validation.constraints.Min
5+
6+
data class RateRequest(
7+
@field:Min(1, message = "평점은 1 이상이어야 합니다.")
8+
@field:Max(5, message = "평점은 5 이하여야 합니다.")
9+
val rating: Int,
10+
val comment: String?,
11+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.back.koreaTravelGuide.domain.rate.dto
2+
3+
import com.back.koreaTravelGuide.domain.rate.entity.Rate
4+
import java.time.ZonedDateTime
5+
6+
data class RateResponse(
7+
val id: Long,
8+
val raterNickname: String,
9+
val rating: Int,
10+
val comment: String?,
11+
val createdAt: ZonedDateTime,
12+
) {
13+
companion object {
14+
fun from(rate: Rate): RateResponse {
15+
return RateResponse(
16+
id = rate.id!!,
17+
raterNickname = rate.user.nickname,
18+
rating = rate.rating,
19+
comment = rate.comment,
20+
createdAt = rate.createdAt,
21+
)
22+
}
23+
}
24+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.back.koreaTravelGuide.domain.rate.entity
2+
3+
import com.back.koreaTravelGuide.domain.user.entity.User
4+
import jakarta.persistence.Column
5+
import jakarta.persistence.Entity
6+
import jakarta.persistence.EnumType
7+
import jakarta.persistence.Enumerated
8+
import jakarta.persistence.GeneratedValue
9+
import jakarta.persistence.GenerationType
10+
import jakarta.persistence.Id
11+
import jakarta.persistence.JoinColumn
12+
import jakarta.persistence.ManyToOne
13+
import jakarta.persistence.Table
14+
import jakarta.validation.constraints.Max
15+
import jakarta.validation.constraints.Min
16+
import java.time.ZoneId
17+
import java.time.ZonedDateTime
18+
19+
@Entity
20+
@Table(name = "rates")
21+
class Rate(
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
val id: Long? = null,
25+
@ManyToOne
26+
@JoinColumn(name = "user_id", nullable = false)
27+
val user: User,
28+
@Enumerated(EnumType.STRING)
29+
@Column(nullable = false)
30+
val targetType: RateTargetType,
31+
@Column(nullable = false)
32+
val targetId: Long,
33+
@Min(1)
34+
@Max(5)
35+
@Column(nullable = false)
36+
var rating: Int,
37+
@Column(columnDefinition = "Text")
38+
var comment: String?,
39+
@Column(nullable = false, updatable = false)
40+
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
41+
@Column(nullable = false)
42+
var updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
43+
) {
44+
fun update(
45+
rating: Int,
46+
comment: String?,
47+
) {
48+
this.rating = rating
49+
this.comment = comment
50+
this.updatedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul"))
51+
}
52+
}
53+
54+
// /bigint id PK "Auto Increment"
55+
// bigint user_id FK "평가자 ID (GUEST만)"
56+
// enum target_type "AI_CHAT_SESSION, GUIDE"
57+
// bigint target_id "평가 대상 ID (세션 ID 또는 가이드 ID)"
58+
// int rating "평점 1-5"
59+
// text comment "평가 코멘트 (선택사항)"
60+
// datetime created_at "평가 일시"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.back.koreaTravelGuide.domain.rate.entity
2+
3+
enum class RateTargetType {
4+
AI_SESSION,
5+
GUIDE,
6+
}

0 commit comments

Comments
 (0)