Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b431eef
update AGENTS.md and related documentation for clarity and accuracy
YeaChan05 Sep 29, 2025
5c51f49
add hall registration API documentation with request/response example…
YeaChan05 Oct 1, 2025
e010b46
refactor domain model to introduce Section entity and update relation…
YeaChan05 Oct 1, 2025
690e71b
implement hall registration API with request/response models and vali…
YeaChan05 Oct 1, 2025
b15ee01
add authorization for hall registration API and update tests for acce…
YeaChan05 Oct 1, 2025
2fb6e9e
update hall registration API tests to include checks for invalid toke…
YeaChan05 Oct 1, 2025
354231b
add validation for hall registration request and update tests for emp…
YeaChan05 Oct 1, 2025
02049e1
add validation for empty sections array in hall registration request …
YeaChan05 Oct 1, 2025
5176a6b
add validation for empty section name in hall registration request an…
YeaChan05 Oct 1, 2025
684e878
add validation for empty seats array in hall registration request and…
YeaChan05 Oct 1, 2025
3349d1d
add validation for rowNumber and seatNumber in hall registration requ…
YeaChan05 Oct 1, 2025
b4b2d25
add validation for duplicate rowNumber and seatNumber combinations in…
YeaChan05 Oct 1, 2025
a1cec21
add validation for duplicate section names in hall registration reque…
YeaChan05 Oct 1, 2025
6997479
refactor hall registration to use hallName instead of name and implem…
YeaChan05 Oct 2, 2025
aa6c2d7
add test for handling duplicate hall names returning INTERNAL_SERVER_…
YeaChan05 Oct 2, 2025
78f18b8
update section name validation to return BAD_REQUEST for duplicates a…
YeaChan05 Oct 2, 2025
0917f3e
add test for duplicate seat requests within the same section returnin…
YeaChan05 Oct 2, 2025
30f5c42
add test to ensure hall is not saved when sub-information is invalid
YeaChan05 Oct 2, 2025
64157c1
add validation to ensure at least one section and one seat are provid…
YeaChan05 Oct 2, 2025
a0c6b5d
add test to ensure BAD_REQUEST is returned when section seats are empty
YeaChan05 Oct 2, 2025
91f6432
rename hall existence checks for clarity and consistency
YeaChan05 Oct 2, 2025
927439c
add HallFixture for generating random hall names and refactor MemberS…
YeaChan05 Oct 2, 2025
7e224b0
enhance integration test utilities and add response wrapper tests
YeaChan05 Oct 2, 2025
34174e6
rename fastTestPasswordEncoder to testPasswordEncoder for clarity
YeaChan05 Oct 2, 2025
9af0374
refactor documentation for clarity on ports, adapters, and security c…
YeaChan05 Oct 2, 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
408 changes: 133 additions & 275 deletions AGENTS.md

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
- 패키지 구조 예
- 도메인: `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}/*`
- 웹 어댑터: `application/src/main/java/org/mandarin/booking/adapter/webapi/*`
- 보안 설정: `internal/src/main/java/org/mandarin/booking/adapter/*`,
`application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java`

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

Expand Down Expand Up @@ -74,7 +76,7 @@
- 테스트 주도 개발(TDD) 지향: 테스트 우선, 기능 추가 시 관련 스펙 테스트 동반.
- 테스트 정책 문서: [docs/specs/policy/test.md](docs/specs/policy/test.md)
- 통합 테스트: Spring Context 기동, 보안 필터/컨트롤러/JPA 연동을 포함한 경로 검증.
- 예시: `src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`
- 예시: `application/src/test/java/org/mandarin/booking/webapi/**/POST_specs.java`
- 모듈 구조 테스트: 모듈간 의존관계 테스트
- 예시: `application/src/test/java/org/mandarin/booking/arch/ModuleDependencyRulesTest.java`

Expand All @@ -89,14 +91,15 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile
- 차후 추가 작성
- 예외 처리: `CustomAuthenticationEntryPoint`, `CustomAccessDeniedHandler`

근거: `src/main/java/org/mandarin/booking/adapter/security/*`
근거: `internal/src/main/java/org/mandarin/booking/adapter/*`,
`application/src/main/java/org/mandarin/booking/adapter/security/ApplicationAuthorizationRequestMatcherConfigurer.java`

---

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

- 프로필: `local`(기본), `test`, `prod(비어있음)`
- 근거: `src/main/resources/application.yml` 및 `application-*.yml`
- 근거: `application/src/main/resources/application.yml` 및 `application-*.yml`
- local: MySQL + JPA `ddl-auto: create`, JWT 시크릿/TTL 설정
- 근거: `application-local.yml`, Docker Compose: [compose.yaml](application/src/main/resources/compose.yaml)
- test: H2 메모리 + MySQL 호환 모드 + JPA `ddl-auto: create`
Expand Down Expand Up @@ -133,9 +136,9 @@ Build/Test 구성 근거: `build.gradle`의 `tasks.named('test')` 설정(Profile
## 11. 버전/도구 근거 링크

- 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`
버전: [root build.gradle](build.gradle), [gradle-wrapper.properties](gradle/wrapper/gradle-wrapper.properties)
- 애플리케이션 엔트리포인트: `application/src/main/java/org/mandarin/booking/BookingApplication.java`
- 보안 설정/필터: `internal/src/main/java/org/mandarin/booking/adapter/SecurityConfig.java`,
`internal/src/main/java/org/mandarin/booking/adapter/JwtFilter.java`
- 아키텍처 규칙: [docs/specs/policy/application.md](docs/specs/policy/application.md)
- 테스트 정책: [docs/specs/policy/test.md](docs/specs/policy/test.md)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public void authorizeRequests(
.requestMatchers(HttpMethod.GET, "/api/show").permitAll()
.requestMatchers(HttpMethod.GET, "/api/show/*").permitAll()// 인증이 필요한 GET /show/* 엔드포인트 추가시 설정을 이 줄 아래에
.requestMatchers(HttpMethod.POST, "/api/show").hasAuthority("ROLE_ADMIN")
.requestMatchers(HttpMethod.POST, "/api/hall").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.mandarin.booking.adapter.webapi;

import jakarta.validation.Valid;
import org.mandarin.booking.app.hall.HallRegisterer;
import org.mandarin.booking.domain.hall.HallRegisterRequest;
import org.mandarin.booking.domain.hall.HallRegisterResponse;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/hall")
record HallController(HallRegisterer registerer) {
@PostMapping
HallRegisterResponse register(Authentication authentication,
@RequestBody @Valid HallRegisterRequest request) {
return registerer.register(authentication.getName(), request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mandarin.booking.app.hall;

import lombok.RequiredArgsConstructor;
import org.mandarin.booking.domain.hall.Hall;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
class HallCommandRepository {
private final HallRepository jpaRepository;


public Hall insert(Hall hall) {
return jpaRepository.save(hall);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ boolean existsById(Long hallId) {
return repository.existsById(hallId);
}

public Hall findById(Long hallId) {
boolean existsByHallName(String hallName) {
return repository.existsByHallName(hallName);
}

Hall findById(Long hallId) {
return repository.findById(hallId)
.orElseThrow(() -> new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다."));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mandarin.booking.app.hall;

import org.mandarin.booking.domain.hall.HallRegisterRequest;
import org.mandarin.booking.domain.hall.HallRegisterResponse;

public interface HallRegisterer {
HallRegisterResponse register(String userId, HallRegisterRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ interface HallRepository extends Repository<Hall, Long> {
boolean existsById(Long id);

Optional<Hall> findById(Long id);

boolean existsByHallName(String hallName);

Hall save(Hall hall);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,43 @@
import lombok.RequiredArgsConstructor;
import org.mandarin.booking.domain.hall.Hall;
import org.mandarin.booking.domain.hall.HallException;
import org.mandarin.booking.domain.hall.HallRegisterRequest;
import org.mandarin.booking.domain.hall.HallRegisterResponse;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
class HallService implements HallValidator, HallFetcher {
class HallService implements HallValidator, HallFetcher, HallRegisterer {
private final HallQueryRepository queryRepository;
private final HallCommandRepository commandRepository;

@Override
public void checkHallExist(Long hallId) {
public Hall fetch(Long hallId) {
return queryRepository.findById(hallId);
}

@Override
public void checkHallExistByHallId(Long hallId) {
if (!queryRepository.existsById(hallId)) {
throw new HallException("NOT_FOUND", "해당 공연장을 찾을 수 없습니다.");
}
}

@Override
public Hall fetch(Long hallId) {
return queryRepository.findById(hallId);
public void checkHallExistByHallName(String hallName) {
if (queryRepository.existsByHallName(hallName)) {
throw new HallException("INTERNAL_SERVER_ERROR", "이미 존재하는 공연장 이름입니다.");
}
}

@Override
public HallRegisterResponse register(String userId, HallRegisterRequest request) {
checkHallExistByHallName(request.hallName());

var hall = Hall.create(request.hallName(), userId);

var saved = commandRepository.insert(hall);

return new HallRegisterResponse(saved.getId());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.mandarin.booking.app.hall;

public interface HallValidator {
void checkHallExist(Long hallId);
void checkHallExistByHallId(Long hallId);

void checkHallExistByHallName(String hallName);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,42 @@
import lombok.RequiredArgsConstructor;
import org.mandarin.booking.domain.member.Member;
import org.mandarin.booking.domain.member.Member.MemberCreateCommand;
import org.mandarin.booking.domain.member.MemberException;
import org.mandarin.booking.domain.member.MemberRegisterRequest;
import org.mandarin.booking.domain.member.MemberRegisterResponse;
import org.mandarin.booking.domain.member.SecurePasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
class MemberService implements MemberRegisterer {
private final MemberRegisterValidator validator;
class MemberService implements MemberRegisterer, MemberValidator {
private final MemberCommandRepository command;
private final MemberQueryRepository queryRepository;
private final SecurePasswordEncoder securePasswordEncoder;

@Override
public MemberRegisterResponse register(MemberRegisterRequest request) {
validator.checkDuplicateUserId(request.userId());
validator.checkDuplicateEmail(request.email());
checkDuplicateUserId(request.userId());
checkDuplicateEmail(request.email());

var createCommand
= new MemberCreateCommand(request.nickName(), request.userId(), request.password(), request.email());
var newMember = Member.create(createCommand, securePasswordEncoder);
var savedMember = this.command.insert(newMember);
return MemberRegisterResponse.from(savedMember);
}

@Override
public void checkDuplicateEmail(String email) {
if (queryRepository.existsByEmail(email)) {
throw new MemberException("이미 존재하는 이메일입니다: " + email);
}
}

@Override
public void checkDuplicateUserId(String userId) {
if (queryRepository.existsByUserId(userId)) {
throw new MemberException("이미 존재하는 회원입니다: " + userId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.mandarin.booking.app.member;

public interface MemberValidator {
void checkDuplicateEmail(String email);

void checkDuplicateUserId(String userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ SliceView<ShowResponse> fetch(@Nullable Integer page,
show.type,
show.rating,
show.posterUrl,
select(hall.name)
select(hall.hallName)
.from(hall)
.where(hall.id.eq(show.hallId)),
show.performanceStartDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ShowService implements ShowRegisterer, ShowFetcher {
public ShowRegisterResponse register(ShowRegisterRequest request) {
var hallId = request.hallId();

hallValidator.checkHallExist(hallId);
hallValidator.checkHallExistByHallId(hallId);
var command = ShowCreateCommand.from(request);
var show = Show.create(hallId, command);

Expand Down Expand Up @@ -78,7 +78,7 @@ public ShowDetailResponse fetchShowDetail(Long showId) {
show.getPerformanceStartDate(),
show.getPerformanceEndDate(),
hall.getId(),
hall.getName(),
hall.getHallName(),
show.getScheduleResponses()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mandarin.booking.utils;

import java.util.UUID;

public class HallFixture {
public static String generateHallName() {
return UUID.randomUUID().toString().substring(0, 10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public String getValidRefreshToken() {
}

public String getAuthToken() {
var member = fixture.insertDummyMember();
var member = fixture.getOrCreateDefaultMember();
return "Bearer " + this.getUserToken(member.getUserId(), member.getNickName(), member.getAuthorities())
.accessToken();
}
Expand All @@ -62,4 +62,3 @@ public String getAuthToken(MemberAuthority... memberAuthority) {
.accessToken();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@TestConfiguration
public class TestConfig {
Expand All @@ -26,4 +27,20 @@ public IntegrationTestUtils integrationTestUtils(@Autowired TestFixture testFixt
public TestFixture testFixture(@Autowired SecurePasswordEncoder securePasswordEncoder) {
return new TestFixture(entityManager, securePasswordEncoder);
}

@Bean
@Primary
public SecurePasswordEncoder testPasswordEncoder() {
return new SecurePasswordEncoder() {
@Override
public String encode(String password) {
return "ENC:" + (password == null ? "" : password);
}

@Override
public boolean matches(String rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
};
}
}
Loading