From d55651e07d241890c2cc7f9cf0b636f6bfd6ff76 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:42:49 +0900 Subject: [PATCH 01/12] =?UTF-8?q?refactor(#87):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/entity/Notification.kt | 4 +- .../service/NotificationService.kt | 57 +++++++++++++------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/entity/Notification.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/entity/Notification.kt index 7415bcf..1b4fb3d 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/entity/Notification.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/entity/Notification.kt @@ -11,9 +11,9 @@ import java.time.LocalDateTime @Table(name = "notifications") @EntityListeners(AuditingEntityListener::class) class Notification( - @JoinColumn(name = "member_id", nullable = false) + @JoinColumn(name = "member_id", nullable = true) @ManyToOne - val member: Member, + val member: Member? = null, @Enumerated(EnumType.STRING) @Column(name = "type", nullable = false) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt index 854cdc5..ff9b66d 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt @@ -30,7 +30,8 @@ class NotificationService( return NotificationFindAllResponse( notifications - .map { NotificationDetail.from(it) }) + .map { NotificationDetail.from(it) } + ) } @Transactional @@ -38,7 +39,9 @@ class NotificationService( val notification = notificationRepository.findById(notificationId) .orElseThrow { ApiException(NotificationErrorCode.NOTIFICATION_NOT_FOUND) } - if (notification.member.id != memberId) { + if (notification.isAdminStatus()) return; + + if (notification.member?.id != memberId) { throw ApiException(NotificationErrorCode.NOTIFICATION_ACCESS_DENIED) } @@ -58,7 +61,7 @@ class NotificationService( member: Member, status: NotificationStatus, formatValues: List, - needPush: Boolean = false + needPush: Boolean = false, ) { val notification = Notification( member = member, @@ -69,23 +72,32 @@ class NotificationService( notificationRepository.save(notification) if (needPush) { - val studentId = member.studentId + sendPushNotification(member, status, formatValues) + } + } - if (member.fcmToken == null) { - log.warn { "(studentId=${studentId}) FCM Token is null" } - return - } + private fun sendPushNotification( + member: Member, + status: NotificationStatus, + formatValues: List, + ) { + val studentId = member.studentId - fcmService.sendPushNotification( - member.fcmToken!!, - status.title, - status.formattedMessage(*formatValues.toTypedArray()), - status.link, - studentId - ) + if (member.fcmToken == null) { + log.warn { "(studentId=${studentId}) FCM Token is null" } + return } + + fcmService.sendPushNotification( + member.fcmToken!!, + status.title, + status.formattedMessage(*formatValues.toTypedArray()), + status.link, + studentId + ) } + @Transactional fun sendNotificationToAdmin( type: NotificationStatus, formatValues: List, @@ -93,8 +105,17 @@ class NotificationService( ) { val admins = memberRepository.findAllByRole(Role.ADMIN) - admins.forEach { admin -> - sendNotification(admin, type, formatValues, needPush) + val notification = Notification( + status = type, + formatValues = formatValues.joinToString(",") + ) + + notificationRepository.save(notification) + + if (needPush) { + admins.forEach { admin -> + sendPushNotification(admin, type, formatValues) + } } } @@ -103,4 +124,6 @@ class NotificationService( return NotificationCountResponse(count) } + + private fun Notification.isAdminStatus(): Boolean = status.name.contains("ADMIN", true) } \ No newline at end of file From eb2e6a93321fa81200875cd0bc43d38ab96ff32c Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Sun, 18 May 2025 14:45:49 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat(#92):=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/controller/NotificationApi.kt | 15 +++++++++++++++ .../controller/NotificationController.kt | 9 +++++++++ .../notification/service/NotificationService.kt | 7 +++++++ 3 files changed, 31 insertions(+) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationApi.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationApi.kt index 1da6455..1bb3d1a 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationApi.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationApi.kt @@ -62,4 +62,19 @@ interface NotificationApi { fun getNotificationCount( @AuthenticationPrincipal userAuthInfo: UserAuthInfo ): ResponseEntity + + @Operation( + summary = "알림 모두 읽음 처리", + description = "사용자가 알림을 모두 읽음 처리합니다." + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "알림 모두 읽음 처리 완료"), + ApiResponse(responseCode = "403", description = "권한이 없는 사용자"), + ApiResponse(responseCode = "404", description = "알림을 찾을 수 없음") + ] + ) + fun readAllNotifications( + @AuthenticationPrincipal userAuthInfo: UserAuthInfo + ): ResponseEntity } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationController.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationController.kt index 5d8c1ec..6b1074d 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/controller/NotificationController.kt @@ -39,4 +39,13 @@ class NotificationController ( notificationService.readNotification(memberId, notificationId) return ResponseEntity.ok().build() } + + @PatchMapping("/all") + override fun readAllNotifications( + @AuthenticationPrincipal userAuthInfo: UserAuthInfo + ): ResponseEntity { + val memberId = userAuthInfo.memberId + notificationService.readAllNotifications(memberId) + return ResponseEntity.ok().build() + } } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt b/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt index ff9b66d..e3f9e80 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/notification/service/NotificationService.kt @@ -125,5 +125,12 @@ class NotificationService( return NotificationCountResponse(count) } + @Transactional + fun readAllNotifications(memberId: Long?) { + notificationRepository + .findAllUserNotificationsByMemberId(memberId!!) + .forEach { it.readNotification() } + } + private fun Notification.isAdminStatus(): Boolean = status.name.contains("ADMIN", true) } \ No newline at end of file From 3ae13c37973df156c33420e6828c1d7080285d21 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Sun, 18 May 2025 14:54:54 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refactor(#95):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20API=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=EA=B0=84=EC=9D=84=20=EB=8C=80?= =?UTF-8?q?=EC=97=AC=EC=8B=9C=EA=B0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/rental/dto/response/DashboardResponse.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/rental/dto/response/DashboardResponse.kt b/src/main/kotlin/site/billilge/api/backend/domain/rental/dto/response/DashboardResponse.kt index 2d9bc91..77ddb41 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/rental/dto/response/DashboardResponse.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/rental/dto/response/DashboardResponse.kt @@ -28,8 +28,8 @@ data class DashboardResponse( val renterStudentId: String, @field:Schema(description = "대여 상태", example = "PENDING") val status: RentalStatus, - @field:Schema(description = "신청일") - val applicatedAt: LocalDateTime + @field:Schema(description = "대여 시각") + val rentAt: LocalDateTime?, ) { companion object { @JvmStatic @@ -42,7 +42,7 @@ data class DashboardResponse( renterName = rentalHistory.member.name, renterStudentId = rentalHistory.member.studentId, status = rentalHistory.rentalStatus, - applicatedAt = rentalHistory.applicatedAt + rentAt = rentalHistory.rentAt ) } } From a7446e3b39ab0859c46252d82399f03c13305917 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Tue, 20 May 2025 12:13:17 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=95=A1=EC=85=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1=20util=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payer/controller/AdminPayerController.kt | 6 ++ .../dto/response/PayerExcelFileResponse.kt | 8 +++ .../backend/domain/payer/event/PayerEvents.kt | 7 +++ .../event/listener/PayerEventListener.kt | 21 +++++++ .../domain/payer/service/PayerService.kt | 25 +++++++- .../api/backend/global/config/AsyncConfig.kt | 9 +++ .../backend/global/utils/ExcelGenerator.kt | 63 +++++++++++++++++++ .../api/backend/global/utils/ExcelRow.kt | 7 +++ 8 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt create mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt create mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt create mode 100644 src/main/kotlin/site/billilge/api/backend/global/config/AsyncConfig.kt create mode 100644 src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt create mode 100644 src/main/kotlin/site/billilge/api/backend/global/utils/ExcelRow.kt diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt index ed74ab8..d6564cc 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt @@ -4,6 +4,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest import site.billilge.api.backend.domain.payer.dto.request.PayerRequest +import site.billilge.api.backend.domain.payer.dto.response.PayerExcelFileResponse import site.billilge.api.backend.domain.payer.dto.response.PayerFindAllResponse import site.billilge.api.backend.domain.payer.service.PayerService import site.billilge.api.backend.global.annotation.OnlyAdmin @@ -35,4 +36,9 @@ class AdminPayerController( payerService.deletePayers(request) return ResponseEntity.noContent().build() } + + @GetMapping("/excel") + fun getExcelFileUrl(): ResponseEntity { + return ResponseEntity.ok(payerService.getExcelFileUrl()); + } } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt new file mode 100644 index 0000000..8fbf182 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt @@ -0,0 +1,8 @@ +package site.billilge.api.backend.domain.payer.dto.response + +import io.swagger.v3.oas.annotations.media.Schema + +data class PayerExcelFileResponse( + @field:Schema(description = "S3에 업로드된 납부자 목록 엑셀 파일 URL") + val fileUrl: String +) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt new file mode 100644 index 0000000..b259d32 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt @@ -0,0 +1,7 @@ +package site.billilge.api.backend.domain.payer.event + +sealed class PayerEvent(open val payerIds: List) + +data class PayerAddEvent(override val payerIds: List) : PayerEvent(payerIds) + +data class PayerDeleteEvent(override val payerIds: List) : PayerEvent(payerIds) \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt new file mode 100644 index 0000000..65119d2 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt @@ -0,0 +1,21 @@ +package site.billilge.api.backend.domain.payer.event.listener + +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener +import site.billilge.api.backend.domain.payer.event.PayerAddEvent +import site.billilge.api.backend.domain.payer.event.PayerDeleteEvent + +@Component +class PayerEventListener { + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun onPayerAdd(event: PayerAddEvent) { + + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun onPayerDelete(event: PayerDeleteEvent) { + + } +} \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt index df1e9d3..e53854e 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt @@ -1,5 +1,7 @@ package site.billilge.api.backend.domain.payer.service +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.stereotype.Service @@ -8,18 +10,25 @@ import site.billilge.api.backend.domain.member.entity.Member import site.billilge.api.backend.domain.member.repository.MemberRepository import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest import site.billilge.api.backend.domain.payer.dto.request.PayerRequest +import site.billilge.api.backend.domain.payer.dto.response.PayerExcelFileResponse import site.billilge.api.backend.domain.payer.dto.response.PayerFindAllResponse import site.billilge.api.backend.domain.payer.dto.response.PayerSummary import site.billilge.api.backend.domain.payer.entity.Payer +import site.billilge.api.backend.domain.payer.event.PayerAddEvent +import site.billilge.api.backend.domain.payer.event.PayerDeleteEvent import site.billilge.api.backend.domain.payer.repository.PayerRepository import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition +import java.time.LocalDate @Service @Transactional(readOnly = true) class PayerService( + private val publisher: ApplicationEventPublisher, private val payerRepository: PayerRepository, - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, + @Value("\${cloud.aws.s3.base-url}") + private val s3BaseUrl: String, ) { fun isPayer(name: String, studentId: String): Boolean { val enrollmentYear = studentId.substring(0, 4) @@ -71,6 +80,7 @@ class PayerService( @Transactional fun addPayers(request: PayerRequest) { + val newPayers = mutableListOf() request.payers.forEach { payerItem -> val name = payerItem.name val studentId = payerItem.studentId @@ -87,11 +97,15 @@ class PayerService( this.registered = registered } - payerRepository.save(payer) + newPayers.add(payer) } registeredMember?.isFeePaid = true } + + payerRepository.saveAll(newPayers) + + publisher.publishEvent(PayerAddEvent(newPayers.map { it.id })) } @Transactional @@ -106,5 +120,12 @@ class PayerService( } payerRepository.deleteAllById(request.payerIds) + + publisher.publishEvent(PayerDeleteEvent(request.payerIds)) + } + + fun getExcelFileUrl(): PayerExcelFileResponse { + val currentYear = LocalDate.now().year + return PayerExcelFileResponse(s3BaseUrl + "/payers/payer_${currentYear}.xlsx") } } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/global/config/AsyncConfig.kt b/src/main/kotlin/site/billilge/api/backend/global/config/AsyncConfig.kt new file mode 100644 index 0000000..71b0bd9 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/global/config/AsyncConfig.kt @@ -0,0 +1,9 @@ +package site.billilge.api.backend.global.config + +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableAsync + +@EnableAsync +@Configuration +class AsyncConfig { +} \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt new file mode 100644 index 0000000..c3469fd --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt @@ -0,0 +1,63 @@ +package site.billilge.api.backend.global.utils + +import org.apache.poi.ss.usermodel.IndexedColors +import org.apache.poi.xssf.streaming.SXSSFSheet +import org.apache.poi.xssf.streaming.SXSSFWorkbook +import org.springframework.stereotype.Component +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +private const val HEADER_ROW = 0 + +@Component +class ExcelGenerator { + + fun generateByMultipleSheets( + sheetData: Map, List>> + ): ByteArrayInputStream { + val workbook = SXSSFWorkbook() + + sheetData.forEach { (sheetName, sheetContent) -> + val (headerTitles, rows) = sheetContent + val sheet = workbook.createSheet(sheetName) + styleHeaders(workbook, sheet, headerTitles) + fillData(sheet, rows, headerTitles.size) + } + + val out = ByteArrayOutputStream() + workbook.write(out) + workbook.close() + + return ByteArrayInputStream(out.toByteArray()) + } + + private fun styleHeaders(workbook: SXSSFWorkbook, sheet: SXSSFSheet, headerTitles: Array) { + val headerFont = workbook.createFont() + headerFont.bold = true + + val headerCellStyle = workbook.createCellStyle() + headerCellStyle.setFont(headerFont) + headerCellStyle.fillForegroundColor = IndexedColors.GREY_25_PERCENT.index + headerCellStyle.fillPattern = org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND + + val headerRow = sheet.createRow(HEADER_ROW) + headerTitles.forEachIndexed { col, title -> + val cell = headerRow.createCell(col) + cell.setCellValue(title) + cell.cellStyle = headerCellStyle + } + } + + private fun fillData(sheet: SXSSFSheet, rows: List, columnSize: Int) { + sheet.trackAllColumnsForAutoSizing() + + rows.forEachIndexed { index, excelRow -> + val row = sheet.createRow(index + 1) + excelRow.data.forEachIndexed { propertyIndex, property -> + row.createCell(propertyIndex).setCellValue(property) + } + } + + repeat(columnSize) { col -> sheet.autoSizeColumn(col) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelRow.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelRow.kt new file mode 100644 index 0000000..c717b82 --- /dev/null +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelRow.kt @@ -0,0 +1,7 @@ +package site.billilge.api.backend.global.utils + +data class ExcelRow( + val data: List +) { + constructor(vararg data: String) : this(data.toList()) +} \ No newline at end of file From 1d45d1b1de01c1aa91dc3a2b4549c26a5ee5bbe9 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Tue, 20 May 2025 13:39:56 +0900 Subject: [PATCH 05/12] =?UTF-8?q?deploy(#97):=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd_dev.yml | 68 ++++++++++++++++++ .../workflows/{ci-cd.yml => ci-cd_prod.yml} | 2 +- Dockerfile-dev | 10 +++ src/main/resources/application-dev.yml | 70 +++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-cd_dev.yml rename .github/workflows/{ci-cd.yml => ci-cd_prod.yml} (97%) create mode 100644 Dockerfile-dev create mode 100644 src/main/resources/application-dev.yml diff --git a/.github/workflows/ci-cd_dev.yml b/.github/workflows/ci-cd_dev.yml new file mode 100644 index 0000000..5b32541 --- /dev/null +++ b/.github/workflows/ci-cd_dev.yml @@ -0,0 +1,68 @@ +name: Development Server CI/CD with Gradle and Docker + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build-docker-image: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + 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 + + - name: Build with Gradle Wrapper + run: ./gradlew clean build -x test + + # Docker 관련 작업들은 push 시에만 실행되도록 설정 + - name: docker image build + if: github.event_name == 'push' + run: docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest . + + # DockerHub 로그인 + - name: docker login + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Docker Hub 이미지 푸시 + - name: docker Hub push + if: github.event_name == 'push' + run: docker push ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest + + run-docker-image-on-ec2: + needs: build-docker-image + #push 했을 때만 배포가 진행되도록 + if: github.event_name == 'push' + runs-on: self-hosted + steps: + - name: docker pull + run: sudo docker pull ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest + + - name: docker stop container + run: sudo docker stop springboot || true + + - name: docker run new container + run: sudo docker run --env-file /home/ubuntu/billilge.env --name springboot --rm -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/billilge-dev:latest + + - name: delete old docker image + run: sudo docker image prune -f \ No newline at end of file diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd_prod.yml similarity index 97% rename from .github/workflows/ci-cd.yml rename to .github/workflows/ci-cd_prod.yml index 41854b3..ee3bae9 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd_prod.yml @@ -1,4 +1,4 @@ -name: CI/CD with Gradle and Docker +name: Production Server CI/CD with Gradle and Docker on: push: diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 0000000..3721652 --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,10 @@ +# Dockerfile + +# jdk17 Image Start +FROM openjdk:17 + +# jar 파일 복제 +COPY build/libs/*.jar app.jar + +# 실행 명령어 +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=dev", "-jar", "app.jar"] \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..dbdc010 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,70 @@ +spring: + application: + name: backend + + datasource: + url: jdbc:mysql://${DEV_DB_URL}?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.cj.jdbc.Driver + username: ${DEV_DB_USERNAME} + password: ${DEV_DB_PASSWORD} + + security: + oauth2: + client: + registration: + # 구글 로그인 추가 + google: + client-id: ${DEV_OAUTH2_GOOGLE_CLIENT_ID} + client-secret: ${DEV_OAUTH2_GOOGLE_CLIENT_SECRET} + scope: + - email + - profile + jpa: + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + dialect.storage_engine: innodb + show-sql: true + hibernate: + ddl-auto: update + + servlet: + multipart: + max-file-size: 5MB + max-request-size: 10MB + resolve-lazily: true + + +jwt: + secret_key: ${DEV_JWT_SECRET_KEY} + issuer: ${DEV_JWT_ISSUER} + +login: + admin-password: ${DEV_LOGIN_ADMIN_PASSWORD} + redirect: + url: ${DEV_LOGIN_REDIRECT_URL} + +cors: + allowed-origins: ${DEV_CORS_ALLOWED_ORIGINS} + +exam-period: + start-date: ${DEV_EXAM_PERIOD_START_DATE} + end-date: ${DEV_EXAM_PERIOD_END_DATE} + +cloud: + aws: + s3: + bucket: ${DEV_S3_BUCKET} + base-url: ${DEV_S3_BASE_URL} + credentials: + access-key: ${DEV_S3_ACCESS_KEY} + secret-key: ${DEV_S3_SECRET_KEY} + region: + static: us-west-2 + stack: + auto: false + +swagger: + server: + base-url: ${DEV_SWAGGER_SERVER_BASE_URL} \ No newline at end of file From d7953cd21e752ed4e32fc1ff9fefeb76c0ec5428 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Wed, 21 May 2025 17:17:41 +0900 Subject: [PATCH 06/12] =?UTF-8?q?HOTFIX:=20redirect-url=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/oauth2/OAuth2AuthenticationSuccessHandler.kt | 2 +- src/main/resources/application-dev.yml | 4 ++-- src/main/resources/application-prod.yml | 3 +-- src/main/resources/application.yml | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/global/security/oauth2/OAuth2AuthenticationSuccessHandler.kt b/src/main/kotlin/site/billilge/api/backend/global/security/oauth2/OAuth2AuthenticationSuccessHandler.kt index 4f1dcb5..1c99f35 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/security/oauth2/OAuth2AuthenticationSuccessHandler.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/security/oauth2/OAuth2AuthenticationSuccessHandler.kt @@ -23,7 +23,7 @@ class OAuth2AuthenticationSuccessHandler( private val memberRepository: MemberRepository, - @Value("\${login.redirect.url}") private val redirectUrl: String, + @Value("\${login.redirect-url}") private val redirectUrl: String, ) : SimpleUrlAuthenticationSuccessHandler() { override fun onAuthenticationSuccess( request: HttpServletRequest?, response: HttpServletResponse?, authentication: Authentication? diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index dbdc010..1202e12 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -16,6 +16,7 @@ spring: google: client-id: ${DEV_OAUTH2_GOOGLE_CLIENT_ID} client-secret: ${DEV_OAUTH2_GOOGLE_CLIENT_SECRET} + redirect-uri: ${DEV_SWAGGER_SERVER_BASE_URL}/login/oauth2/code/google scope: - email - profile @@ -42,8 +43,7 @@ jwt: login: admin-password: ${DEV_LOGIN_ADMIN_PASSWORD} - redirect: - url: ${DEV_LOGIN_REDIRECT_URL} + redirect-url: ${DEV_LOGIN_REDIRECT_URL} cors: allowed-origins: ${DEV_CORS_ALLOWED_ORIGINS} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index fa273d8..6a207c6 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -42,8 +42,7 @@ jwt: login: admin-password: ${LOGIN_ADMIN_PASSWORD} - redirect: - url: ${LOGIN_REDIRECT_URL} + redirect-url: ${LOGIN_REDIRECT_URL} cors: allowed-origins: ${CORS_ALLOWED_ORIGINS} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ea5aa73..8c3d881 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,4 +15,4 @@ springdoc: show-actuator: true # Spring Actuator의 endpoint까지 보여줄 것인지? default-consumes-media-type: application/json # request media type 의 기본 값 default-produces-media-type: application/json # response media type 의 기본 값 - paths-to-match: /** # 해당 패턴에 매칭되는 controller만 swagger-ui에 노출한다. + paths-to-match: /** # 해당 패턴에 매칭되는 controller만 swagger-ui에 노출한다. \ No newline at end of file From 42cbcc3a64826cbfb2fdc2d64300233970ae0ff1 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 11:17:00 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat(#85):=20=ED=95=99=EC=83=9D=ED=9A=8C?= =?UTF-8?q?=EB=B9=84=20=EB=82=A9=EB=B6=80=EC=9E=90=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payer/controller/AdminPayerController.kt | 21 ++++++++-- .../dto/response/PayerExcelFileResponse.kt | 8 ---- .../backend/domain/payer/event/PayerEvents.kt | 7 ---- .../event/listener/PayerEventListener.kt | 21 ---------- .../payer/repository/PayerRepository.kt | 2 + .../domain/payer/service/PayerService.kt | 39 +++++++++++-------- 6 files changed, 42 insertions(+), 56 deletions(-) delete mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt delete mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt delete mode 100644 src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt index d6564cc..33aa989 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt @@ -1,15 +1,19 @@ package site.billilge.api.backend.domain.payer.controller +import org.springframework.core.io.InputStreamResource +import org.springframework.http.ContentDisposition +import org.springframework.http.HttpHeaders import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest import site.billilge.api.backend.domain.payer.dto.request.PayerRequest -import site.billilge.api.backend.domain.payer.dto.response.PayerExcelFileResponse import site.billilge.api.backend.domain.payer.dto.response.PayerFindAllResponse import site.billilge.api.backend.domain.payer.service.PayerService import site.billilge.api.backend.global.annotation.OnlyAdmin import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition +import java.time.LocalDate +import java.time.format.DateTimeFormatter @RestController @RequestMapping("/admin/members/payers") @@ -38,7 +42,18 @@ class AdminPayerController( } @GetMapping("/excel") - fun getExcelFileUrl(): ResponseEntity { - return ResponseEntity.ok(payerService.getExcelFileUrl()); + fun createPayerExcel(): ResponseEntity { + val excel = payerService.createPayerExcel() + val currentDate = LocalDate.now() + val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") + val headers = HttpHeaders().apply { + contentDisposition = ContentDisposition.builder("attachment") + .filename("kmusw_payers_${dateFormatter.format(currentDate)}.xlsx") + .build() + } + + return ResponseEntity.ok() + .headers(headers) + .body(InputStreamResource((excel))) } } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt deleted file mode 100644 index 8fbf182..0000000 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/dto/response/PayerExcelFileResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package site.billilge.api.backend.domain.payer.dto.response - -import io.swagger.v3.oas.annotations.media.Schema - -data class PayerExcelFileResponse( - @field:Schema(description = "S3에 업로드된 납부자 목록 엑셀 파일 URL") - val fileUrl: String -) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt deleted file mode 100644 index b259d32..0000000 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/PayerEvents.kt +++ /dev/null @@ -1,7 +0,0 @@ -package site.billilge.api.backend.domain.payer.event - -sealed class PayerEvent(open val payerIds: List) - -data class PayerAddEvent(override val payerIds: List) : PayerEvent(payerIds) - -data class PayerDeleteEvent(override val payerIds: List) : PayerEvent(payerIds) \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt deleted file mode 100644 index 65119d2..0000000 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/event/listener/PayerEventListener.kt +++ /dev/null @@ -1,21 +0,0 @@ -package site.billilge.api.backend.domain.payer.event.listener - -import org.springframework.stereotype.Component -import org.springframework.transaction.event.TransactionPhase -import org.springframework.transaction.event.TransactionalEventListener -import site.billilge.api.backend.domain.payer.event.PayerAddEvent -import site.billilge.api.backend.domain.payer.event.PayerDeleteEvent - -@Component -class PayerEventListener { - - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - fun onPayerAdd(event: PayerAddEvent) { - - } - - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - fun onPayerDelete(event: PayerDeleteEvent) { - - } -} \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/repository/PayerRepository.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/repository/PayerRepository.kt index a545714..ce0468a 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/repository/PayerRepository.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/repository/PayerRepository.kt @@ -15,4 +15,6 @@ interface PayerRepository : JpaRepository { @Query("SELECT p FROM Payer p WHERE p.name LIKE CONCAT('%', :name, '%')") fun findAllByNameContaining(@Param("name") name: String, pageable: Pageable): Page + + fun findAllByEnrollmentYear(enrollmentYear: String): List } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt index e53854e..4ab0b56 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt @@ -1,7 +1,5 @@ package site.billilge.api.backend.domain.payer.service -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Sort import org.springframework.stereotype.Service @@ -10,25 +8,25 @@ import site.billilge.api.backend.domain.member.entity.Member import site.billilge.api.backend.domain.member.repository.MemberRepository import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest import site.billilge.api.backend.domain.payer.dto.request.PayerRequest -import site.billilge.api.backend.domain.payer.dto.response.PayerExcelFileResponse import site.billilge.api.backend.domain.payer.dto.response.PayerFindAllResponse import site.billilge.api.backend.domain.payer.dto.response.PayerSummary import site.billilge.api.backend.domain.payer.entity.Payer -import site.billilge.api.backend.domain.payer.event.PayerAddEvent -import site.billilge.api.backend.domain.payer.event.PayerDeleteEvent import site.billilge.api.backend.domain.payer.repository.PayerRepository import site.billilge.api.backend.global.dto.PageableCondition import site.billilge.api.backend.global.dto.SearchCondition -import java.time.LocalDate +import site.billilge.api.backend.global.utils.ExcelGenerator +import site.billilge.api.backend.global.utils.ExcelRow +import java.io.ByteArrayInputStream +import java.time.Year @Service @Transactional(readOnly = true) class PayerService( - private val publisher: ApplicationEventPublisher, private val payerRepository: PayerRepository, + private val memberRepository: MemberRepository, - @Value("\${cloud.aws.s3.base-url}") - private val s3BaseUrl: String, + + private val excelGenerator: ExcelGenerator ) { fun isPayer(name: String, studentId: String): Boolean { val enrollmentYear = studentId.substring(0, 4) @@ -104,15 +102,12 @@ class PayerService( } payerRepository.saveAll(newPayers) - - publisher.publishEvent(PayerAddEvent(newPayers.map { it.id })) } @Transactional fun deletePayers(request: PayerDeleteRequest) { val payerStudentIds = payerRepository.findAllByIds(request.payerIds) .mapNotNull { it.studentId } - .toList() memberRepository.findAllByStudentIds(payerStudentIds) .forEach { member -> @@ -120,12 +115,22 @@ class PayerService( } payerRepository.deleteAllById(request.payerIds) - - publisher.publishEvent(PayerDeleteEvent(request.payerIds)) } - fun getExcelFileUrl(): PayerExcelFileResponse { - val currentYear = LocalDate.now().year - return PayerExcelFileResponse(s3BaseUrl + "/payers/payer_${currentYear}.xlsx") + fun createPayerExcel(): ByteArrayInputStream { + val startYear = 2015 + val currentYear = Year.now().value + val headerTitles = arrayOf("이름", "학번") + val sheetData = mutableMapOf, List>>() + + for (year in startYear..currentYear) { + val yearText = "$year" + val payersByYearExcelRow = payerRepository.findAllByEnrollmentYear(yearText) + .map { payer -> ExcelRow(payer.name, payer.studentId ?: "${yearText}XXXX") } + + sheetData.put(yearText, headerTitles to payersByYearExcelRow) + } + + return excelGenerator.generateByMultipleSheets(sheetData) } } \ No newline at end of file From 6f2b0831334aeddf26dd785fa46d733837df46cb Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 11:47:16 +0900 Subject: [PATCH 08/12] =?UTF-8?q?HOTFIX:=20=EB=82=A9=EB=B6=80=EC=9E=90=20?= =?UTF-8?q?=EC=97=91=EC=85=80=20=ED=8C=8C=EC=9D=BC=20=EA=B9=A8=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/payer/controller/AdminPayerApi.kt | 21 ++++++++++++++++ .../payer/controller/AdminPayerController.kt | 5 +++- .../backend/global/utils/ExcelGenerator.kt | 25 +++++++++++-------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerApi.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerApi.kt index e39599b..241db2a 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerApi.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerApi.kt @@ -1,9 +1,12 @@ package site.billilge.api.backend.domain.payer.controller import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema 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.core.io.InputStreamResource import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam @@ -62,4 +65,22 @@ interface AdminPayerApi { ] ) fun deletePayers(@RequestBody request: PayerDeleteRequest): ResponseEntity + + @Operation( + summary = "학생회비 납부자 Excel 파일 다운로드", + description = "학생회비 납부자 데이터를 엑셀 파일로 다운받을 수 있는 관리자용 API" + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "파일 다운로드 성공", + content = [Content( + mediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + schema = Schema(type = "string", format = "binary") + )] + ) + ] + ) + fun createPayerExcel(): ResponseEntity } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt index 33aa989..e4183b1 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/controller/AdminPayerController.kt @@ -3,6 +3,7 @@ package site.billilge.api.backend.domain.payer.controller import org.springframework.core.io.InputStreamResource import org.springframework.http.ContentDisposition import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import site.billilge.api.backend.domain.payer.dto.request.PayerDeleteRequest @@ -42,7 +43,7 @@ class AdminPayerController( } @GetMapping("/excel") - fun createPayerExcel(): ResponseEntity { + override fun createPayerExcel(): ResponseEntity { val excel = payerService.createPayerExcel() val currentDate = LocalDate.now() val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") @@ -51,9 +52,11 @@ class AdminPayerController( .filename("kmusw_payers_${dateFormatter.format(currentDate)}.xlsx") .build() } + val excelMediaType = MediaType.valueOf("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return ResponseEntity.ok() .headers(headers) + .contentType(excelMediaType) .body(InputStreamResource((excel))) } } \ No newline at end of file diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt index c3469fd..5d51139 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt @@ -17,18 +17,21 @@ class ExcelGenerator { ): ByteArrayInputStream { val workbook = SXSSFWorkbook() - sheetData.forEach { (sheetName, sheetContent) -> - val (headerTitles, rows) = sheetContent - val sheet = workbook.createSheet(sheetName) - styleHeaders(workbook, sheet, headerTitles) - fillData(sheet, rows, headerTitles.size) - } - - val out = ByteArrayOutputStream() - workbook.write(out) - workbook.close() + try { + sheetData.forEach { (sheetName, sheetContent) -> + val (headerTitles, rows) = sheetContent + val sheet = workbook.createSheet(sheetName) + styleHeaders(workbook, sheet, headerTitles) + fillData(sheet, rows, headerTitles.size) + } - return ByteArrayInputStream(out.toByteArray()) + val out = ByteArrayOutputStream() + workbook.write(out) + return ByteArrayInputStream(out.toByteArray()) + } finally { + workbook.dispose() + workbook.close() + } } private fun styleHeaders(workbook: SXSSFWorkbook, sheet: SXSSFSheet, headerTitles: Array) { From 1730f1a5bf8bf5e751b3cac5afb160d364dfb659 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 12:18:57 +0900 Subject: [PATCH 09/12] =?UTF-8?q?refactor:=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=ED=98=95=ED=83=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/backend/domain/payer/service/PayerService.kt | 2 +- .../site/billilge/api/backend/global/utils/ExcelGenerator.kt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt index 4ab0b56..446fb8b 100644 --- a/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt +++ b/src/main/kotlin/site/billilge/api/backend/domain/payer/service/PayerService.kt @@ -123,7 +123,7 @@ class PayerService( val headerTitles = arrayOf("이름", "학번") val sheetData = mutableMapOf, List>>() - for (year in startYear..currentYear) { + for (year in currentYear downTo startYear) { val yearText = "$year" val payersByYearExcelRow = payerRepository.findAllByEnrollmentYear(yearText) .map { payer -> ExcelRow(payer.name, payer.studentId ?: "${yearText}XXXX") } diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt index 5d51139..4aa943d 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt @@ -49,6 +49,11 @@ class ExcelGenerator { cell.setCellValue(title) cell.cellStyle = headerCellStyle } + + //이름 칸 너비 + sheet.setColumnWidth(0, 10) + //학번 칸 너비 + sheet.setColumnWidth(1, 16) } private fun fillData(sheet: SXSSFSheet, rows: List, columnSize: Int) { From 9710917374839068c5aa208fa94c8c9554333f7f Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 12:24:26 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20column=20sizing=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site/billilge/api/backend/global/utils/ExcelGenerator.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt index 4aa943d..f6e74a8 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt @@ -57,15 +57,11 @@ class ExcelGenerator { } private fun fillData(sheet: SXSSFSheet, rows: List, columnSize: Int) { - sheet.trackAllColumnsForAutoSizing() - rows.forEachIndexed { index, excelRow -> val row = sheet.createRow(index + 1) excelRow.data.forEachIndexed { propertyIndex, property -> row.createCell(propertyIndex).setCellValue(property) } } - - repeat(columnSize) { col -> sheet.autoSizeColumn(col) } } } \ No newline at end of file From dc5aefd5ccd02c880d208df4f14dd8099f16a6d7 Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 12:27:55 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20column=20=EB=84=88=EB=B9=84=20?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20256=20=EA=B3=B1=ED=95=9C=20=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../billilge/api/backend/global/utils/ExcelGenerator.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt index f6e74a8..58c9b56 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/utils/ExcelGenerator.kt @@ -22,7 +22,7 @@ class ExcelGenerator { val (headerTitles, rows) = sheetContent val sheet = workbook.createSheet(sheetName) styleHeaders(workbook, sheet, headerTitles) - fillData(sheet, rows, headerTitles.size) + fillData(sheet, rows) } val out = ByteArrayOutputStream() @@ -51,12 +51,12 @@ class ExcelGenerator { } //이름 칸 너비 - sheet.setColumnWidth(0, 10) + sheet.setColumnWidth(0, 10 * 256) //학번 칸 너비 - sheet.setColumnWidth(1, 16) + sheet.setColumnWidth(1, 16 * 256) } - private fun fillData(sheet: SXSSFSheet, rows: List, columnSize: Int) { + private fun fillData(sheet: SXSSFSheet, rows: List) { rows.forEachIndexed { index, excelRow -> val row = sheet.createRow(index + 1) excelRow.data.forEachIndexed { propertyIndex, property -> From 67ee3cf8e76c77b976fbb9318db68a4cbb18329b Mon Sep 17 00:00:00 2001 From: Sumin Hwang <163857590+tnals0924@users.noreply.github.com> Date: Thu, 22 May 2025 17:31:17 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat(#100):=20=ED=86=A0=ED=81=B0=EC=97=90?= =?UTF-8?q?=20=ED=95=99=EC=83=9D=ED=9A=8C=EB=B9=84=20=EB=82=A9=EB=B6=80=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/backend/global/security/jwt/TokenProvider.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/site/billilge/api/backend/global/security/jwt/TokenProvider.kt b/src/main/kotlin/site/billilge/api/backend/global/security/jwt/TokenProvider.kt index 645ad33..2406778 100644 --- a/src/main/kotlin/site/billilge/api/backend/global/security/jwt/TokenProvider.kt +++ b/src/main/kotlin/site/billilge/api/backend/global/security/jwt/TokenProvider.kt @@ -37,10 +37,10 @@ class TokenProvider( fun generateToken(member: Member, expiredAt: Duration): String { val now = Date() - return makeToken(Date(now.time + expiredAt.toMillis()), member.studentId, member.role, member.name) + return makeToken(Date(now.time + expiredAt.toMillis()), member.studentId, member.role, member.name, member.isFeePaid) } - private fun makeToken(expiry: Date, studentId: String, role: Role, name: String): String { + private fun makeToken(expiry: Date, studentId: String, role: Role, name: String, isFeePaid: Boolean): String { val now = Date() return Jwts.builder() @@ -51,6 +51,7 @@ class TokenProvider( .setSubject(studentId) .claim("role", role.name) .claim("name", name) + .claim("isFeePaid", isFeePaid) .signWith(createSecretKey(), signatureAlgorithm) .compact() }