Skip to content

Commit b050e80

Browse files
authored
Merge pull request #103 from billilge/develop
release: 1차 스프린트 배포
2 parents 4ad9300 + 2855f60 commit b050e80

File tree

20 files changed

+390
-32
lines changed

20 files changed

+390
-32
lines changed

.github/workflows/ci-cd_dev.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Development Server CI/CD with Gradle and Docker
2+
3+
on:
4+
push:
5+
branches: [ "develop" ]
6+
pull_request:
7+
branches: [ "develop" ]
8+
9+
jobs:
10+
build-docker-image:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up JDK 17
17+
uses: actions/setup-java@v4
18+
with:
19+
java-version: '17'
20+
distribution: 'temurin'
21+
22+
- name: Create Firebase Config Directory
23+
run: |
24+
mkdir -p src/main/resources/firebase
25+
echo "${{ secrets.FIREBASE_SERVICE_KEY }}" | base64 --decode > src/main/resources/firebase/firebaseServiceKey.json
26+
shell: bash
27+
28+
- name: Setup Gradle
29+
uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
30+
31+
- name: Build with Gradle Wrapper
32+
run: ./gradlew clean build -x test
33+
34+
# Docker 관련 작업들은 push 시에만 실행되도록 설정
35+
- name: docker image build
36+
if: github.event_name == 'push'
37+
run: docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest .
38+
39+
# DockerHub 로그인
40+
- name: docker login
41+
if: github.event_name == 'push'
42+
uses: docker/login-action@v3
43+
with:
44+
username: ${{ secrets.DOCKER_USERNAME }}
45+
password: ${{ secrets.DOCKER_PASSWORD }}
46+
47+
# Docker Hub 이미지 푸시
48+
- name: docker Hub push
49+
if: github.event_name == 'push'
50+
run: docker push ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest
51+
52+
run-docker-image-on-ec2:
53+
needs: build-docker-image
54+
#push 했을 때만 배포가 진행되도록
55+
if: github.event_name == 'push'
56+
runs-on: self-hosted
57+
steps:
58+
- name: docker pull
59+
run: sudo docker pull ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest
60+
61+
- name: docker stop container
62+
run: sudo docker stop springboot || true
63+
64+
- name: docker run new container
65+
run: sudo docker run --env-file /home/ubuntu/billilge.env --name springboot --rm -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest
66+
67+
- name: delete old docker image
68+
run: sudo docker image prune -f
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI/CD with Gradle and Docker
1+
name: Production Server CI/CD with Gradle and Docker
22

33
on:
44
push:

Dockerfile-dev

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Dockerfile
2+
3+
# jdk17 Image Start
4+
FROM openjdk:17
5+
6+
# jar 파일 복제
7+
COPY build/libs/*.jar app.jar
8+
9+
# 실행 명령어
10+
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=dev", "-jar", "app.jar"]

src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationApi.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,19 @@ interface NotificationApi {
6262
fun getNotificationCount(
6363
@AuthenticationPrincipal userAuthInfo: UserAuthInfo
6464
): ResponseEntity<NotificationCountResponse>
65+
66+
@Operation(
67+
summary = "알림 모두 읽음 처리",
68+
description = "사용자가 알림을 모두 읽음 처리합니다."
69+
)
70+
@ApiResponses(
71+
value = [
72+
ApiResponse(responseCode = "200", description = "알림 모두 읽음 처리 완료"),
73+
ApiResponse(responseCode = "403", description = "권한이 없는 사용자"),
74+
ApiResponse(responseCode = "404", description = "알림을 찾을 수 없음")
75+
]
76+
)
77+
fun readAllNotifications(
78+
@AuthenticationPrincipal userAuthInfo: UserAuthInfo
79+
): ResponseEntity<Void>
6580
}

src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationController.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,13 @@ class NotificationController (
3939
notificationService.readNotification(memberId, notificationId)
4040
return ResponseEntity.ok().build()
4141
}
42+
43+
@PatchMapping("/all")
44+
override fun readAllNotifications(
45+
@AuthenticationPrincipal userAuthInfo: UserAuthInfo
46+
): ResponseEntity<Void> {
47+
val memberId = userAuthInfo.memberId
48+
notificationService.readAllNotifications(memberId)
49+
return ResponseEntity.ok().build()
50+
}
4251
}

src/main/kotlin/site/billilge/api/backend/domain/notification/entity/Notification.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import java.time.LocalDateTime
1111
@Table(name = "notifications")
1212
@EntityListeners(AuditingEntityListener::class)
1313
class Notification(
14-
@JoinColumn(name = "member_id", nullable = false)
14+
@JoinColumn(name = "member_id", nullable = true)
1515
@ManyToOne
16-
val member: Member,
16+
val member: Member? = null,
1717

1818
@Enumerated(EnumType.STRING)
1919
@Column(name = "type", nullable = false)

src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ class NotificationService(
3030

3131
return NotificationFindAllResponse(
3232
notifications
33-
.map { NotificationDetail.from(it) })
33+
.map { NotificationDetail.from(it) }
34+
)
3435
}
3536

3637
@Transactional
3738
fun readNotification(memberId: Long?, notificationId: Long) {
3839
val notification = notificationRepository.findById(notificationId)
3940
.orElseThrow { ApiException(NotificationErrorCode.NOTIFICATION_NOT_FOUND) }
4041

41-
if (notification.member.id != memberId) {
42+
if (notification.isAdminStatus()) return;
43+
44+
if (notification.member?.id != memberId) {
4245
throw ApiException(NotificationErrorCode.NOTIFICATION_ACCESS_DENIED)
4346
}
4447

@@ -58,7 +61,7 @@ class NotificationService(
5861
member: Member,
5962
status: NotificationStatus,
6063
formatValues: List<String>,
61-
needPush: Boolean = false
64+
needPush: Boolean = false,
6265
) {
6366
val notification = Notification(
6467
member = member,
@@ -69,32 +72,50 @@ class NotificationService(
6972
notificationRepository.save(notification)
7073

7174
if (needPush) {
72-
val studentId = member.studentId
75+
sendPushNotification(member, status, formatValues)
76+
}
77+
}
7378

74-
if (member.fcmToken == null) {
75-
log.warn { "(studentId=${studentId}) FCM Token is null" }
76-
return
77-
}
79+
private fun sendPushNotification(
80+
member: Member,
81+
status: NotificationStatus,
82+
formatValues: List<String>,
83+
) {
84+
val studentId = member.studentId
7885

79-
fcmService.sendPushNotification(
80-
member.fcmToken!!,
81-
status.title,
82-
status.formattedMessage(*formatValues.toTypedArray()),
83-
status.link,
84-
studentId
85-
)
86+
if (member.fcmToken == null) {
87+
log.warn { "(studentId=${studentId}) FCM Token is null" }
88+
return
8689
}
90+
91+
fcmService.sendPushNotification(
92+
member.fcmToken!!,
93+
status.title,
94+
status.formattedMessage(*formatValues.toTypedArray()),
95+
status.link,
96+
studentId
97+
)
8798
}
8899

100+
@Transactional
89101
fun sendNotificationToAdmin(
90102
type: NotificationStatus,
91103
formatValues: List<String>,
92104
needPush: Boolean = false
93105
) {
94106
val admins = memberRepository.findAllByRole(Role.ADMIN)
95107

96-
admins.forEach { admin ->
97-
sendNotification(admin, type, formatValues, needPush)
108+
val notification = Notification(
109+
status = type,
110+
formatValues = formatValues.joinToString(",")
111+
)
112+
113+
notificationRepository.save(notification)
114+
115+
if (needPush) {
116+
admins.forEach { admin ->
117+
sendPushNotification(admin, type, formatValues)
118+
}
98119
}
99120
}
100121

@@ -103,4 +124,13 @@ class NotificationService(
103124

104125
return NotificationCountResponse(count)
105126
}
127+
128+
@Transactional
129+
fun readAllNotifications(memberId: Long?) {
130+
notificationRepository
131+
.findAllUserNotificationsByMemberId(memberId!!)
132+
.forEach { it.readNotification() }
133+
}
134+
135+
private fun Notification.isAdminStatus(): Boolean = status.name.contains("ADMIN", true)
106136
}

src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerApi.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package site.billilge.api.backend.domain.payer.controller
22

33
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.media.Content
5+
import io.swagger.v3.oas.annotations.media.Schema
46
import io.swagger.v3.oas.annotations.responses.ApiResponse
57
import io.swagger.v3.oas.annotations.responses.ApiResponses
68
import io.swagger.v3.oas.annotations.tags.Tag
9+
import org.springframework.core.io.InputStreamResource
710
import org.springframework.http.ResponseEntity
811
import org.springframework.web.bind.annotation.RequestBody
912
import org.springframework.web.bind.annotation.RequestParam
@@ -62,4 +65,22 @@ interface AdminPayerApi {
6265
]
6366
)
6467
fun deletePayers(@RequestBody request: PayerDeleteRequest): ResponseEntity<Void>
68+
69+
@Operation(
70+
summary = "학생회비 납부자 Excel 파일 다운로드",
71+
description = "학생회비 납부자 데이터를 엑셀 파일로 다운받을 수 있는 관리자용 API"
72+
)
73+
@ApiResponses(
74+
value = [
75+
ApiResponse(
76+
responseCode = "200",
77+
description = "파일 다운로드 성공",
78+
content = [Content(
79+
mediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
80+
schema = Schema(type = "string", format = "binary")
81+
)]
82+
)
83+
]
84+
)
85+
fun createPayerExcel(): ResponseEntity<InputStreamResource>
6586
}

src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package site.billilge.api.backend.domain.payer.controller
22

3+
import org.springframework.core.io.InputStreamResource
4+
import org.springframework.http.ContentDisposition
5+
import org.springframework.http.HttpHeaders
6+
import org.springframework.http.MediaType
37
import org.springframework.http.ResponseEntity
48
import org.springframework.web.bind.annotation.*
59
import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest
@@ -9,6 +13,8 @@ import site.billilge.api.backend.domain.payer.service.PayerService
913
import site.billilge.api.backend.global.annotation.OnlyAdmin
1014
import site.billilge.api.backend.global.dto.PageableCondition
1115
import site.billilge.api.backend.global.dto.SearchCondition
16+
import java.time.LocalDate
17+
import java.time.format.DateTimeFormatter
1218

1319
@RestController
1420
@RequestMapping("/admin/members/payers")
@@ -35,4 +41,22 @@ class AdminPayerController(
3541
payerService.deletePayers(request)
3642
return ResponseEntity.noContent().build()
3743
}
44+
45+
@GetMapping("/excel")
46+
override fun createPayerExcel(): ResponseEntity<InputStreamResource> {
47+
val excel = payerService.createPayerExcel()
48+
val currentDate = LocalDate.now()
49+
val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd")
50+
val headers = HttpHeaders().apply {
51+
contentDisposition = ContentDisposition.builder("attachment")
52+
.filename("kmusw_payers_${dateFormatter.format(currentDate)}.xlsx")
53+
.build()
54+
}
55+
val excelMediaType = MediaType.valueOf("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
56+
57+
return ResponseEntity.ok()
58+
.headers(headers)
59+
.contentType(excelMediaType)
60+
.body(InputStreamResource((excel)))
61+
}
3862
}

src/main/kotlin/site/billilge/api/backend/domain/payer/repository/PayerRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ interface PayerRepository : JpaRepository<Payer, Long?> {
1515

1616
@Query("SELECT p FROM Payer p WHERE p.name LIKE CONCAT('%', :name, '%')")
1717
fun findAllByNameContaining(@Param("name") name: String, pageable: Pageable): Page<Payer>
18+
19+
fun findAllByEnrollmentYear(enrollmentYear: String): List<Payer>
1820
}

0 commit comments

Comments
 (0)