Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8485f48
refactor: reorganize application-prod.yml and update .gitignore
YeaChan05 Sep 9, 2025
6885d60
update .gitignore to include relative paths for docs and build direct…
YeaChan05 Sep 9, 2025
e35089d
update .gitignore to include relative paths for docs and build direct…
YeaChan05 Sep 9, 2025
7247b23
update .gitignore to include relative paths for docs and build direct…
YeaChan05 Sep 9, 2025
7cf3ba3
separate domain module from main module
YeaChan05 Sep 10, 2025
480fc21
modularize package structure and update imports
YeaChan05 Sep 10, 2025
feb8185
modularize package structure and update imports
YeaChan05 Sep 10, 2025
6c1404e
configure build.gradle for modularization and add dependencies
YeaChan05 Sep 10, 2025
3c4e777
remove null body handling from ResponseWrapper
YeaChan05 Sep 11, 2025
70c0158
ensure non-null body in ResponseWrapper
YeaChan05 Sep 11, 2025
7bfa33f
change dependency from api to implementation for common module
YeaChan05 Sep 11, 2025
6e522a3
remove java-test-fixtures plugin and update test task JVM arguments
YeaChan05 Sep 11, 2025
6e3384a
remove unused canScheduleOn method from Hall class
YeaChan05 Sep 11, 2025
ccd6aad
modularize package structure by removing 'webapi' prefix from adapter…
YeaChan05 Sep 11, 2025
c652a9e
rename test classes and reorganize package structure
YeaChan05 Sep 11, 2025
50b4331
remove public modifier from BCryptSecurePasswordEncoder and CustomAut…
YeaChan05 Sep 11, 2025
77df444
refactor build configuration and modularize dependencies
YeaChan05 Sep 11, 2025
916dc21
clean up code formatting and improve readability
YeaChan05 Sep 11, 2025
ddd3bcf
remove public modifier from controller and repository classes
YeaChan05 Sep 11, 2025
54c66dd
rename webapi packages to adapter and update dependencies
YeaChan05 Sep 11, 2025
aaa1a4f
move packages to improve modularity and organization
YeaChan05 Sep 11, 2025
1a0cad5
refactor HallService and ShowService to implement HallValidator for h…
YeaChan05 Sep 11, 2025
31221d4
remove unused event dto
YeaChan05 Sep 11, 2025
5cbb254
add Spring Modulith dependencies and named interfaces for member, sho…
YeaChan05 Sep 11, 2025
104ac54
move test utility classes to utils package for better organization
YeaChan05 Sep 11, 2025
20b9bf6
downgrade Querydsl dependencies to version 5.1.0 for consistency
YeaChan05 Sep 11, 2025
7a3537a
disable bootJar task and enable jar task in build configuration
YeaChan05 Sep 11, 2025
3ee350f
update build configuration and enhance documentation utilities
YeaChan05 Sep 13, 2025
a6920d6
remove outdated comments and streamline build directory references
YeaChan05 Sep 14, 2025
afb7cf2
update dependency configuration and add Docker compose file
YeaChan05 Sep 15, 2025
5f05158
rename controllers and update package structure for modularization
YeaChan05 Sep 15, 2025
c8bef45
modify few documents
YeaChan05 Sep 15, 2025
63209b7
update README to simplify project overview
YeaChan05 Sep 15, 2025
0a75371
update README to reflect new package structure for modularization
YeaChan05 Sep 15, 2025
d0cee67
update build.gradle to improve task dependencies and fix output direc…
YeaChan05 Sep 15, 2025
24c3b1c
update build.gradle to remove specific version for asciidoctor extension
YeaChan05 Sep 15, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ name: Java CI with Gradle

on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
branches: ["main"]
branches: [ "main" ]

jobs:
build:
Expand Down
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Gradle 캐시·빌드 정보
.gradle/
/build
**/build
build
.idea
.aiassistant/rules/AGENTS.md
/src/docs/
224 changes: 122 additions & 102 deletions AGENTS.md

Large diffs are not rendered by default.

57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# booking — 공연 예매 시스템 (개요 중심 README)

- 저장소 루트: 단일 모듈(Spring Boot) 프로젝트
# booking — 공연 예매 시스템

---

Expand All @@ -12,29 +10,37 @@
- 도메인 개요: [docs/specs/domain.md](docs/specs/domain.md)
- 아키텍처/개발 규칙: [docs/specs/policy/application.md](docs/specs/policy/application.md)
- 테스트 규칙: [docs/specs/policy/test.md](docs/specs/policy/test.md)

---

## 2. 핵심 기능

- 추후 작성 예정

테스트로 검증되는 수용 기준은 각 API 문서 하단의 체크리스트를 참고하세요.

---

## 3. 아키텍처 개요 (Hexagonal)
헥사고날 아키텍처를 적용하여 다음과 같은 레이어 규칙을 따릅니다.

- domain: 순수한 도메인 모델과 비즈니스 규칙. 프레임워크 의존 금지.
- app: 유스케이스 서비스, 입력/출력 포트, 트랜잭션 경계, 검증, AOP.
- adapter: 웹 API, 보안, 영속성 등 외부 인터페이스.
헥사고날 아키텍처를 일부 차용한 모듈 구조를 채택했습니다. 각 모듈의 책임은 다음과 같습니다.

- internal: 애플리케이션 내부의 생태계를 관리한다. 직접적인 비즈니스 관리영역이 아닌 '애플리케이션' 자체를 관리한다. 로그 설정, web 설정, 보안 설정등 비즈니스 요구사항을 직접적으로 나타내지 않는
구현들이 존재한다.
- external: 외부 세계와의 통신을 담당한다. 도메인 로직은 물론 애플리케이션과도 완전 독립적인 모듈이다. MQ, STMP등등에 대한 기능의 구현이 존재한다.
- domain: 비즈니스 영역의 핵심이 되는 영역이다. 비즈니스를 해결하기 위한 도메인 그 자체를 의미하며 도메인 개념을 로직으로 풀어나가는 영역이다. Entity와 통신 객체들이 여기에 해당한다.
- common: 공통코드들을 관리한다. 파급효과가 가장 큰 영역인 만큼 라이브러리 사용을 방지하고 POJO 스타일을 원칙으로 한다. 상수와 type object들이 존재한다.
- application: 모든 영역들을 통합해 애플리케이션을 만들어 관리한다. Spring boot의 main class가 존재하며, 각 모듈들을 통합해 비즈니스 요구사항을 해결한다. 비즈니스 로직을 해결하는
영역과 이를 전달하는 영역으로 대부분의 Service 영역과 Controller영역, 그리고 통합 테스트가 존재한다.

- 근거와 세부 규칙

근거와 세부 규칙
- 정책 문서: [docs/specs/policy](docs/specs/policy)
- 레이어 테스트: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java`
- 레이어 테스트: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java`
- 패키지 구조 예
- 도메인: `src/main/java/org/mandarin/booking/domain/*`
- 앱/포트/영속 어댑터: `src/main/java/org/mandarin/booking/app/*` (`app/persist` 포함)
- 웹/보안 어댑터: `src/main/java/org/mandarin/booking/adapter/{webapi,security}/*`
- 도메인: `domain/src/main/java/org/mandarin/booking/domain/*`
- 앱/포트/영속 어댑터: `application/src/main/java/org/mandarin/booking/app/*`
- 웹/보안 어댑터: `application/src/main/java/org/mandarin/booking/adapter/{webapi,security}/*`

텍스트 다이어그램: [Controllers/Security/External] → adapter → app(ports, services) → domain

Expand All @@ -44,34 +50,40 @@

- Show (Aggregate Root): 제목, 감독, 장르, 상영시간, 개봉일, 등급, 줄거리, 포스터URL, 출연진 등. 팩토리/커맨드 기반 생성.
- Member (Aggregate Root): 닉네임, userId, email, passwordHash, 권한 목록. 비밀번호 해시 일치 검증.
- Hall (Entity): 상영관 이름, 좌석 배치(행/열), 총 좌석 수.

자세한 속성과 규칙: [docs/specs/domain.md](docs/specs/domain.md)

---

## 5. 기술 스택과 선택 근거

- 추후 작성 예정

선택 이유(요지)

- Hexagonal: 테스트 용이성과 변경 격리를 위해 계층 경계를 명확히. 또한, 추후 모듈화 or MSA 전환시 이점을 위해 애플리케이션 아키텍처를 영역에 따라 구분.
- Spring Security + JWT: 무상태(stateless) API 인증과 확장성.
- JPA + RDB(H2/MySQL): 표준 ORM과 빠른 테스트 사이클.
- Spring Modulith: 명확한 Bounded Context 경계 분리 및 추수 MSA 전환 대비

---

## 6. 개발 방식과 테스트 전략

- 테스트 주도 개발(TDD) 지향: 테스트 우선, 기능 추가 시 관련 스펙 테스트 동반.
- 테스트 정책 문서: [docs/specs/policy/test.md](docs/specs/policy/test.md)
- 통합 테스트: Spring Context 기동, 보안 필터/컨트롤러/JPA 연동을 포함한 경로 검증.
- 예시: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`
- 아키텍처 테스트: 레이어 규칙 준수 확인.
- 예시: `src/test/java/org/mandarin/booking/arch/HexagonalArchitectureTest.java`
- 모듈 구조 테스트: 모듈간 의존관계 테스트
- 예시: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java`

Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profiles, JUnit Platform, ByteBuddy javaagent).

---

## 7. 보안 개요

- 필터 기반 JWT 인증: `JwtFilter`가 Authorization `Bearer <token>`을 파싱해 SecurityContext 설정.
- 경로별 권한: `SecurityConfig`의 `@Order(1) apiChain`
- 차후 추가 작성
Expand All @@ -82,18 +94,20 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile
---

## 8. 데이터/환경 구성

- 프로필: `local`(기본), `test`, `prod(비어있음)`
- 근거: `src/main/resources/application.yml` 및 `application-*.yml`
- local: MySQL + JPA `ddl-auto: create`, JWT 시크릿/TTL 설정
- 근거: `application-local.yml`, Docker Compose: [compose.yaml](compose.yaml)
- 근거: `application-local.yml`, Docker Compose: [compose.yaml](application/src/main/resources/compose.yaml)
- test: H2 메모리 + MySQL 호환 모드 + JPA `ddl-auto: create`
- 근거: `application-test.yml`

민감정보는 운영 환경에서 환경변수로 주입하는 것을 권장합니다(로컬에 예시 값 존재).
민감정보는 운영 환경에서 환경변수로 주입하는 것을 권장합니다(로컬에 예시 값 존재).

---

## 9. API 문서

- 로그인: [docs/specs/api/login.md](docs/specs/api/login.md)
- 회원 가입: [docs/specs/api/member_register.md](docs/specs/api/member_register.md)
- 토큰 재발급: [docs/specs/api/reissue.md](docs/specs/api/reissue.md)
Expand All @@ -104,19 +118,24 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile
---

## 10. 프로젝트 상태 및 향후 계획

- CI/CD, 코드 포매터, 마이그레이션 도구(Flyway/Liquibase)는 현재 문서/설정 부재로 "확인 불가" 상태입니다.
- TODO/메모: [docs/devlog/*](docs/devlog), [docs/todo.md](docs/todo.md)
- 권장 향후 작업
- prod 프로필 구성과 비밀 주입 전략 수립
- CI 파이프라인(.github/workflows) 도입
- DB 마이그레이션 도구 채택 및 규약 수립
- 인증/인가 정책 문서 구체화: [docs/specs/policy/authentication.md](docs/specs/policy/authentication.md), [docs/specs/policy/authorization.md](docs/specs/policy/authorization.md)
- 인증/인가 정책 문서
구체화: [docs/specs/policy/authentication.md](docs/specs/policy/authentication.md), [docs/specs/policy/authorization.md](docs/specs/policy/authorization.md)

---

## 11. 버전/도구 근거 링크
- Spring Boot/Java/Gradle 버전: [build.gradle](build.gradle), [gradle-wrapper.properties](gradle/wrapper/gradle-wrapper.properties)

- Spring Boot/Java/Gradle
버전: [build.gradle](application/build.gradle), [gradle-wrapper.properties](gradle/wrapper/gradle-wrapper.properties)
- 애플리케이션 엔트리포인트: `src/main/java/org/mandarin/booking/BookingApplication.java`
- 보안 설정/필터: `src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java`, `src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java`
- 보안 설정/필터: `src/main/java/org/mandarin/booking/adapter/security/SecurityConfig.java`,
`src/main/java/org/mandarin/booking/adapter/security/JwtFilter.java`
- 아키텍처 규칙: [docs/specs/policy/application.md](docs/specs/policy/application.md)
- 테스트 정책: [docs/specs/policy/test.md](docs/specs/policy/test.md)
2 changes: 2 additions & 0 deletions application/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/main/resources/static/docs/
spy.log
143 changes: 143 additions & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
plugins {
id 'com.epages.restdocs-api-spec' version '0.18.2'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}

configurations {
byteBuddyAgent
asciidoctorExt
}

dependencyManagement {
imports {
mavenBom 'org.springframework.modulith:spring-modulith-bom:1.4.3'
}
}

dependencies {
api(project(':domain'))
api(project(':internal'))
api(project(':external'))
implementation project(':common')

implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
developmentOnly 'org.springframework.boot:spring-boot-devtools'

testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
byteBuddyAgent 'net.bytebuddy:byte-buddy-agent:1.17.6'

runtimeOnly 'org.springframework.modulith:spring-modulith-runtime'
implementation 'org.springframework.modulith:spring-modulith-starter-core'
implementation 'org.springframework.modulith:spring-modulith-starter-jpa'
implementation 'org.springframework.modulith:spring-modulith-events-api:1.4.3'
testImplementation 'org.springframework.modulith:spring-modulith-starter-test'

testImplementation 'io.rest-assured:rest-assured'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

def snippetsDir = layout.buildDirectory.dir("generated-snippets")
def generatedIndexDir = layout.buildDirectory.dir("tmp/asciidoc")
def asciidocOutputDir = layout.buildDirectory.dir("asciidoc")

tasks.register('prepareSnippetsDir') {
doLast {
snippetsDir.get().asFile.mkdirs()
}
}

tasks.named('test') {
jvmArgs "-javaagent:${configurations.byteBuddyAgent.singleFile}"
outputs.dir(snippetsDir)
}

tasks.register('generateIndexAdoc') {
dependsOn tasks.named('test'), tasks.named('prepareSnippetsDir')
outputs.file(generatedIndexDir.map { it.file("index.adoc") })
doLast {
def genDir = generatedIndexDir.get().asFile
def snip = snippetsDir.get().asFile

genDir.mkdirs()

def ops = snip.listFiles()?.findAll { it.isDirectory() } ?: []

def groupKey = { String name -> name.contains(' - ') ? name.substring(0, name.indexOf(' - ')) : name }

def grouped = ops.groupBy { dir -> groupKey(dir.name) }.sort { a, b -> a.key <=> b.key }

def includes = ['curl-request.adoc',
'http-request.adoc',
'http-response.adoc',
'path-parameters.adoc',
'query-parameters.adoc',
'request-parameters.adoc',
'request-fields.adoc',
'response-fields.adoc',
'links.adoc']

def sb = new StringBuilder()
sb << "= API 문서\n:toc: left\n:sectnums:\n\n"

grouped.each { grp, dirs ->
sb << "== ${grp}\n\n"

dirs.findAll { it.name == grp }.each { dir ->
includes.each { inc ->
def f = new File(dir, inc)
if (f.exists()) {
def caption = inc.replace('.adoc', '').replace('-', ' ')
sb << ".${caption}\ninclude::{snippets}/" << dir.name << "/" << inc << "[]\n\n"
}
}
}

dirs.findAll { it.name != grp }
.sort { it.name }
.each { dir ->
def methodTitle = dir.name.substring(grp.length() + " - ".length())
sb << "=== ${methodTitle}\n\n"
includes.each { inc ->
def f = new File(dir, inc)
if (f.exists()) {
def caption = inc.replace('.adoc', '').replace('-', ' ')
sb << ".${caption}\ninclude::{snippets}/" << dir.name << "/" << inc << "[]\n\n"
}
}
}
}

new File(genDir, "index.adoc").text = sb.toString()
}
}

tasks.named('asciidoctor') {
dependsOn tasks.named('prepareSnippetsDir'), tasks.named('generateIndexAdoc')
inputs.dir(snippetsDir)
configurations = [project.configurations.asciidoctorExt]
sourceDir generatedIndexDir.get().asFile
sources { include 'index.adoc' }
baseDirFollowsSourceFile()
outputDir asciidocOutputDir.get().asFile
attributes 'snippets': snippetsDir.get().asFile.absolutePath
}

tasks.named('build') {
dependsOn tasks.named('asciidoctor')
}

tasks.named('bootJar') {
dependsOn tasks.named('asciidoctor')
from(asciidocOutputDir.get()) { into 'static/docs' }
}

tasks.named('clean') {
doFirst {
delete asciidocOutputDir.get().asFile
delete generatedIndexDir.get().asFile
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.mandarin.booking.app;
package org.mandarin.booking.adapter.security;

import lombok.RequiredArgsConstructor;
import org.mandarin.booking.domain.member.SecurePasswordEncoder;
Expand All @@ -7,7 +7,7 @@

@Component
@RequiredArgsConstructor
public class BCryptSecurePasswordEncoder implements SecurePasswordEncoder {
class BCryptSecurePasswordEncoder implements SecurePasswordEncoder {
private final BCryptPasswordEncoder bCryptPasswordEncoder;

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package org.mandarin.booking.adapter.security;

import lombok.RequiredArgsConstructor;
import org.mandarin.booking.app.persist.MemberQueryRepository;
import org.mandarin.booking.domain.member.AuthException;
import org.mandarin.booking.AuthException;
import org.mandarin.booking.adapter.CustomMemberAuthenticationToken;
import org.mandarin.booking.app.member.MemberQueryRepository;
import org.mandarin.booking.domain.member.Member;
import org.mandarin.booking.domain.member.MemberDetails;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
class CustomAuthenticationProvider implements AuthenticationProvider {
private final MemberQueryRepository queryRepository;

@Override
Expand All @@ -34,7 +35,11 @@ public Authentication authenticate(Authentication authentication) throws Authent
}

private void specifyToken(CustomMemberAuthenticationToken token, Member member) {
MemberDetails details = MemberDetails.from(member);
var details = User.builder()
.username(member.getUserId())
.authorities(member.getParsedAuthorities())
.password(member.getPasswordHash())
.build();
token.setDetails(details);// set user details
token.setAuthenticated(true);
}
Expand Down
Loading