Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
518c093
refactor: 엔티티 수정
hyxklee Jun 8, 2026
8f567e2
refactor: 엔티티 수정
hyxklee Jun 8, 2026
aa50a6b
feat: 회비 초안 생성 및 제거 API 구현
hyxklee Jun 10, 2026
b8c98e9
Merge branch 'refs/heads/dev' into feat/WTH-404-회비-기능-API-구현
hyxklee Jun 11, 2026
7255207
feat: 회비 납부 대상자 조회 및 설정 API 구현
hyxklee Jun 11, 2026
c572d9b
feat: 회비 이월 조회/설정 API 구현
hyxklee Jun 11, 2026
d4fa333
feat: 전역 페이지 response 객체 구현
hyxklee Jun 11, 2026
1129e55
feat: 계좌 정보 저장 API 구현
hyxklee Jun 11, 2026
c667c13
refactor: 동아리 장부 접근 권한 확인 확장 함수 분리
hyxklee Jun 11, 2026
9697031
docs: 탈퇴시 처리 주석 추가
hyxklee Jun 11, 2026
7d2437b
feat: 회비 공개 설정 API 추가
hyxklee Jun 11, 2026
569cec2
feat: 회비 등록 API 구현
hyxklee Jun 11, 2026
e34c455
feat: 회비 등록 API 예외 추가
hyxklee Jun 11, 2026
cd824f6
feat: 공개 여부 DTO 추가
hyxklee Jun 11, 2026
a0e4e2f
docs: dto 설명 추가
hyxklee Jun 11, 2026
7a8ddae
refactor: 확장함수 사용
hyxklee Jun 11, 2026
925be61
feat: 회비 등록 상태 조회 API 구현
hyxklee Jun 11, 2026
7704aff
refactor: 엔티티 수정
hyxklee Jun 11, 2026
2dc8bc8
refactor: 컨트롤러 제거
hyxklee Jun 11, 2026
edb3a89
refactor: API 응답 코드 추가
hyxklee Jun 11, 2026
a146608
refactor: 초안 폐기시 삭제
hyxklee Jun 11, 2026
bc79a4d
test: 회비 등록 관련 테스트 추가
hyxklee Jun 11, 2026
6327dd8
test: 회비 등록 관련 테스트 추가
hyxklee Jun 11, 2026
e10f737
refactor: 설명 선택 처리
hyxklee Jun 11, 2026
76979be
docs: API 명세 수정
hyxklee Jun 11, 2026
2f82d16
flyway: DB 마이그레이션 쿼리 추가
hyxklee Jun 13, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.weeth.domain.account.application.dto.request

import com.weeth.domain.account.domain.vo.BankAccount
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class SaveAccountBankAccountRequest(
@field:Schema(description = "계좌 공개 여부", example = "true")
val bankAccountVisible: Boolean,
@field:Schema(description = "입금 계좌 정보. bankAccountVisible=true 일 때 필수", nullable = true)
@field:Valid
val bankAccount: BankAccountRequest?,
)

data class BankAccountRequest(
@field:Schema(description = "은행명", example = "국민은행")
@field:NotBlank
@field:Size(max = 30)
val bankName: String,
@field:Schema(description = "계좌번호", example = "123-456-789012")
@field:NotBlank
@field:Size(max = 50)
val accountNumber: String,
@field:Schema(description = "예금주", example = "가천대 검도부")
@field:NotBlank
@field:Size(max = 30)
val holder: String,
@field:Schema(description = "입금 안내 메모", example = "이름_회비 형식으로 입금해 주세요.", nullable = true)
@field:Size(max = 30)
val guide: String? = null,
) {
fun toBankAccount(): BankAccount =
BankAccount.of(
bankName = bankName,
accountNumber = accountNumber,
holder = holder,
guide = guide,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.weeth.domain.account.application.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Positive
import jakarta.validation.constraints.Size

data class SaveAccountBasicRequest(
@field:Schema(description = "회비 이름", example = "5기 정기 회비")
@field:Size(max = 30)
@field:NotBlank
val name: String,
@field:Schema(description = "1인 회비 금액 (원)", example = "50000")
@field:Positive
val duesAmount: Int,
@field:Schema(description = "회비 설명", example = "동아리 운영비로 사용됩니다.", nullable = true)
@field:Size(max = 30)
val description: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.weeth.domain.account.application.dto.request

import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Size

data class SaveAccountCarryOverRequest(
@field:Schema(description = "이월 활성화 여부", example = "true")
val enabled: Boolean,
@field:Schema(description = "이월 금액 (원). enabled=true 일 때 필수", example = "152129", nullable = true)
val amount: Int?,
@field:Schema(description = "이월 메모. 사용자가 입력하지 않은 경우는 'O기 이월 금액입니다.'를 넣어주세요.", example = "4기 잔액", nullable = true)
@field:Size(max = 30)
val memo: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.weeth.domain.account.application.dto.request

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

data class SavePaymentTargetsRequest(
@field:Schema(
description = "납부 대상으로 지정할 동아리 회원 ID 목록. 두 목록에 모두 없는 회원의 기존 상태는 유지됩니다.",
example = "[1, 2, 3]",
)
val targetedClubMemberIds: List<Long> = emptyList(),
@field:Schema(
description = "납부 대상에서 제외할 동아리 회원 ID 목록. 두 목록에 모두 없는 회원의 기존 상태는 유지됩니다.",
example = "[4, 5]",
)
val excludedClubMemberIds: List<Long> = emptyList(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.weeth.domain.account.application.dto.request

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

data class UpdateMemberVisibilityRequest(
@field:Schema(description = "부원 거래 내역 공개 여부", example = "true")
val visible: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.weeth.domain.account.application.dto.response

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

data class AccountCarryOverSourceResponse(
@field:Schema(
description =
"이전 기수 활성 장부 존재 여부. true면 잔액 배너를 노출하고 이월 금액으로 balance를 사용해주세요. " +
"false면 '이전 기수 정보가 없습니다' 안내와 함께 금액 직접 입력 UI를 노출해주세요.",
example = "true",
)
val hasPreviousAccount: Boolean,
@field:Schema(description = "이전 기수. 이전 장부가 없으면 null", example = "3", nullable = true)
val cardinalNumber: Int?,
@field:Schema(description = "이전 기수 장부 잔액 (원). 이전 장부가 없으면 null", example = "240000", nullable = true)
val balance: Int?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.weeth.domain.account.application.dto.response

import com.weeth.domain.account.domain.enums.AccountPaymentStatus
import com.weeth.domain.account.domain.enums.AccountTargetStatus
import com.weeth.domain.club.domain.enums.MemberRole
import com.weeth.domain.club.domain.enums.MemberStatus
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

data class AccountPaymentTargetResponse(
@field:Schema(description = "납부 대상 ID. 아직 선택되지 않은 후보 부원은 null")
val targetId: Long?,
@field:Schema(description = "동아리 부원 프로필")
val paymentTargetInfo: PaymentTargetInfoResponse,
@field:Schema(description = "납부 대상 상태 (TARGETED: 대상, EXCLUDED: 제외)", example = "TARGETED")
val targetStatus: AccountTargetStatus,
@field:Schema(description = "납부 상태 (PAID: 납부 완료, UNPAID: 미납)", example = "UNPAID")
val paymentStatus: AccountPaymentStatus,
@field:Schema(description = "납부 금액 (원)", example = "50000")
val dueAmount: Int,
@field:Schema(description = "실제 납부된 금액 (원)", example = "50000")
val paidAmount: Int,
@field:Schema(description = "납부 일시", nullable = true)
val paidAt: LocalDateTime?,
@field:Schema(description = "납부 확인자 ID", nullable = true)
val confirmedBy: Long?,
@field:Schema(description = "메모", nullable = true)
val memo: String?,
) {
data class PaymentTargetInfoResponse(
@field:Schema(description = "사용자 ID", example = "1")
val userId: Long,
@field:Schema(description = "멤버 ID", example = "1")
val clubMemberId: Long,
@field:Schema(description = "사용자 이름", example = "홍길동")
val name: String,
@field:Schema(description = "전화번호", example = "01012345678")
val tel: String?,
@field:Schema(description = "학교", example = "가천대학교")
val school: String?,
@field:Schema(description = "학과", example = "컴퓨터공학과")
val department: String?,
@field:Schema(description = "멤버 권한", example = "USER")
val memberRole: MemberRole,
@field:Schema(description = "멤버 상태", example = "ACTIVE")
val memberStatus: MemberStatus,
@field:Schema(description = "동아리 프로필 이미지 URL", example = "https://cdn.example.com/profile.jpg")
val profileImageUrl: String?,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.weeth.domain.account.application.dto.response

import com.weeth.global.common.response.PageResponse
import io.swagger.v3.oas.annotations.media.Schema

data class AccountPaymentTargetsResponse(
@field:Schema(description = "납부 대상 요약")
val summary: PaymentTargetSummaryResponse,
@field:Schema(description = "납부 대상 페이지")
val targets: PageResponse<AccountPaymentTargetResponse>,
) {
data class PaymentTargetSummaryResponse(
@field:Schema(description = "전체 활성 부원 수", example = "18")
val totalCount: Int,
@field:Schema(description = "선택된 납부 대상 수", example = "12")
val targetedCount: Int,
@field:Schema(description = "제외된 납부 대상 수", example = "6")
val excludedCount: Int,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.weeth.domain.account.application.dto.response

import com.weeth.domain.account.domain.enums.AccountRegistrationStep
import io.swagger.v3.oas.annotations.media.Schema

data class AccountRegistrationStatusResponse(
@field:Schema(description = "회비 장부 ID")
val accountId: Long,
@field:Schema(
description =
"다음에 작성할 단계. 이어서 작성 시 이 단계 화면에서 시작하고, " +
"스텝퍼에서 이 단계 이전은 완료(✓) 표시해주세요. " +
"(BASIC → PAYMENT_TARGETS → CARRY_OVER → BANK_ACCOUNT → REVIEW 순)",
example = "CARRY_OVER",
)
val registrationStep: AccountRegistrationStep,
@field:Schema(description = "기본 정보 (BASIC 단계 저장 후 non-null). 값으로 폼을 채우고, null이면 빈 폼으로 시작해주세요.", nullable = true)
val basic: BasicInfoResponse?,
@field:Schema(
description = "이월 설정 (CARRY_OVER 단계 저장 후 non-null). 값으로 폼을 채우고, null이면 빈 폼으로 시작해주세요.",
nullable = true,
)
val carryOver: CarryOverResponse?,
@field:Schema(
description =
"납부 대상 설정 요약 (PAYMENT_TARGETS 단계 저장 후 non-null). " +
"대상 목록과 체크 상태는 납부 대상 목록 조회 API로 별도 조회해주세요.",
nullable = true,
)
val paymentTargets: PaymentTargetsResponse?,
@field:Schema(
description = "계좌 설정 (BANK_ACCOUNT 단계 저장 후 non-null). 값으로 폼을 채우고, null이면 빈 폼으로 시작해주세요.",
nullable = true,
)
val bankAccount: BankAccountRegistrationResponse?,
@field:Schema(description = "이전 기수 장부 잔액. 이월 설정 단계의 안내 문구에 사용해주세요. 이전 장부가 없으면 null", nullable = true)
val previousAccountBalance: PreviousAccountBalanceResponse?,
) {
data class BasicInfoResponse(
@field:Schema(description = "회비 장부 이름", example = "5기 정기 회비")
val name: String,
@field:Schema(description = "1인 회비 금액 (원)", example = "50000")
val duesAmount: Int,
@field:Schema(description = "회비 설명", example = "동아리 운영비로 사용됩니다.")
val description: String?,
)

data class CarryOverResponse(
@field:Schema(description = "이월 활성화 여부", example = "true")
val enabled: Boolean,
@field:Schema(description = "이월 금액 (원). enabled=false 이면 0", example = "240000")
val amount: Int,
@field:Schema(description = "이월 메모", example = "3기 잔액", nullable = true)
val memo: String?,
)

data class PaymentTargetsResponse(
@field:Schema(description = "납부 대상 인원 수", example = "20")
val targetCount: Int,
@field:Schema(description = "납부 제외 인원 수", example = "2")
val excludedCount: Int,
)

data class BankAccountRegistrationResponse(
@field:Schema(description = "계좌 공개 여부", example = "true")
val bankAccountVisible: Boolean,
@field:Schema(description = "입금 계좌 정보. bankAccountVisible=false 이면 null", nullable = true)
val bankAccount: BankAccountResponse?,
)

data class PreviousAccountBalanceResponse(
@field:Schema(description = "이전 기수", example = "3")
val cardinalNumber: Int,
@field:Schema(description = "이전 기수 장부 잔액", example = "240000")
val balance: Int,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ data class AccountResponse(
@field:Schema(description = "회비 ID", example = "1")
val accountId: Long,
@field:Schema(description = "회비 설명", example = "2024년 2학기 회비")
val description: String,
val description: String?,
@field:Schema(description = "총 금액", example = "100000")
val totalAmount: Int,
@field:Schema(description = "현재 금액", example = "90000")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.weeth.domain.account.application.dto.response

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

data class BankAccountResponse(
@field:Schema(description = "은행명", example = "국민은행")
val bankName: String,
@field:Schema(description = "계좌번호", example = "123-456-789012")
val accountNumber: String,
@field:Schema(description = "예금주", example = "가천대 검도부")
val holder: String,
@field:Schema(description = "입금 안내 메모", example = "이름_회비 형식으로 입금해 주세요.", nullable = true)
val guide: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.weeth.domain.account.application.dto.response

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

data class CreateAccountDraftResponse(
@field:Schema(description = "회비 장부 ID")
val accountId: Long,
@field:Schema(
description =
"새로 생성된 초안 여부. false면 작성 중인 초안이 이미 있으므로 " +
"'이어서 작성 / 새로 작성' 분기를 노출해주세요. " +
"이어서 작성 시 등록 현황 조회 API로 폼을 복원하고, 새로 작성 시 초안 폐기 API 호출 후 본 API를 재호출해주세요.",
example = "true",
)
val isNew: Boolean,
@field:Schema(
description = "기존 초안의 마지막 수정자 이름. '00님이 작성 중인 회비가 있어요' 안내 문구에 사용해주세요. 신규 초안이면 null",
nullable = true,
)
val lastModifiedByName: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.weeth.domain.account.application.exception

import com.weeth.global.common.exception.BaseException

class AccountCarryOverAmountMismatchException : BaseException(AccountErrorCode.ACCOUNT_CARRY_OVER_AMOUNT_MISMATCH)
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,19 @@ enum class AccountErrorCode(

@ExplainError("영수증이 요청한 기수의 장부에 속하지 않거나 동아리에 속하지 않는 경우에 발생합니다.")
RECEIPT_ACCOUNT_MISMATCH(20103, HttpStatus.BAD_REQUEST, "영수증이 해당 기수의 장부에 속하지 않습니다."),

@ExplainError("초안 상태가 아닌 회비 장부를 등록 플로우에서 수정하거나 폐기하려고 할 때 발생합니다.")
ACCOUNT_INVALID_DRAFT_STATE(20104, HttpStatus.BAD_REQUEST, "초안 상태의 회비 장부만 처리할 수 있습니다."),

@ExplainError("납부 대상 멤버가 기수 명부에 없거나, 대상/제외 목록에 같은 멤버가 동시에 포함될 때 발생합니다.")
ACCOUNT_PAYMENT_TARGET_MEMBER_INVALID(20105, HttpStatus.BAD_REQUEST, "유효하지 않은 납부 대상 멤버입니다."),

@ExplainError("이미 납부 완료된 대상을 제외하려고 할 때 발생합니다.")
ACCOUNT_PAYMENT_TARGET_ALREADY_PAID(20106, HttpStatus.BAD_REQUEST, "납부 완료된 대상은 제외할 수 없습니다."),

@ExplainError("모든 등록 단계를 저장하지 않은 채 회비 등록 완료를 요청할 때 발생합니다.")
ACCOUNT_REGISTRATION_STEP_INCOMPLETE(20107, HttpStatus.BAD_REQUEST, "모든 등록 단계를 완료한 후 등록할 수 있습니다."),

@ExplainError("이월 금액이 완료 시점의 이전 기수 장부 잔액과 다를 때 발생합니다. 이월 재원을 다시 조회한 후 이월 설정을 재저장해야 합니다.")
ACCOUNT_CARRY_OVER_AMOUNT_MISMATCH(20108, HttpStatus.CONFLICT, "이전 기수 잔액이 변경되었습니다. 이월 금액을 다시 확인해주세요."),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.weeth.domain.account.application.exception

import com.weeth.global.common.exception.BaseException

class AccountInvalidDraftStateException : BaseException(AccountErrorCode.ACCOUNT_INVALID_DRAFT_STATE)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.weeth.domain.account.application.exception

import com.weeth.global.common.exception.BaseException

class AccountPaymentTargetMemberInvalidException : BaseException(AccountErrorCode.ACCOUNT_PAYMENT_TARGET_MEMBER_INVALID)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.weeth.domain.account.application.exception

import com.weeth.global.common.exception.BaseException

class AccountPaymentTargetPaidException : BaseException(AccountErrorCode.ACCOUNT_PAYMENT_TARGET_ALREADY_PAID)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.weeth.domain.account.application.exception

import com.weeth.global.common.exception.BaseException

class AccountRegistrationStepIncompleteException : BaseException(AccountErrorCode.ACCOUNT_REGISTRATION_STEP_INCOMPLETE)
Loading