Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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: 6 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ jobs:
java-version: '17'
distribution: 'temurin'

- name: Create Firebase Config Directory
run: |
mkdir -p src/main/resources/firebase
echo "${{ secrets.FIREBASE_SERVICE_KEY }}" | base64 --decode > src/main/resources/firebase/firebaseServiceKey.json
shell: bash

- name: Setup Gradle
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ out/

application-local.yml

.DS_Store
.DS_Store
/src/main/resources/firebase
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {

implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")

implementation("com.google.firebase:firebase-admin:9.4.3")

compileOnly("org.projectlombok:lombok")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package site.billilge.api.backend.domain.item.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import site.billilge.api.backend.domain.item.dto.response.ItemFindAllResponse
import site.billilge.api.backend.global.dto.SearchCondition

@Tag(name = "Item", description = "대여 물품 조회 API")
interface ItemApi {

@Operation(
summary = "대여 물품 목록 조회",
description = "검색어에 따라 물품 이름에 포함된 단어를 기반으로 대여 물품 목록을 조회합니다."
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "대여 물품 목록 조회 성공"
)
]
)
@GetMapping
fun getItems(
@ModelAttribute searchCondition: SearchCondition
): ResponseEntity<ItemFindAllResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package site.billilge.api.backend.domain.item.controller

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import site.billilge.api.backend.domain.item.dto.response.ItemFindAllResponse
import site.billilge.api.backend.domain.item.service.ItemService
import site.billilge.api.backend.global.dto.SearchCondition

@RestController
@RequestMapping("/items")
class ItemController(
private val itemService: ItemService
) : ItemApi {
@GetMapping
override fun getItems(
@ModelAttribute searchCondition: SearchCondition
): ResponseEntity<ItemFindAllResponse> {
val response = itemService.searchItems(searchCondition)
return ResponseEntity.ok(response)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package site.billilge.api.backend.domain.item.dto.response
import io.swagger.v3.oas.annotations.media.Schema
import site.billilge.api.backend.domain.item.entity.Item
import site.billilge.api.backend.domain.item.enums.ItemType
import site.billilge.api.backend.domain.item.exception.ItemErrorCode
import site.billilge.api.backend.global.exception.ApiException

@Schema
data class ItemDetail(
Expand All @@ -23,8 +21,7 @@ data class ItemDetail(
@JvmStatic
fun from(item: Item): ItemDetail {
return ItemDetail(
itemId = item.id
?: throw ApiException(ItemErrorCode.ITEM_ID_IS_NULL),
itemId = item.id!!,
itemName = item.name,
itemType = item.type,
count = item.count,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package site.billilge.api.backend.domain.item.repository

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import site.billilge.api.backend.domain.item.entity.Item
import java.util.*

interface ItemRepository: JpaRepository<Item, Long>, ItemRepositoryCustom {
override fun findById(id: Long): Optional<Item>
fun existsByName(name: String): Boolean

@Query("SELECT i FROM Item i WHERE i.name LIKE CONCAT('%', :search, '%')")
fun findByItemName(@Param("search") search: String): List<Item>
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,10 @@ class ItemService(
if (image.contentType != "image/svg+xml")
throw ApiException(ItemErrorCode.IMAGE_IS_NOT_SVG)
}

fun searchItems(searchCondition: SearchCondition): ItemFindAllResponse {
val items = itemRepository.findByItemName(searchCondition.search)

return ItemFindAllResponse(items.map { ItemDetail.from(it) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package site.billilge.api.backend.domain.member.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.RequestBody
import site.billilge.api.backend.domain.member.dto.request.MemberFCMTokenRequest
import site.billilge.api.backend.global.security.oauth2.UserAuthInfo

@Tag(name = "Member", description = "회원 API")
interface MemberApi {
@Operation(
summary = "FCM 토큰 전송",
description = "서버 측으로 회원 기기의 FCM 토큰을 전송하는 API"
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "FCM 토큰 전송 성공"
)
]
)
fun setFCMToken(
@AuthenticationPrincipal userAuthInfo: UserAuthInfo,
@RequestBody request: MemberFCMTokenRequest
): ResponseEntity<Void>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package site.billilge.api.backend.domain.member.controller

import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import site.billilge.api.backend.domain.member.dto.request.MemberFCMTokenRequest
import site.billilge.api.backend.domain.member.service.MemberService
import site.billilge.api.backend.global.security.oauth2.UserAuthInfo

@RestController
@RequestMapping("/members")
class MemberController(
private val memberService: MemberService,
) : MemberApi {
@PostMapping("/me/fcm-token")
override fun setFCMToken(
@AuthenticationPrincipal userAuthInfo: UserAuthInfo,
@RequestBody request: MemberFCMTokenRequest
): ResponseEntity<Void> {
memberService.setMemberFCMToken(userAuthInfo.memberId, request)
return ResponseEntity.ok().build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package site.billilge.api.backend.domain.member.dto.request

import io.swagger.v3.oas.annotations.media.Schema

@Schema
data class MemberFCMTokenRequest(
@field:Schema(description = "회원의 디바이스 FCM 토큰")
val token: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class Member(
var role: Role = Role.USER
protected set

@Column(name = "fcm_token")
var fcmToken: String? = null
protected set

@CreatedDate
@Column(name = "created_at", nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now()
Expand All @@ -57,4 +61,8 @@ class Member(
fun updateEmail(email: String) {
this.email = email
}

fun updateFCMToken(fcmToken: String) {
this.fcmToken = fcmToken
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ interface MemberRepository: JpaRepository<Member, Long> {

@Query("select m from Member m where m.studentId in :studentIds")
fun findAllByStudentIds(@Param("studentIds") studentIds: List<String>): List<Member>

fun findAllByRole(role: Role): List<Member>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package site.billilge.api.backend.domain.member.service

import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import site.billilge.api.backend.domain.member.dto.request.AdminRequest
import site.billilge.api.backend.domain.member.dto.request.MemberFCMTokenRequest
import site.billilge.api.backend.domain.member.dto.request.SignUpRequest
import site.billilge.api.backend.domain.member.dto.response.*
import site.billilge.api.backend.domain.member.exception.MemberErrorCode
Expand Down Expand Up @@ -118,4 +120,12 @@ class MemberService(

return MemberFindAllResponse(memberDetails.toList(), members.totalPages)
}

@Transactional
fun setMemberFCMToken(memberId: Long?, request: MemberFCMTokenRequest) {
val member = (memberRepository.findByIdOrNull(memberId!!)
?: throw ApiException(MemberErrorCode.MEMBER_NOT_FOUND))

member.updateFCMToken(request.token)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package site.billilge.api.backend.domain.notification.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import site.billilge.api.backend.domain.notification.dto.response.NotificationFindAllResponse
import site.billilge.api.backend.global.security.oauth2.UserAuthInfo

@Tag(name = "(Admin) Notification", description = "관리자용 알림 API")
interface AdminNotificationApi {
@Operation(
summary = "관리자 알림 목록 조회",
description = "로그인한 사용자의 관리자 알림 목록을 최신순으로 조회합니다."
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "관리자 알림 목록 조회 성공"
)
]
)
fun getAdminNotifications(@AuthenticationPrincipal userAuthInfo: UserAuthInfo): ResponseEntity<NotificationFindAllResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package site.billilge.api.backend.domain.notification.controller

import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import site.billilge.api.backend.domain.notification.dto.response.NotificationFindAllResponse
import site.billilge.api.backend.domain.notification.service.NotificationService
import site.billilge.api.backend.global.security.oauth2.UserAuthInfo

@RestController
@RequestMapping("/admin/notifications")
class AdminNotificationController(
private val notificationService: NotificationService
) : AdminNotificationApi {
@GetMapping
override fun getAdminNotifications(@AuthenticationPrincipal userAuthInfo: UserAuthInfo): ResponseEntity<NotificationFindAllResponse> {
return ResponseEntity.ok(notificationService.getAdminNotifications(userAuthInfo.memberId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package site.billilge.api.backend.domain.notification.controller

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PathVariable
import site.billilge.api.backend.domain.notification.dto.response.NotificationCountResponse
import site.billilge.api.backend.domain.notification.dto.response.NotificationFindAllResponse
import site.billilge.api.backend.global.security.oauth2.UserAuthInfo

@Tag(name = "Notification", description = "알림 API")
interface NotificationApi {

@Operation(
summary = "사용자의 알림 목록 조회",
description = "로그인한 사용자의 알림 목록을 최신순으로 조회합니다."
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "알림 목록 조회 성공"
)
]
)
fun getNotifications(
@AuthenticationPrincipal userAuthInfo: UserAuthInfo
): ResponseEntity<NotificationFindAllResponse>


@Operation(
summary = "알림 읽음 처리",
description = "사용자가 특정 알림을 읽음 처리합니다."
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "알림 읽음 처리 완료"),
ApiResponse(responseCode = "403", description = "권한이 없는 사용자"),
ApiResponse(responseCode = "404", description = "알림을 찾을 수 없음")
]
)
fun readNotification(
@AuthenticationPrincipal userAuthInfo: UserAuthInfo,
@PathVariable notificationId: Long
): ResponseEntity<Void>

@Operation(
summary = "알림 개수 조회",
description = "로그인한 사용자의 알림 개수를 조회합니다. (메인 페이지)"
)
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "개수 조회 성공"
)
]
)
fun getNotificationCount(
@AuthenticationPrincipal userAuthInfo: UserAuthInfo
): ResponseEntity<NotificationCountResponse>
}
Loading