Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4f1dda4
feat: core:common 인증 추상 추가
tnals0924 Aug 18, 2026
a4d0e15
chore: JWT 설정 바인딩 골격 추가
tnals0924 Aug 18, 2026
6a4012b
feat: JWT 토큰 발급·파싱 JwtProvider 추가
tnals0924 Aug 18, 2026
e7f25de
feat: JWT 인증 필터와 PrincipalProvider 구현 추가
tnals0924 Aug 18, 2026
4c2449d
feat: SecurityConfig 추가로 JWT 인증 체인 구성
tnals0924 Aug 18, 2026
70a0540
refactor: 인증 클래스에 Lombok·정적 팩토리 적용
tnals0924 Aug 18, 2026
0b0f836
docs: 정적 팩토리·Lombok 사용 규칙 추가
tnals0924 Aug 18, 2026
691d944
refactor: MemberJpaEntity 생성을 정적 팩토리로 변경
tnals0924 Aug 18, 2026
c355cc7
refactor: SecurityConfig 생성자 주입을 @RequiredArgsConstructor로 변경
tnals0924 Aug 18, 2026
a42acd4
refactor: 인증 실패 응답 처리를 EntryPoint·AccessDeniedHandler 클래스로 분리
tnals0924 Aug 18, 2026
a1f1de9
chore: .gitignore에 계획 문서 경로 추가
tnals0924 Aug 18, 2026
4e4ca78
refactor: JwtAuthFilter 예외 처리를 SecurityConfig 인증 실패 경로로 이관
tnals0924 Aug 18, 2026
fb7d351
refactor: 공개 엔드포인트를 PublicEndpoints로 분리
tnals0924 Aug 18, 2026
8c4cea5
feat: 공통 API 응답 래퍼 ApiResponse 추가
xeoxxn Aug 20, 2026
b2af7be
feat: 커서 기반 목록 조회 응답 CursorSliceResponse 추가
xeoxxn Aug 20, 2026
66bf2e1
docs: architecture.md의 SecurityConfig 위치를 gateway:auth로 정정
tnals0924 Aug 24, 2026
97fcf30
refactor: PublicEndpoints의 전체 PathPattern을 static 상수로 추출
tnals0924 Aug 24, 2026
b188cf3
docs: 커밋 메시지에 AI 트레일러를 넣지 않는 규칙 추가
tnals0924 Aug 24, 2026
1f2ce7b
Merge pull request #8 from billilge/feat/#7-api-response-format
tnals0924 Aug 24, 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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### gateway:auth — JWT ###
# HS256 서명 키. 최소 256비트(32바이트) 이상이어야 한다
JWT_SECRET_KEY=replace-with-32-byte-or-longer-secret-key
JWT_ISSUER=stream-server
# Access Token 만료 시간 (ms)
JWT_ACCESS_TOKEN_EXPIRY=3600000
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/

.DS_Store

docs/plans
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ infrastructure/

- 커밋: `type: 제목` (한글, 마침표 없음). type은 `feat`/`fix`/`refactor`/`docs`/`test`/`chore`/`init`
- 작업 단위별로 커밋을 나눈다
- 커밋 메시지·PR 본문에 `Co-Authored-By: Claude ...`, `Claude-Session: ...` 등 AI 트레일러를 넣지 않는다
- 브랜치: `{type}/#{이슈번호}-{작업내용}`
- PR 제목: `[{Type}/#{이슈번호}] {설명}`, Squash Merge 기본, `main` 직접 push 금지
41 changes: 41 additions & 0 deletions api/common-api/src/main/java/kr/ac/kookmin/stream/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kr.ac.kookmin.stream;

import kr.ac.kookmin.stream.common.BusinessException;
import kr.ac.kookmin.stream.common.ErrorCode;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class ApiResponse<T> {

private static final String SUCCESS_CODE = "SUCCESS";
private static final String SUCCESS_MESSAGE = "요청에 성공했습니다.";

private final boolean success;
private final String code;
private final String message;
private final T data;

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, SUCCESS_CODE, SUCCESS_MESSAGE, data);
}

public static <T> ApiResponse<T> success() {
return success(null);
}

public static <T> ApiResponse<T> error(BusinessException exception) {
ErrorCode errorCode = exception.getErrorCode();
return new ApiResponse<>(false, errorCode.name(), exception.getMessage(), null);
}

public static <T> ApiResponse<T> error(ErrorCode errorCode) {
return new ApiResponse<>(false, errorCode.name(), errorCode.message(), null);
}

public static <T> ApiResponse<T> error(String code, String message) {
return new ApiResponse<>(false, code, message, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.ac.kookmin.stream;

import java.util.List;
import kr.ac.kookmin.stream.common.CursorSliceResult;

public record CursorSliceResponse<T>(List<T> content, boolean hasNext, Long nextCursor) {

public static <T> CursorSliceResponse<T> from(CursorSliceResult<T> result) {
return new CursorSliceResponse<>(
result.content(),
result.hasNext(),
result.nextCursor()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class StreamServerApplication {

public static void main(String[] args) {
Expand Down
3 changes: 3 additions & 0 deletions bootstrap/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
spring:
application:
name: stream-server
config:
import:
- classpath:application-gateway-auth.yml
6 changes: 6 additions & 0 deletions bootstrap/src/test/resources/application-gateway-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 테스트 클래스패스가 gateway:auth의 동명 파일을 가린다.
# 실제 값은 환경변수로 주입되므로 테스트에서는 더미 값을 쓴다.
jwt:
secret-key: test-secret-key-must-be-at-least-32-bytes-long
issuer: stream-server
access-token-expiry: 3600000
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.ac.kookmin.stream.common;

/**
* 학생회 부서. ADMIN에게만 부여되며, member 도메인의 학부(Department)와는 다른 개념이다.
*/
public enum CouncilDepartment {
PRESIDENCY, // 회장단
EXECUTIVE, // 집행부
GENERAL_AFFAIRS, // 총무부
PLANNING, // 기획부
PR, // 홍보부
MEDIA, // 미디어부
WELFARE, // 복지부
COMMUNICATION // 소통부
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.ac.kookmin.stream.common;

import java.util.List;

public record CursorSliceResult<T>(List<T> content, boolean hasNext, Long nextCursor) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.ac.kookmin.stream.common;

import java.util.Set;

public interface PrincipalProvider {
Long userId();
Set<Role> roles();
Set<CouncilDepartment> councilDepartments();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.ac.kookmin.stream.common;

public enum Role {
STUDENT,
ADMIN
}
3 changes: 2 additions & 1 deletion docs/conventions/00-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Java 21 + Spring Boot 4.1 + Spring Modulith 기반, 단일 학생회 플랫폼
| 문서 | 다루는 내용 | 언제 참조하는가 |
| --- | --- | --- |
| [`architecture.md`](./architecture.md) | 모듈 구조, 의존 방향, Modulith 경계 규칙, 레이어, 도메인 간 통신(UseCase·이벤트·아웃박스) | 새 모듈/도메인 설계, 의존성 리뷰 |
| [`coding-style.md`](./coding-style.md) | 네이밍, 도메인 객체(record)/DTO/Command/Entity/Repository/Service/UseCase 패턴, Validation | 실제 코드 작성/리뷰 |
| [`coding-style.md`](./coding-style.md) | 네이밍, 도메인 객체(record)/DTO/Command/Entity/Repository/Service/UseCase 패턴, 정적 팩토리·Lombok, Validation | 실제 코드 작성/리뷰 |
| [`error-handling.md`](./error-handling.md) | `ErrorCode`/`BusinessException`, `GlobalExceptionHandler`, `@ApiErrorCode` Swagger 문서화 | 에러 코드 추가, 예외 처리 |
| [`config-and-auth.md`](./config-and-auth.md) | 설정 바인딩, 2계층 권한 모델(role + 부서), `PrincipalProvider`, `DepartmentAccessChecker` | 설정값 추가, 인증·인가 작업 |
| [`logging.md`](./logging.md) | MDC 요청 추적, `MdcFilter`/`LoggingFilter`, 로그 레벨, JSON 로깅 | 로깅 코드, MDC 필드 추가 |
Expand All @@ -28,6 +28,7 @@ Java 21 + Spring Boot 4.1 + Spring Modulith 기반, 단일 학생회 플랫폼
- "A 도메인 변화에 B가 반응" → `architecture.md` 6-2절 (이벤트 + 아웃박스)
- "부서 권한으로 승인 제한" → `config-and-auth.md` 4-4절 (`DepartmentAccessChecker`)
- "soft delete 컬럼 인덱스/유니크" → `flyway-migration.md` 3-4절
- "객체를 어떻게 생성하지 / Lombok 어디까지" → `coding-style.md` 2-10·2-11절
- "에러 코드 추가" → `error-handling.md`
- "도메인 내부 구현 숨기기" → `architecture.md` 4-3절 (최상위 공개 / `internal`)

Expand Down
10 changes: 5 additions & 5 deletions docs/conventions/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ root
│ ├── welfare/ # 물품 대여·회비·공지 (대여는 자체 신청 프로세스)
│ └── internal/ # 학생회 내부 운영 (운영진·부서 관리 등 어드민 전용)
├── gateway/ # 횡단관심사 그룹
│ ├── auth/ # 인증/인가 (Spring Security, JWT, DepartmentAccessChecker). config-and-auth.md 참조
│ ├── auth/ # 인증/인가 (SecurityConfig(role→URL), JWT, DepartmentAccessChecker). config-and-auth.md 참조
│ └── logging/ # MDC 기반 요청 추적. logging.md 참조
└── infrastructure/ # 기술 구현 (아웃바운드 어댑터)
├── db/ # JPA Entity, Repository 구현체, Flyway 마이그레이션 (MySQL)
Expand Down Expand Up @@ -83,12 +83,12 @@ root
`api:*`는 **클라이언트(admin/app)를 모듈 경계**로 삼는다(팀·도메인이 아니라). 팀 소유권은 모듈을 쪼개지 않고 **모듈 내부를 팀(bounded context) 단위 패키지**로 가른다.

- `admin-api`·`app-api` 내부를 **팀(bounded context) 단위 패키지**로 나눠 팀별 파일이 서로 겹치지 않게 한다. 한 팀이 여러 도메인을 묶을 수 있고(예: core = auth·member), admin·app 양쪽에 컨트롤러를 둘 수 있다.
- 여러 팀이 같은 파일을 편집하는 지점은 **보안 설정(role→URL)·라우팅·공통 응답/예외**뿐이며 `common-api`로 한정한다.
- 여러 팀이 같은 파일을 편집하는 지점은 **라우팅·공통 응답/예외**뿐이며 `common-api`로 한정한다. 보안 설정(`SecurityConfig`의 role→URL 인가)은 `gateway:auth`가 소유한다.
- admin 별도 배포가 필요해지면 `admin-api` + 필요한 도메인을 조립하는 bootstrap을 추가한다(현재는 단일 bootstrap).

```
api/
├── common-api # 여러 팀이 공유하는 유일한 지점 (보안·라우팅·응답/예외)
├── common-api # 여러 팀이 공유하는 유일한 지점 (라우팅·응답/예외)
├── admin-api # ADMIN /v1/admin/**
│ └── {basePackage}.{팀} # 팀(bounded context) 패키지 = 소유 단위
└── app-api # STUDENT /v1/app/**
Expand All @@ -100,7 +100,7 @@ api/
```
api/
├── common-api
│ └── {basePackage} # SecurityConfig(role→URL), WebMvcConfig, ApiResponse, GlobalExceptionHandler
│ └── {basePackage} # WebMvcConfig, ApiResponse, GlobalExceptionHandler
├── admin-api
│ └── {basePackage}
│ ├── core # core 팀 (auth·member)
Expand All @@ -122,7 +122,7 @@ api/
- 한 팀 패키지(`core`, `welfare`)의 파일은 그 팀만 건드린다 — admin·app에 흩어져 있어도 소유는 팀 단위다.
- `core` 팀처럼 **여러 도메인(auth·member)을 한 팀이 묶을 수 있다.** 팀 패키지명은 도메인명과 1:1일 필요가 없다.
- 컨트롤러는 접두사(`Admin`/`App`)로 클라이언트를 구분하고, 각 도메인의 공개 `{Domain}Service`(또는 교차 도메인 시 `UseCase`)만 호출한다(5절·6-1절).
- 팀이 겹쳐 충돌하는 지점은 `common-api`의 보안·라우팅·공통 응답뿐이다 — 이 파일들만 변경 시 팀 간 조율이 필요하다.
- 팀이 겹쳐 충돌하는 지점은 `common-api`의 라우팅·공통 응답과 `gateway:auth`의 `SecurityConfig`뿐이다 — 이 파일들만 변경 시 팀 간 조율이 필요하다.

---

Expand Down
Loading