Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4ec439a
deploy: 내부서버 배포를 위한 dev 버전 환경변수 이름 수정
tnals0924 Jan 5, 2026
058cfc6
deploy: ci-cd_dev.yml 파일 삭제
tnals0924 Jan 5, 2026
317c38a
chore: docker build만 수행되도록 actions 수정
tnals0924 Jan 5, 2026
757cfe4
chore: JDK 21로 업그레이드
tnals0924 Jan 5, 2026
c76a3ec
chore: 개발버전 Dockerfile 변경
tnals0924 Jan 5, 2026
062e504
refactor: facade 패턴 도입
tnals0924 Feb 16, 2026
1596fc4
Merge pull request #105 from billilge/refactor/#104
tnals0924 Feb 16, 2026
531fe29
feat: 관리자 역할 분리
tnals0924 Feb 16, 2026
3a6eeac
feat: 관리자 등록 시, 역할 지정해서 등록할 수 있도록 구현
tnals0924 Feb 16, 2026
103a91b
Merge pull request #107 from billilge/feat/#106
tnals0924 Feb 16, 2026
d88fb0a
feat: 디스플레이 포스터, 일정 기능 구현
tnals0924 Feb 16, 2026
0c416c2
feat: 관리자 역할 변경 기능 구현
tnals0924 Feb 16, 2026
549e6f9
Merge branch 'feat/#106' into develop
tnals0924 Feb 16, 2026
e612839
feat: 환경변수 수정 가능하도록 DB화 및 API 구현
tnals0924 Feb 18, 2026
a1a455d
Merge pull request #109 from billilge/feat/#108
tnals0924 Feb 18, 2026
93f02a5
Merge pull request #111 from billilge/feat/#110
tnals0924 Feb 18, 2026
1542d9e
feat: 관리자 비밀번호 수정 api 구현
tnals0924 Feb 18, 2026
5bb51fc
feat: 디스플레이용 조회 API 구현
tnals0924 Feb 19, 2026
d76cd9c
deploy: 기존 ec2 배포 파이프라인 주석 처리
tnals0924 Feb 19, 2026
7a17270
HOTFIX: config_value 테이블 이름 변경
tnals0924 Feb 19, 2026
e84d784
HOTFIX: config value 컬럼 수정
tnals0924 Feb 19, 2026
541d1d8
feat: 관리자 목록 조회 시 역할도 추가하도록 구현
tnals0924 Feb 19, 2026
3bd23ba
feat: 관리자 조회 시, GA와 WORKER도 함께 조회하도록 수정
tnals0924 Feb 19, 2026
fd73512
feat: 관리자용 대여물품 검색 api 추가
tnals0924 Feb 19, 2026
4693f17
chore: 관리자 대여기록 추가 시, 학생회비 미납 문구 수정
tnals0924 Feb 19, 2026
4eb9080
chore: CLAUDE.md 파일 추가
tnals0924 Feb 20, 2026
f835dae
feat: 파일 스토리지 AWS S3에서 minio로 마이그레이션
tnals0924 Feb 20, 2026
b4c75cd
Merge pull request #113 from billilge/feat/#112
tnals0924 Feb 20, 2026
3a8bad6
feat: 새학기 배포 전 마무리
tnals0924 Mar 2, 2026
7aa6770
fix: 대여기록에 근무자 기록되도록 수정
tnals0924 Mar 2, 2026
19b52cb
Merge branch 'main' into develop
tnals0924 Mar 4, 2026
04b15b5
HOTFIX: enum 중복 수정
tnals0924 Mar 4, 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
36 changes: 18 additions & 18 deletions .github/workflows/ci-cd_dev.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Development Server CI/CD with Gradle and Docker
name: Production Server CI/CD with Gradle and Docker

on:
push:
Expand Down Expand Up @@ -49,20 +49,20 @@ jobs:
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
# 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: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:latest
#
# - name: delete old docker image
# run: sudo docker image prune -f
34 changes: 17 additions & 17 deletions .github/workflows/ci-cd_prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,20 @@ jobs:
if: github.event_name == 'push'
run: docker push ${{ secrets.DOCKER_USERNAME }}/billilge: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: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:latest

- name: delete old docker image
run: sudo docker image prune -f
# 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: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:latest
#
# - name: delete old docker image
# run: sudo docker image prune -f
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ application-local.yml

.DS_Store
/src/main/resources/firebase
/src/main/resources/payer_insert_queries.sql
/src/main/resources/*.sql
182 changes: 182 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Billilge Backend

대학교 학생회 물품 대여 관리 시스템 백엔드

## 기술 스택

- **Spring Boot 3.4.1** / **Kotlin 1.9.25** / **JDK 21**
- **JPA + QueryDSL** (MySQL)
- **Spring Security** (JWT + Google OAuth2)
- **Firebase Cloud Messaging** (푸시 알림)
- **AWS S3** (이미지 업로드)
- **Apache POI** (Excel 생성)

## 빌드 & 실행

```bash
./gradlew compileKotlin # 컴파일 확인
./gradlew build # 전체 빌드
./gradlew bootRun # 실행
```

## 아키텍처

```
Controller → Facade → Service → Repository
```

| 레이어 | 역할 | DTO 참조 |
|--------|------|----------|
| **Controller** | HTTP 요청/응답, `@AuthenticationPrincipal`로 인증 정보 추출 | Request/Response DTO |
| **Facade** | Request DTO 분해, 크로스 도메인 조합, Response DTO 생성 | Request/Response DTO |
| **Service** | 비즈니스 로직, 자기 도메인 Repository만 사용 | Entity/primitives만 |
| **Repository** | 데이터 접근 (JPA + QueryDSL) | Entity/Query DTO만 |

### 핵심 규칙

- **Service는 Request/Response DTO를 참조하지 않는다** — Entity, primitives, 글로벌 DTO(`PageableCondition`, `SearchCondition`)만 사용
- **Service는 타 도메인 Repository를 직접 의존하지 않는다** — 타 도메인 Service를 통해 접근 (예외: `PayerService → MemberRepository` 순환 의존 방지)
- **크로스 도메인 조합은 Facade에서 수행한다** — Facade가 여러 Service를 호출해 엔티티를 조합 후 Service에 전달
- **Facade에서 트랜잭션 필요 시 `@Transactional` 명시** — 여러 서비스 호출을 하나의 persistence context로 묶어야 할 때

### 트랜잭션 패턴

- Service 클래스에 `@Transactional(readOnly = true)` 기본 적용
- 쓰기 메서드만 `@Transactional`로 오버라이드
- JPA dirty checking 활용 — `repository.save()` 없이 엔티티 필드 변경으로 자동 반영

## 도메인 구조

```
domain/
├── item/ # 물품 관리
├── member/ # 회원, 인증
├── notification/ # 알림 (FCM 푸시)
├── payer/ # 회비 납부자 관리
└── rental/ # 대여/반납 관리
```

각 도메인 패키지 구조:
```
domain/{name}/
├── controller/ # API 컨트롤러 + Api 인터페이스 (Swagger)
├── facade/ # Facade (DTO 변환, 크로스 도메인 조합)
├── service/ # 비즈니스 로직
├── repository/ # JPA Repository + Custom(QueryDSL)
├── entity/ # JPA Entity
├── dto/ # request/, response/
├── enums/ # 도메인 열거형
└── exception/ # 도메인 에러 코드
```

## 서비스 의존성

```
ItemService → ItemRepository, S3Service
MemberService → MemberRepository, TokenProvider, PayerService
NotificationService → NotificationRepository, FCMService, MemberService
PayerService → PayerRepository, MemberRepository, ExcelGenerator
RentalService → RentalRepository, NotificationService
```

## 대여 상태 머신

```
[사용자 신청] PENDING → CONFIRMED → RENTAL → RETURN_PENDING → RETURN_CONFIRMED → RETURNED
→ REJECTED
PENDING → CANCEL (사용자 취소)

[관리자 생성] 대여물품: → RENTAL (바로 대여중)
소모품: → RETURNED (즉시 반납 처리)
```

- **CONFIRMED**: 재고 차감, 담당자(worker) 배정
- **RETURNED**: 재고 복원 (소모품 제외)
- **소모품(CONSUMPTION)**: RENTAL 상태 요청 시 자동으로 RETURNED 처리

## 대여 비즈니스 규칙

- 회비 납부자(`isFeePaid`)만 대여 가능
- 동일 물품 중복 대여 불가 (`ignoreDuplicate`로 우회 가능)
- 시험 기간 대여 불가 (`exam-period.start-date` / `end-date`)
- 주말 대여 불가
- 과거 시간 대여 불가
- 10시~17시만 대여 가능
- Dev 모드(`/rentals/dev`): 시간 검증 생략, ADMIN 역할 필요

## API 엔드포인트

### 인증 (Public)
| Method | Path | 설명 |
|--------|------|------|
| POST | `/auth/sign-up` | 회원가입 |
| POST | `/auth/admin-login` | 관리자 로그인 |

### 물품 (Public)
| Method | Path | 설명 |
|--------|------|------|
| GET | `/items` | 물품 검색 |

### 회원 (JWT 필요)
| Method | Path | 설명 |
|--------|------|------|
| POST | `/members/me/fcm-token` | FCM 토큰 등록 |

### 알림 (JWT 필요)
| Method | Path | 설명 |
|--------|------|------|
| GET | `/notifications` | 알림 목록 |
| GET | `/notifications/count` | 안읽은 알림 수 |
| PATCH | `/notifications/{id}` | 알림 읽음 |
| PATCH | `/notifications/all` | 전체 읽음 |

### 대여 (JWT 필요)
| Method | Path | 설명 |
|--------|------|------|
| POST | `/rentals` | 대여 신청 |
| POST | `/rentals/dev` | 개발용 대여 (시간 검증 생략) |
| GET | `/rentals` | 대여 이력 조회 |
| PATCH | `/rentals/{id}` | 대여 취소 |
| PATCH | `/rentals/return/{id}` | 반납 신청 |
| GET | `/rentals/return-required` | 반납 필요 목록 |

### 관리자 (JWT + @OnlyAdmin)
| Method | Path | 설명 |
|--------|------|------|
| GET | `/admin/items` | 물품 목록 (대여자 수 포함) |
| POST | `/admin/items` | 물품 추가 |
| PUT | `/admin/items/{id}` | 물품 수정 |
| GET | `/admin/items/{id}` | 물품 상세 |
| DELETE | `/admin/items/{id}` | 물품 삭제 |
| GET | `/admin/members` | 회원 목록 |
| GET | `/admin/members/admins` | 관리자 목록 |
| POST | `/admin/members/admins` | 관리자 등록 |
| DELETE | `/admin/members/admins` | 관리자 해제 |
| GET | `/admin/members/payers` | 납부자 목록 |
| POST | `/admin/members/payers` | 납부자 등록 |
| DELETE | `/admin/members/payers` | 납부자 삭제 |
| GET | `/admin/members/payers/excel` | 납부자 엑셀 다운로드 |
| GET | `/admin/notifications` | 관리자 알림 |
| GET | `/admin/rentals/dashboard` | 대시보드 |
| GET | `/admin/rentals` | 대여 이력 |
| PATCH | `/admin/rentals/{id}` | 대여 상태 변경 |
| POST | `/admin/rentals` | 관리자 대여 생성 |
| DELETE | `/admin/rentals/{id}` | 대여 이력 삭제 |

## Global 패키지

```
global/
├── annotation/ # @OnlyAdmin
├── config/ # SecurityConfig, CorsConfig, SwaggerConfig, QueryDslConfig, AsyncConfig
├── dto/ # PageableCondition, SearchCondition, PageableResponse
├── exception/ # ApiException, ErrorCode, GlobalExceptionHandler
├── external/
│ ├── fcm/ # FCMConfig, FCMService
│ └── s3/ # S3Config, S3Service
├── logging/ # LoggingFilter
├── security/
│ ├── jwt/ # TokenProvider, TokenAuthenticationFilter
│ └── oauth2/ # Google OAuth2 핸들러, UserAuthInfo
└── utils/ # DateUtils(isWeekend), ExcelGenerator
```
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Dockerfile

# jdk17 Image Start
FROM openjdk:17
# jdk21 Image Start
FROM eclipse-temurin:21-jre

# jar 파일 복제
COPY build/libs/*.jar app.jar
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Dockerfile

# jdk17 Image Start
FROM openjdk:17
# jdk21 Image Start
FROM eclipse-temurin:21-jre

# jar 파일 복제
COPY build/libs/*.jar app.jar
Expand Down
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ version = "0.0.1-SNAPSHOT"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(21)
}
}

Expand Down Expand Up @@ -47,7 +47,7 @@ dependencies {

implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")

implementation("io.awspring.cloud:spring-cloud-starter-aws:2.4.4")
implementation("io.minio:minio:8.5.7")
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("org.apache.tika:tika-core:2.9.2")
implementation("org.apache.tika:tika-parsers-standard-package:2.9.2")
Expand All @@ -70,6 +70,7 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.security:spring-security-test")
runtimeOnly("com.h2database:h2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package site.billilge.api.backend.domain.configvalue.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.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import site.billilge.api.backend.domain.configvalue.dto.request.ChangeAdminPasswordRequest
import site.billilge.api.backend.domain.configvalue.dto.request.ConfigValueBulkUpdateRequest
import site.billilge.api.backend.domain.configvalue.dto.request.ConfigValueUpdateRequest
import site.billilge.api.backend.domain.configvalue.dto.response.ConfigValueDetail
import site.billilge.api.backend.domain.configvalue.dto.response.ConfigValueFindAllResponse

@Tag(name = "(Admin) ConfigValue", description = "관리자용 설정값 API")
interface AdminConfigValueApi {
@Operation(
summary = "설정값 조회",
description = "키로 설정값을 조회하는 API"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "설정값 조회 성공"),
ApiResponse(responseCode = "404", description = "설정값을 찾을 수 없습니다.")
]
)
fun getByKey(@RequestParam key: String): ResponseEntity<ConfigValueDetail>

@Operation(
summary = "설정값 일괄 조회",
description = "여러 키로 설정값을 일괄 조회하는 API"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "설정값 일괄 조회 성공")
]
)
fun getAllByKeys(@RequestParam keys: List<String>): ResponseEntity<ConfigValueFindAllResponse>

@Operation(
summary = "설정값 수정",
description = "설정값을 수정하는 API (존재하지 않으면 생성)"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "설정값 수정 성공")
]
)
fun update(@RequestBody request: ConfigValueUpdateRequest): ResponseEntity<Void>

@Operation(
summary = "설정값 일괄 수정",
description = "여러 설정값을 일괄 수정하는 API (존재하지 않으면 생성)"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "설정값 일괄 수정 성공")
]
)
fun updateAll(@RequestBody request: ConfigValueBulkUpdateRequest): ResponseEntity<Void>

@Operation(
summary = "관리자 비밀번호 변경",
description = "현재 비밀번호를 검증 후 새 비밀번호로 변경하는 API"
)
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "비밀번호 변경 성공"),
ApiResponse(responseCode = "400", description = "현재 비밀번호가 일치하지 않습니다.")
]
)
fun changeAdminPassword(@RequestBody request: ChangeAdminPasswordRequest): ResponseEntity<Void>
}
Loading
Loading