Compare commits

..

17 Commits

Author SHA1 Message Date
b636ac926e test: 변경된 OrderService의 확정 로직 테스트 반영 2025-10-16 15:29:49 +09:00
385f98fb21 test: PaymentServiceTest에 결제 수단별 테스트 추가 2025-10-16 15:29:34 +09:00
9fe576af11 remove: OrderService 로직 변화로 사용하지 않게 된 클래스 제거 2025-10-16 15:29:20 +09:00
a83352c733 refactor: OrderService의 결제 & 예약 확정 로직에 이벤트 발행 추가 2025-10-16 15:28:56 +09:00
66bf68826b feat: 예약 확정 EventListener 및 테스트 2025-10-16 14:19:22 +09:00
cce59e522e refactor: 성능 테스트 환경 설정을 위해 사용되는 Repository 별도 분리 2025-10-16 14:00:28 +09:00
c3330e5652 refactor: 결제 확정 로직 변화에 따른 테스트 및 API 수정
- 기존의 결제 확정 API 및 테스트 제거(취소는 유지)
- PaymentWriter 제거
- 테스트 코드 반영
2025-10-16 13:55:13 +09:00
c1eb1aa2b4 refactor: PaymentService 내 결제 확정 로직에 이벤트 발행 추가 및 테스트 2025-10-16 13:36:27 +09:00
dbd2b9fb0c feat: PaymentClient에서 받은 외부 API 응답을 이벤트로 매핑하는 확장함수 추가 및 이전 커밋의 PaymentEventListenerTest 패키지 이동 2025-10-16 13:18:45 +09:00
e0e7902654 feat: 결제 정보를 저장하는 PaymentEventListener 및 테스트 추가 2025-10-16 13:15:52 +09:00
d0ee55be95 feat: ReservationConfirmEvent 객체 정의 2025-10-16 10:26:52 +09:00
f4b9d1207e feat: PaymentEvent 객체 정의 및 매핑 확장함수 추가 2025-10-16 10:26:34 +09:00
257fcb517d feat: PaymentDetail 도메인 객체 및 매핑 확장함수 추가 2025-10-16 10:26:18 +09:00
747245d9ac refactor: k6 스크립트 수정
- 초기 셋업 데이터 로드는 로컬에서 받아오도록 수정
- tag 추가로 ID별 구분이 아닌 API별 구분 집계
2025-10-16 10:25:31 +09:00
e6cfd7b68b fix: 빌드 스크립트 오타 수정 2025-10-16 10:24:34 +09:00
e3cfb6c78b refactor: 로컬 테스트를 위한 payment API 기본 경로 수정 2025-10-15 19:22:25 +09:00
f6ef6e21ec refactor: Dockerfile 단순화 및 별도 스크립트 추가 2025-10-15 19:22:07 +09:00
40 changed files with 1065 additions and 250 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# issue-pr-template
공통으로 사용하게 될 이슈, PR 템플릿 저장소

845
query.md Normal file
View File

@ -0,0 +1,845 @@
## Auth
**로그인**
```sql
-- 회원
-- 이메일로 회원 조회
SELECT
u.id
FROM
users u
WHERE
u.email = ?
LIMIT 1;
-- 연락처로 회원 조회
SELECT
u.id
FROM
users u
WHERE
u.phone = ?
LIMIT 1;
-- 회원 추가
INSERT INTO users (
created_at, created_by, email, name, password, phone, region_code,
status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 회원 상태 이력 추가
INSERT INTO user_status_history (
created_at, created_by, reason, status, updated_at, updated_by,
user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?
);
```
### Payment
**결제 승인 & 저장**
```sql
-- 결제 정보 추가
INSERT INTO payment ( approved_at, method, order_id, payment_key, requested_at, reservation_id, status, total_amount, type, id
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 결제 상세 정보 추가
INSERT INTO payment_detail ( payment_id, supplied_amount, vat, id
) VALUES ( ?, ?, ?, ?
);
-- 카드 결제 상세 정보 추가
INSERT INTO payment_card_detail ( amount, approval_number, card_number, card_type, easypay_discount_amount, easypay_provider_code, installment_plan_months, is_interest_free, issuer_code, owner_type, id
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**결제 취소**
SQL
```sql
-- 예약 ID로 결제 정보 조회
SELECT
p.id,
p.approved_at,
p.method,
p.order_id,
p.payment_key,
p.requested_at,
p.reservation_id,
p.status,
p.total_amount,
p.type
FROM
payment p
WHERE
p.reservation_id = ?;
-- 추가
-- 취소된 결제 정보 추가
INSERT INTO canceled_payment (
cancel_amount, cancel_reason, canceled_at, canceled_by,
card_discount_amount, easypay_discount_amount, payment_id,
requested_at, transfer_discount_amount, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
### Region
**모든 시/도 조회**
```sql
SELECT DISTINCT
r.sido_code,
r.sido_name
FROM
region r
ORDER BY
r.sido_name;
```
**시/군/구 조회**
```sql
SELECT
r.sigungu_code,
r.sigungu_name
FROM
region r
WHERE
r.sido_code = ?
GROUP BY
r.sigungu_code, r.sigungu_name
ORDER BY
r.sigungu_name;
```
**지역 코드 조회**
```sql
SELECT
r.code
FROM
region r
WHERE
r.sido_code = ? AND r.sigungu_code = ?;
```
### Reservation
**Pending 예약 생성**
```sql
-- schedule 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- theme 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 예약 추가
INSERT INTO reservation (
created_at, created_by, participant_count, requirement,
reserver_contact, reserver_name, schedule_id, status,
updated_at, updated_by, user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**확정**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 예약 확정
UPDATE
reservation
SET
participant_count = ?, requirement = ?, reserver_contact = ?,
reserver_name = ?, schedule_id = ?, status = ?,
updated_at = ?, updated_by = ?, user_id = ?
WHERE
id = ?;
-- Schedule 확정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**취소**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 취소 예약 추가
INSERT INTO canceled_reservation (
cancel_reason, canceled_at, canceled_by,
reservation_id, status, id
) VALUES (
?, ?, ?, ?, ?, ?
);
-- 예약 취소
UPDATE
reservation
SET
participant_count = ?, requirement = ?, reserver_contact = ?,
reserver_name = ?, schedule_id = ?, status = ?,
updated_at = ?, updated_by = ?, user_id = ?
WHERE
id = ?;
-- 일정 활성화
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**회원 예약 조회**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.user_id = ? AND r.status IN (?, ?);
-- 일정 조회 -> 각 예약별 1개씩(N개)
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id
JOIN store st ON st.id = s.store_id
WHERE
s.id = ?;
```
**예약 상세 조회**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 회원 연락처 정보 조회
SELECT
u.id, u.created_at, u.created_by, u.email, u.name, u.password,
u.phone, u.region_code, u.status, u.updated_at, u.updated_by
FROM
users u
WHERE
u.id = ?;
-- 결제 정보 조회
SELECT
p.id, p.approved_at, p.method, p.order_id, p.payment_key,
p.requested_at, p.reservation_id, p.status, p.total_amount, p.type
FROM
payment p
WHERE
p.reservation_id = ?;
-- 결제 상세 정보 조회
SELECT
pd.id,
CASE
WHEN pbt.id IS NOT NULL THEN 1 -- bank_transfer
WHEN pcd.id IS NOT NULL THEN 2 -- card
WHEN pep.id IS NOT NULL THEN 3 -- easypay
WHEN pd.id IS NOT NULL THEN 0 -- etc
END AS payment_type,
pd.payment_id, pd.supplied_amount, pd.vat,
pbt.bank_code, pbt.settlement_status,
pcd.amount, pcd.approval_number, pcd.card_number, pcd.card_type,
pcd.easypay_discount_amount, pcd.easypay_provider_code,
pcd.installment_plan_months, pcd.is_interest_free, pcd.issuer_code,
pcd.owner_type,
pep.amount AS easypay_amount,
pep.discount_amount AS easypay_discount_amount,
pep.easypay_provider_code AS easypay_provider
FROM
payment_detail pd
LEFT JOIN payment_bank_transfer_detail pbt ON pd.id = pbt.id
LEFT JOIN payment_card_detail pcd ON pd.id = pcd.id
LEFT JOIN payment_easypay_prepaid_detail pep ON pd.id = pep.id
WHERE
pd.payment_id = ?;
-- 취소 결제 정보 조회
SELECT
cp.id, cp.cancel_amount, cp.cancel_reason, cp.canceled_at,
cp.canceled_by, cp.card_discount_amount, cp.easypay_discount_amount,
cp.payment_id, cp.requested_at, cp.transfer_discount_amount
FROM
canceled_payment cp
WHERE
cp.payment_id = ?;
```
### Schedule
**날짜, 시간, 테마로 조회**
```sql
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id AND (? IS NULL OR t.id = ?)
JOIN store st ON st.id = s.store_id AND st.id = ?
WHERE
s.date = ?
```
**감사 정보 조회**
```sql
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 작업자 조회(createdBy, updatedBy)
SELECT
a.id, a.account, a.created_at, a.created_by, a.name, a.password,
a.permission_level, a.phone, a.store_id, a.type, a.updated_at,
a.updated_by
FROM
admin a
WHERE
a.id = ?;
```
**일정 생성**
```sql
-- 날짜, 시간, 테마가 같은 일정 존재 여부 확인
SELECT EXISTS (
SELECT 1
FROM schedule s
WHERE
s.store_id = ?
AND s.date = ?
AND s.theme_id = ?
AND s.time = ?
);
-- 시간이 겹치는 같은 날의 일정이 있는지 확인
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id AND (? IS NULL OR s.theme_id = ?)
JOIN store st ON st.id = s.store_id AND st.id = ?
WHERE
s.date = ?
-- 일정 추가
INSERT INTO schedule (
created_at, created_by, date, status, store_id,
theme_id, time, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**일정 수정**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 수정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**일정 삭제**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 삭제
DELETE FROM schedule
WHERE id = ?;
```
**상태 → HOLD 변경**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 수정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
### Store
**매장 상세 조회**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 지역 정보 조회
SELECT
r.code, r.sido_code, r.sido_name, r.sigungu_code, r.sigungu_name
FROM
region r
WHERE
r.code = ?;
-- 감사 정보 조회(createdBy, updatedBy)
SELECT
a.id, a.account, a.created_at, a.created_by, a.name, a.password,
a.permission_level, a.phone, a.store_id, a.type, a.updated_at,
a.updated_by
FROM
admin a
WHERE
a.id = ?;
```
**매장 등록**
```sql
-- 이름 중복 확인
SELECT s.id FROM store s WHERE s.name = ? LIMIT 1;
-- 연락처 중복 확인
SELECT s.id FROM store s WHERE s.contact = ? LIMIT 1;
-- 주소 중복 확인
SELECT s.id FROM store s WHERE s.address = ? LIMIT 1;
-- 사업자번호 중복 확인
SELECT s.id FROM store s WHERE s.business_reg_num = ? LIMIT 1;
-- 추가
INSERT INTO store (
address, business_reg_num, contact, created_at, created_by,
name, region_code, status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**매장 수정**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 수정
UPDATE
store
SET
address = ?, business_reg_num = ?, contact = ?, name = ?,
region_code = ?, status = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**비활성화(status = DISABLE)**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 수정
UPDATE
store
SET
address = ?, business_reg_num = ?, contact = ?, name = ?,
region_code = ?, status = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**모든 매장 조회**
```sql
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.status = 'ACTIVE'
AND (? IS NULL OR s.region_code LIKE ?);
```
**개별 매장 상세 조회**
```sql
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
```
### Theme
**생성**
```sql
-- 이름으로 조회
SELECT
t.id
FROM
theme t
WHERE
t.name = ?
LIMIT 1;
-- 추가
INSERT INTO theme (
available_minutes, created_at, created_by, description, difficulty,
expected_minutes_from, expected_minutes_to, is_active, max_participants,
min_participants, name, price, thumbnail_url, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**Active인 모든 테마 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.is_active = TRUE;
```
**테마 목록 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t;
```
**감사 정보 포함 개별 테마 상세 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
```
**개별 테마 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
```
**삭제**
```sql
-- 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 삭제
DELETE FROM theme WHERE id = ?;
```
**수정**
```sql
-- 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 수정
UPDATE
theme
SET
available_minutes = ?, description = ?, difficulty = ?,
expected_minutes_from = ?, expected_minutes_to = ?, is_active = ?,
max_participants = ?, min_participants = ?, name = ?, price = ?,
thumbnail_url = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**인기 테마 조회**
```sql
SELECT
t.id, t.name, t.description, t.difficulty, t.thumbnail_url, t.price,
t.min_participants, t.max_participants,
t.available_minutes, t.expected_minutes_from, t.expected_minutes_to
FROM
theme t
JOIN (
SELECT
s.theme_id, count(*) as reservation_count
FROM
schedule s
JOIN
reservation r ON s.id = r.schedule_id AND r.status = 'CONFIRMED'
WHERE
s.status = 'RESERVED'
AND (s.date BETWEEN :startFrom AND :endAt)
GROUP BY
s.theme_id
ORDER BY
reservation_count desc
LIMIT :count
) ranked_themes ON t.id = ranked_themes.theme_id
```
### User
**회원가입**
```sql
-- 이메일 중복 확인
SELECT
u.id
FROM
users u
WHERE
u.email = ?
LIMIT 1;
-- 연락처 중복 확인
SELECT
u.id
FROM
users u
WHERE
u.phone = ?
LIMIT 1;
-- 추가
INSERT INTO users (
created_at, created_by, email, name, password, phone, region_code,
status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 상태 변경 이력 추가
INSERT INTO user_status_history (
created_at, created_by, reason, status, updated_at, updated_by,
user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?
);
```
**연락처 정보 조회**
```sql
SELECT
u.id, u.created_at, u.created_by, u.email, u.name, u.password,
u.phone, u.region_code, u.status, u.updated_at, u.updated_by
FROM
users u
WHERE
u.id = ?;
```

View File

@ -8,10 +8,6 @@ dependencies {
// API docs // API docs
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
// Cache
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine")
// DB // DB
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j") runtimeOnly("com.mysql:mysql-connector-j")

View File

@ -3,14 +3,8 @@ package com.sangdol.roomescape
import org.springframework.boot.Banner import org.springframework.boot.Banner
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import java.util.* import java.util.*
@EnableAsync
@EnableCaching
@EnableScheduling
@SpringBootApplication( @SpringBootApplication(
scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"] scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"]
) )

View File

@ -20,7 +20,7 @@ class AdminService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findCredentialsByAccount(account: String): AdminLoginCredentials { fun findCredentialsByAccount(account: String): AdminLoginCredentials {
log.debug { "[findCredentialsByAccount] 관리자 조회 시작: account=${account}" } log.info { "[findCredentialsByAccount] 관리자 조회 시작: account=${account}" }
return adminRepository.findByAccount(account) return adminRepository.findByAccount(account)
?.let { ?.let {
@ -28,14 +28,14 @@ class AdminService(
it.toCredentials() it.toCredentials()
} }
?: run { ?: run {
log.debug { "[findCredentialsByAccount] 관리자 조회 실패: account=${account}" } log.info { "[findCredentialsByAccount] 관리자 조회 실패: account=${account}" }
throw AdminException(AdminErrorCode.ADMIN_NOT_FOUND) throw AdminException(AdminErrorCode.ADMIN_NOT_FOUND)
} }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findOperatorOrUnknown(id: Long): Auditor { fun findOperatorOrUnknown(id: Long): Auditor {
log.debug { "[findOperatorById] 작업자 정보 조회 시작: id=${id}" } log.info { "[findOperatorById] 작업자 정보 조회 시작: id=${id}" }
return adminRepository.findByIdOrNull(id)?.let { admin -> return adminRepository.findByIdOrNull(id)?.let { admin ->
Auditor(admin.id, admin.name).also { Auditor(admin.id, admin.name).also {

View File

@ -33,7 +33,7 @@ class AuthService(
request: LoginRequest, request: LoginRequest,
context: LoginContext context: LoginContext
): LoginSuccessResponse { ): LoginSuccessResponse {
log.debug { "[login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" } log.info { "[login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" }
val (credentials, extraClaims) = getCredentials(request) val (credentials, extraClaims) = getCredentials(request)
val event = LoginHistoryEvent( val event = LoginHistoryEvent(
@ -56,7 +56,10 @@ class AuthService(
} catch (e: Exception) { } catch (e: Exception) {
eventPublisher.publishEvent(event.onFailure()) eventPublisher.publishEvent(event.onFailure())
when (e) { when (e) {
is AuthException -> { throw e } is AuthException -> {
log.info { "[login] 로그인 실패: account = ${request.account}" }
throw e
}
else -> { else -> {
log.warn { "[login] 로그인 실패: message=${e.message} account = ${request.account}" } log.warn { "[login] 로그인 실패: message=${e.message} account = ${request.account}" }
@ -71,7 +74,7 @@ class AuthService(
credentials: LoginCredentials credentials: LoginCredentials
) { ) {
if (credentials.password != request.password) { if (credentials.password != request.password) {
log.debug { "[login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" } log.info { "[login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" }
throw AuthException(AuthErrorCode.LOGIN_FAILED) throw AuthException(AuthErrorCode.LOGIN_FAILED)
} }
} }

View File

@ -11,6 +11,8 @@ import jakarta.annotation.PreDestroy
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.event.EventListener import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -19,6 +21,8 @@ import java.util.concurrent.TimeUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Component @Component
@EnableAsync
@EnableScheduling
class LoginHistoryEventListener( class LoginHistoryEventListener(
private val idGenerator: IDGenerator, private val idGenerator: IDGenerator,
private val loginHistoryRepository: LoginHistoryRepository, private val loginHistoryRepository: LoginHistoryRepository,
@ -31,7 +35,7 @@ class LoginHistoryEventListener(
@Async @Async
@EventListener(classes = [LoginHistoryEvent::class]) @EventListener(classes = [LoginHistoryEvent::class])
fun onLoginCompleted(event: LoginHistoryEvent) { fun onLoginCompleted(event: LoginHistoryEvent) {
log.debug { "[onLoginCompleted] 로그인 이력 저장 이벤트 수신: id=${event.id}, type=${event.type}" } log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 수신: id=${event.id}, type=${event.type}" }
queue.add(event.toEntity(idGenerator.create())).also { queue.add(event.toEntity(idGenerator.create())).also {
log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 큐 저장 완료: id=${event.id}, type=${event.type}" } log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 큐 저장 완료: id=${event.id}, type=${event.type}" }
@ -44,10 +48,10 @@ class LoginHistoryEventListener(
@Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS) @Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS)
fun flushScheduled() { fun flushScheduled() {
log.debug { "[flushScheduled] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" } log.info { "[flushScheduled] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" }
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug { "[flushScheduled] 큐에 있는 로그인 이력이 없음." } log.info { "[flushScheduled] 큐에 있는 로그인 이력이 없음." }
return return
} }
flush() flush()
@ -56,7 +60,7 @@ class LoginHistoryEventListener(
@PreDestroy @PreDestroy
fun flushAll() { fun flushAll() {
log.debug { "[flushAll] 애플리케이션 종료. 큐에 있는 모든 이력 저장 시작: size=${queue.size}" } log.info { "[flushAll] 애플리케이션 종료. 큐에 있는 모든 이력 저장 시작: size=${queue.size}" }
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
flush() flush()
} }
@ -64,10 +68,10 @@ class LoginHistoryEventListener(
} }
private fun flush() { private fun flush() {
log.debug { "[flush] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" } log.info { "[flush] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" }
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.debug { "[flush] 큐에 있는 로그인 이력이 없음." } log.info { "[flush] 큐에 있는 로그인 이력이 없음." }
return; return;
} }

View File

@ -50,7 +50,7 @@ class JwtUtils(
val claims = extractAllClaims(token) val claims = extractAllClaims(token)
return claims.subject ?: run { return claims.subject ?: run {
log.debug { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" } log.info { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" }
throw AuthException(AuthErrorCode.INVALID_TOKEN) throw AuthException(AuthErrorCode.INVALID_TOKEN)
} }
} }

View File

@ -6,7 +6,7 @@ import com.sangdol.roomescape.auth.exception.AuthException
import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils import com.sangdol.roomescape.auth.infrastructure.jwt.JwtUtils
import com.sangdol.roomescape.auth.web.support.User import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.auth.web.support.accessToken import com.sangdol.roomescape.auth.web.support.accessToken
import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.user.business.UserService
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
@ -22,6 +22,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Component @Component
class UserContextResolver( class UserContextResolver(
private val jwtUtils: JwtUtils, private val jwtUtils: JwtUtils,
private val userService: UserService,
) : HandlerMethodArgumentResolver { ) : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean { override fun supportsParameter(parameter: MethodParameter): Boolean {
@ -42,7 +43,7 @@ class UserContextResolver(
MdcPrincipalIdUtil.set(it) MdcPrincipalIdUtil.set(it)
}.toLong() }.toLong()
return CurrentUserContext(id = id) return userService.findContextById(id)
} catch (e: Exception) { } catch (e: Exception) {
log.info { "[UserContextResolver] 회원 조회 실패. message=${e.message}" } log.info { "[UserContextResolver] 회원 조회 실패. message=${e.message}" }
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)

View File

@ -1,5 +1,6 @@
package com.sangdol.roomescape.common.types package com.sangdol.roomescape.common.types
data class CurrentUserContext( data class CurrentUserContext(
val id: Long val id: Long,
val name: String,
) )

View File

@ -32,7 +32,7 @@ class OrderService(
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) { fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
val paymentKey = paymentConfirmRequest.paymentKey val paymentKey = paymentConfirmRequest.paymentKey
log.debug { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" } log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
try { try {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
validateCanConfirm(reservationId) validateCanConfirm(reservationId)
@ -42,7 +42,7 @@ class OrderService(
paymentService.requestConfirm(reservationId, paymentConfirmRequest) paymentService.requestConfirm(reservationId, paymentConfirmRequest)
eventPublisher.publishEvent(ReservationConfirmEvent(reservationId)) eventPublisher.publishEvent(ReservationConfirmEvent(reservationId))
log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료: reservationId=${reservationId}, paymentKey=${paymentKey}" } log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료" }
} catch (e: Exception) { } catch (e: Exception) {
val errorCode: ErrorCode = if (e is RoomescapeException) { val errorCode: ErrorCode = if (e is RoomescapeException) {
e.errorCode e.errorCode
@ -55,7 +55,7 @@ class OrderService(
} }
private fun validateCanConfirm(reservationId: Long) { private fun validateCanConfirm(reservationId: Long) {
log.debug { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" } log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId) val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId) val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)

View File

@ -27,12 +27,15 @@ class OrderValidator {
private fun validateReservationStatus(reservation: ReservationStateResponse) { private fun validateReservationStatus(reservation: ReservationStateResponse) {
when (reservation.status) { when (reservation.status) {
ReservationStatus.CONFIRMED -> { ReservationStatus.CONFIRMED -> {
log.info { "[validateCanConfirm] 이미 확정된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED) throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED)
} }
ReservationStatus.EXPIRED -> { ReservationStatus.EXPIRED -> {
log.info { "[validateCanConfirm] 만료된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.EXPIRED_RESERVATION) throw OrderException(OrderErrorCode.EXPIRED_RESERVATION)
} }
ReservationStatus.CANCELED -> { ReservationStatus.CANCELED -> {
log.info { "[validateCanConfirm] 취소된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.CANCELED_RESERVATION) throw OrderException(OrderErrorCode.CANCELED_RESERVATION)
} }
else -> {} else -> {}
@ -41,14 +44,14 @@ class OrderValidator {
private fun validateScheduleStatus(schedule: ScheduleStateResponse) { private fun validateScheduleStatus(schedule: ScheduleStateResponse) {
if (schedule.status != ScheduleStatus.HOLD) { if (schedule.status != ScheduleStatus.HOLD) {
log.debug { "[validateScheduleStatus] 일정 상태 오류: status=${schedule.status}" } log.info { "[validateScheduleStatus] 일정 상태 오류: status=${schedule.status}" }
throw OrderException(OrderErrorCode.EXPIRED_RESERVATION) throw OrderException(OrderErrorCode.EXPIRED_RESERVATION)
} }
val scheduleDateTime = LocalDateTime.of(schedule.date, schedule.startFrom) val scheduleDateTime = LocalDateTime.of(schedule.date, schedule.startFrom)
val nowDateTime = KoreaDateTime.now() val nowDateTime = KoreaDateTime.now()
if (scheduleDateTime.isBefore(nowDateTime)) { if (scheduleDateTime.isBefore(nowDateTime)) {
log.debug { "[validateScheduleStatus] 과거 시간인 일정으로 인한 실패: scheduleDateTime=${scheduleDateTime}(KST), now=${nowDateTime}(KST)" } log.info { "[validateScheduleStatus] 과거 시간인 일정으로 인한 실패: scheduleDateTime=${scheduleDateTime}(KST), now=${nowDateTime}(KST)" }
throw OrderException(OrderErrorCode.PAST_SCHEDULE) throw OrderException(OrderErrorCode.PAST_SCHEDULE)
} }
} }

View File

@ -31,7 +31,7 @@ class PaymentService(
private val eventPublisher: ApplicationEventPublisher private val eventPublisher: ApplicationEventPublisher
) { ) {
fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse { fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse {
log.debug { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" } log.info { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" }
try { try {
return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also { return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also {
eventPublisher.publishEvent(it.toEvent(reservationId)) eventPublisher.publishEvent(it.toEvent(reservationId))
@ -90,7 +90,7 @@ class PaymentService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findDetailByReservationId(reservationId: Long): PaymentResponse? { fun findDetailByReservationId(reservationId: Long): PaymentResponse? {
log.debug { "[findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" } log.info { "[findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" }
val payment: PaymentEntity? = findByReservationIdOrNull(reservationId) val payment: PaymentEntity? = findByReservationIdOrNull(reservationId)
val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) } val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) }
@ -99,13 +99,11 @@ class PaymentService(
return payment?.toResponse( return payment?.toResponse(
detail = paymentDetail?.toResponse(), detail = paymentDetail?.toResponse(),
cancel = cancelDetail?.toResponse() cancel = cancelDetail?.toResponse()
).also { )
log.info { "[findDetailByReservationId] 예약 결제 정보 조회 완료: reservationId=$reservationId" }
}
} }
private fun findByReservationIdOrThrow(reservationId: Long): PaymentEntity { private fun findByReservationIdOrThrow(reservationId: Long): PaymentEntity {
log.debug { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" }
return paymentRepository.findByReservationId(reservationId) return paymentRepository.findByReservationId(reservationId)
?.also { log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } } ?.also { log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } }
@ -116,7 +114,7 @@ class PaymentService(
} }
private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? { private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? {
log.debug { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" }
return paymentRepository.findByReservationId(reservationId) return paymentRepository.findByReservationId(reservationId)
.also { .also {
@ -129,7 +127,7 @@ class PaymentService(
} }
private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? { private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? {
log.debug { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" } log.info { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" }
return paymentDetailRepository.findByPaymentId(paymentId).also { return paymentDetailRepository.findByPaymentId(paymentId).also {
if (it != null) { if (it != null) {
@ -141,7 +139,7 @@ class PaymentService(
} }
private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? { private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? {
log.debug { "[findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" } log.info { "[findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" }
return canceledPaymentRepository.findByPaymentId(paymentId).also { return canceledPaymentRepository.findByPaymentId(paymentId).also {
if (it == null) { if (it == null) {

View File

@ -29,16 +29,20 @@ class PaymentEventListener(
fun handlePaymentEvent(event: PaymentEvent) { fun handlePaymentEvent(event: PaymentEvent) {
val reservationId = event.reservationId val reservationId = event.reservationId
log.debug { "[handlePaymentEvent] 결제 정보 저장 이벤트 수신: reservationId=${reservationId}, paymentKey=${event.paymentKey}" } log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 수신: reservationId=${reservationId}, paymentKey=${event.paymentKey}" }
val paymentId = idGenerator.create() val paymentId = idGenerator.create()
val paymentEntity: PaymentEntity = event.toEntity(paymentId) val paymentEntity: PaymentEntity = event.toEntity(paymentId)
paymentRepository.save(paymentEntity) paymentRepository.save(paymentEntity).also {
log.info { "[handlePaymentEvent] 결제 정보 저장 완료: paymentId=${paymentId}" }
}
val paymentDetailId = idGenerator.create() val paymentDetailId = idGenerator.create()
val paymentDetailEntity: PaymentDetailEntity = event.toDetailEntity(id = paymentDetailId, paymentId = paymentId) val paymentDetailEntity: PaymentDetailEntity = event.toDetailEntity(id = paymentDetailId, paymentId = paymentId)
paymentDetailRepository.save(paymentDetailEntity) paymentDetailRepository.save(paymentDetailEntity).also {
log.info { "[handlePaymentEvent] 결제 상세 저장 완료: paymentDetailId=${paymentDetailId}" }
}
log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 처리 완료: reservationId=${reservationId}, paymentId=${paymentId}, paymentDetailId=${paymentDetailId}" } log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 처리 완료" }
} }
} }

View File

@ -33,7 +33,7 @@ class TosspayClient(
amount: Int, amount: Int,
): PaymentGatewayResponse { ): PaymentGatewayResponse {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
log.debug { "[TosspayClient.confirm] 결제 승인 요청: paymentKey=$paymentKey, orderId=$orderId, amount=$amount" } log.info { "[TosspayClient.confirm] 결제 승인 요청: paymentKey=$paymentKey, orderId=$orderId, amount=$amount" }
return confirmClient.request(paymentKey, orderId, amount) return confirmClient.request(paymentKey, orderId, amount)
.also { .also {
@ -47,7 +47,7 @@ class TosspayClient(
cancelReason: String cancelReason: String
): PaymentGatewayCancelResponse { ): PaymentGatewayCancelResponse {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
log.debug { "[TosspayClient.cancel] 결제 취소 요청: paymentKey=$paymentKey, amount=$amount, cancelReason=$cancelReason" } log.info { "[TosspayClient.cancel] 결제 취소 요청: paymentKey=$paymentKey, amount=$amount, cancelReason=$cancelReason" }
return cancelClient.request(paymentKey, amount, cancelReason).also { return cancelClient.request(paymentKey, amount, cancelReason).also {
log.info { "[TosspayClient.cancel] 결제 취소 완료: duration_ms=${System.currentTimeMillis() - startTime}ms, paymentKey=$paymentKey" } log.info { "[TosspayClient.cancel] 결제 취소 완료: duration_ms=${System.currentTimeMillis() - startTime}ms, paymentKey=$paymentKey" }

View File

@ -17,7 +17,7 @@ class RegionService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun readAllSido(): SidoListResponse { fun readAllSido(): SidoListResponse {
log.debug { "[readAllSido] 모든 시/도 조회 시작" } log.info { "[readAllSido] 모든 시/도 조회 시작" }
val result: List<Pair<String, String>> = regionRepository.readAllSido() val result: List<Pair<String, String>> = regionRepository.readAllSido()
if (result.isEmpty()) { if (result.isEmpty()) {
@ -32,7 +32,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findSigunguBySido(sidoCode: String): SigunguListResponse { fun findSigunguBySido(sidoCode: String): SigunguListResponse {
log.debug { "[findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" } log.info { "[findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" }
val result: List<Pair<String, String>> = regionRepository.findAllSigunguBySido(sidoCode) val result: List<Pair<String, String>> = regionRepository.findAllSigunguBySido(sidoCode)
if (result.isEmpty()) { if (result.isEmpty()) {
@ -47,7 +47,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findRegionCode(sidoCode: String, sigunguCode: String): RegionCodeResponse { fun findRegionCode(sidoCode: String, sigunguCode: String): RegionCodeResponse {
log.debug { "[findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } log.info { "[findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
return regionRepository.findRegionCode(sidoCode, sigunguCode)?.let { return regionRepository.findRegionCode(sidoCode, sigunguCode)?.let {
log.info { "[findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } log.info { "[findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
@ -60,7 +60,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findRegionInfo(regionCode: String): RegionInfoResponse { fun findRegionInfo(regionCode: String): RegionInfoResponse {
log.debug { "[findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" } log.info { "[findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" }
return regionRepository.findByCode(regionCode)?.let { return regionRepository.findByCode(regionCode)?.let {
log.info { "[findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" } log.info { "[findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" }

View File

@ -45,7 +45,7 @@ class ReservationService(
user: CurrentUserContext, user: CurrentUserContext,
request: PendingReservationCreateRequest request: PendingReservationCreateRequest
): PendingReservationCreateResponse { ): PendingReservationCreateResponse {
log.debug { "[createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } log.info { "[createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" }
run { run {
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(request.scheduleId) val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(request.scheduleId)
@ -64,7 +64,7 @@ class ReservationService(
@Transactional @Transactional
fun confirmReservation(id: Long) { fun confirmReservation(id: Long) {
log.debug { "[confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } log.info { "[confirmReservation] Pending 예약 확정 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id) val reservation: ReservationEntity = findOrThrow(id)
run { run {
@ -81,7 +81,7 @@ class ReservationService(
@Transactional @Transactional
fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) { fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) {
log.debug { "[cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" } log.info { "[cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" }
val reservation: ReservationEntity = findOrThrow(reservationId) val reservation: ReservationEntity = findOrThrow(reservationId)
@ -100,7 +100,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findAllUserReservationOverview(user: CurrentUserContext): ReservationOverviewListResponse { fun findAllUserReservationOverview(user: CurrentUserContext): ReservationOverviewListResponse {
log.debug { "[findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" } log.info { "[findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" }
val reservations: List<ReservationEntity> = reservationRepository.findAllByUserIdAndStatusIsIn( val reservations: List<ReservationEntity> = reservationRepository.findAllByUserIdAndStatusIsIn(
userId = user.id, userId = user.id,
@ -125,7 +125,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findDetailById(id: Long): ReservationAdditionalResponse { fun findDetailById(id: Long): ReservationAdditionalResponse {
log.debug { "[findDetailById] 예약 상세 조회 시작: reservationId=${id}" } log.info { "[findDetailById] 예약 상세 조회 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id) val reservation: ReservationEntity = findOrThrow(id)
val user: UserContactResponse = userService.findContactById(reservation.userId) val user: UserContactResponse = userService.findContactById(reservation.userId)
@ -141,7 +141,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStatusWithLock(id: Long): ReservationStateResponse { fun findStatusWithLock(id: Long): ReservationStateResponse {
log.debug { "[findStatusWithLock] 예약 LOCK + 상태 조회 시작: reservationId=${id}" } log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdForUpdate(id)?.let { return reservationRepository.findByIdForUpdate(id)?.let {
log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 완료: reservationId=${id}" } log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 완료: reservationId=${id}" }
@ -154,7 +154,7 @@ class ReservationService(
@Transactional @Transactional
fun markInProgress(reservationId: Long) { fun markInProgress(reservationId: Long) {
log.debug { "[markInProgress] 예약 상태 ${ReservationStatus.PAYMENT_IN_PROGRESS} 변경 시작." } log.info { "[markInProgress] 예약 상태 ${ReservationStatus.PAYMENT_IN_PROGRESS} 변경 시작." }
findOrThrow(reservationId).apply { findOrThrow(reservationId).apply {
this.status = ReservationStatus.PAYMENT_IN_PROGRESS this.status = ReservationStatus.PAYMENT_IN_PROGRESS
@ -164,7 +164,7 @@ class ReservationService(
} }
private fun findOrThrow(id: Long): ReservationEntity { private fun findOrThrow(id: Long): ReservationEntity {
log.debug { "[findOrThrow] 예약 조회 시작: reservationId=${id}" } log.info { "[findOrThrow] 예약 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdOrNull(id) return reservationRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 예약 조회 완료: reservationId=${id}" } } ?.also { log.info { "[findOrThrow] 예약 조회 완료: reservationId=${id}" } }

View File

@ -1,15 +1,17 @@
package com.sangdol.roomescape.reservation.business package com.sangdol.roomescape.reservation.business
import com.sangdol.common.utils.KoreaDateTime import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest import com.sangdol.common.utils.toKoreaDateTime
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
import com.sangdol.roomescape.reservation.exception.ReservationException import com.sangdol.roomescape.reservation.exception.ReservationException
import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -22,7 +22,7 @@ class ReservationEventListener(
fun handleReservationConfirmEvent(event: ReservationConfirmEvent) { fun handleReservationConfirmEvent(event: ReservationConfirmEvent) {
val reservationId = event.reservationId val reservationId = event.reservationId
log.debug { "[handleReservationConfirmEvent] 예약 확정 이벤트 수신: reservationId=${reservationId}" } log.info { "[handleReservationConfirmEvent] 예약 확정 이벤트 수신: reservationId=${reservationId}" }
val modifiedRows = reservationRepository.confirmReservation(Instant.now(), reservationId) val modifiedRows = reservationRepository.confirmReservation(Instant.now(), reservationId)
if (modifiedRows == 0) { if (modifiedRows == 0) {

View File

@ -5,6 +5,7 @@ import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleReposi
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -14,6 +15,7 @@ import java.util.concurrent.TimeUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Component @Component
@EnableScheduling
class IncompletedReservationScheduler( class IncompletedReservationScheduler(
private val scheduleRepository: ScheduleRepository, private val scheduleRepository: ScheduleRepository,
private val reservationRepository: ReservationRepository private val reservationRepository: ReservationRepository
@ -22,10 +24,10 @@ class IncompletedReservationScheduler(
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
@Transactional @Transactional
fun processExpiredHoldSchedule() { fun processExpiredHoldSchedule() {
log.debug { "[processExpiredHoldSchedule] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" } log.info { "[processExpiredHoldSchedule] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" }
val targets: List<Long> = scheduleRepository.findAllExpiredHeldSchedules(Instant.now()).also { val targets: List<Long> = scheduleRepository.findAllExpiredHeldSchedules(Instant.now()).also {
log.debug { "[processExpiredHoldSchedule] ${it.size} 개의 일정 조회 완료" } log.info { "[processExpiredHoldSchedule] ${it.size} 개의 일정 조회 완료" }
} }
scheduleRepository.releaseHeldSchedules(targets).also { scheduleRepository.releaseHeldSchedules(targets).also {
@ -36,7 +38,7 @@ class IncompletedReservationScheduler(
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
@Transactional @Transactional
fun processExpiredReservation() { fun processExpiredReservation() {
log.debug { "[processExpiredReservation] 결제되지 않은 예약 만료 처리 시작" } log.info { "[processExpiredReservation] 결제되지 않은 예약 만료 처리 시작" }
val targets: List<Long> = reservationRepository.findAllExpiredReservation().also { val targets: List<Long> = reservationRepository.findAllExpiredReservation().also {
log.info { "[processExpiredReservation] ${it.size} 개의 예약 조회 완료" } log.info { "[processExpiredReservation] ${it.size} 개의 예약 조회 완료" }

View File

@ -34,7 +34,7 @@ class AdminScheduleService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse { fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse {
log.debug { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } log.info { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" }
val searchDate = date ?: KoreaDate.today() val searchDate = date ?: KoreaDate.today()
@ -44,12 +44,14 @@ class AdminScheduleService(
.sortedBy { it.time } .sortedBy { it.time }
return schedules.toAdminSummaryResponse() return schedules.toAdminSummaryResponse()
.also { log.info { "[searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" } } .also {
log.info { "[searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" }
}
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findScheduleAudit(id: Long): AuditingInfo { fun findScheduleAudit(id: Long): AuditingInfo {
log.debug { "[findDetail] 일정 감사 정보 조회 시작: id=$id" } log.info { "[findDetail] 일정 감사 정보 조회 시작: id=$id" }
val schedule: ScheduleEntity = findOrThrow(id) val schedule: ScheduleEntity = findOrThrow(id)
@ -62,7 +64,7 @@ class AdminScheduleService(
@Transactional @Transactional
fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse { fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse {
log.debug { "[createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" } log.info { "[createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" }
scheduleValidator.validateCanCreate(storeId, request) scheduleValidator.validateCanCreate(storeId, request)
@ -77,12 +79,14 @@ class AdminScheduleService(
} }
return ScheduleCreateResponse(schedule.id) return ScheduleCreateResponse(schedule.id)
.also { log.info { "[createSchedule] 일정 생성 완료: id=${it.id}" } } .also {
log.info { "[createSchedule] 일정 생성 완료: id=${it.id}" }
}
} }
@Transactional @Transactional
fun updateSchedule(id: Long, request: ScheduleUpdateRequest) { fun updateSchedule(id: Long, request: ScheduleUpdateRequest) {
log.debug { "[updateSchedule] 일정 수정 시작: id=$id, request=${request}" } log.info { "[updateSchedule] 일정 수정 시작: id=$id, request=${request}" }
if (request.isAllParamsNull()) { if (request.isAllParamsNull()) {
log.info { "[updateSchedule] 일정 변경 사항 없음: id=$id" } log.info { "[updateSchedule] 일정 변경 사항 없음: id=$id" }
@ -100,7 +104,7 @@ class AdminScheduleService(
@Transactional @Transactional
fun deleteSchedule(id: Long) { fun deleteSchedule(id: Long) {
log.debug { "[deleteSchedule] 일정 삭제 시작: id=$id" } log.info { "[deleteSchedule] 일정 삭제 시작: id=$id" }
val schedule: ScheduleEntity = findOrThrow(id).also { val schedule: ScheduleEntity = findOrThrow(id).also {
scheduleValidator.validateCanDelete(it) scheduleValidator.validateCanDelete(it)
@ -112,7 +116,7 @@ class AdminScheduleService(
} }
private fun findOrThrow(id: Long): ScheduleEntity { private fun findOrThrow(id: Long): ScheduleEntity {
log.debug { "[findOrThrow] 일정 조회 시작: id=$id" } log.info { "[findOrThrow] 일정 조회 시작: id=$id" }
return scheduleRepository.findByIdOrNull(id) return scheduleRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } } ?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } }

View File

@ -30,12 +30,13 @@ class ScheduleService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse { fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse {
log.debug { "[getStoreScheduleByDate] 매장 일정 조회 시작: storeId=${storeId}, date=$date" } log.info { "[getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" }
val currentDate: LocalDate = KoreaDate.today() val currentDate: LocalDate = KoreaDate.today()
val currentTime: LocalTime = KoreaTime.now() val currentTime: LocalTime = KoreaTime.now()
if (date.isBefore(currentDate)) { if (date.isBefore(currentDate)) {
log.warn { "[getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" }
throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME)
} }
@ -43,7 +44,6 @@ class ScheduleService(
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date)
.filter { it.date.isAfter(currentDate) || it.time.isAfter(currentTime) } .filter { it.date.isAfter(currentDate) || it.time.isAfter(currentTime) }
return schedules.toResponseWithTheme() return schedules.toResponseWithTheme()
.also { .also {
log.info { "[getStoreScheduleByDate] storeId=${storeId}, date=$date${it.schedules.size}개 일정 조회 완료" } log.info { "[getStoreScheduleByDate] storeId=${storeId}, date=$date${it.schedules.size}개 일정 조회 완료" }
@ -52,7 +52,7 @@ class ScheduleService(
@Transactional @Transactional
fun holdSchedule(id: Long) { fun holdSchedule(id: Long) {
log.debug { "[holdSchedule] 일정 Holding 시작: id=$id" } log.info { "[holdSchedule] 일정 Holding 시작: id=$id" }
val schedule = findForUpdateOrThrow(id).also { val schedule = findForUpdateOrThrow(id).also {
scheduleValidator.validateCanHold(it) scheduleValidator.validateCanHold(it)
@ -69,7 +69,7 @@ class ScheduleService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStateWithLock(id: Long): ScheduleStateResponse { fun findStateWithLock(id: Long): ScheduleStateResponse {
log.debug { "[findDateTimeById] 일정 개요 조회 시작 : id=$id" } log.info { "[findDateTimeById] 일정 개요 조회 시작 : id=$id" }
val schedule: ScheduleEntity = scheduleRepository.findByIdForUpdate(id) val schedule: ScheduleEntity = scheduleRepository.findByIdForUpdate(id)
?: run { ?: run {
@ -95,7 +95,7 @@ class ScheduleService(
@Transactional @Transactional
fun changeStatus(scheduleId: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus) { fun changeStatus(scheduleId: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus) {
log.debug { "[reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } log.info { "[reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" }
scheduleRepository.changeStatus(scheduleId, currentStatus, changeStatus).also { scheduleRepository.changeStatus(scheduleId, currentStatus, changeStatus).also {
log.info { "[reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } log.info { "[reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" }
@ -103,7 +103,7 @@ class ScheduleService(
} }
private fun findForUpdateOrThrow(id: Long): ScheduleEntity { private fun findForUpdateOrThrow(id: Long): ScheduleEntity {
log.debug { "[findForUpdateOrThrow] 일정 LOCK + 조회 시작: id=$id" } log.info { "[findForUpdateOrThrow] 일정 LOCK + 조회 시작: id=$id" }
return scheduleRepository.findByIdForUpdate(id) return scheduleRepository.findByIdForUpdate(id)
?.also { log.info { "[findForUpdateOrThrow] 일정 조회 완료: id=$id" } } ?.also { log.info { "[findForUpdateOrThrow] 일정 조회 완료: id=$id" } }

View File

@ -59,7 +59,7 @@ class ScheduleValidator(
private fun validateAlreadyExists(storeId: Long, date: LocalDate, themeId: Long, time: LocalTime) { private fun validateAlreadyExists(storeId: Long, date: LocalDate, themeId: Long, time: LocalTime) {
if (scheduleRepository.existsDuplicate(storeId, date, themeId, time)) { if (scheduleRepository.existsDuplicate(storeId, date, themeId, time)) {
log.debug { log.info {
"[validateAlreadyExists] 동일한 날짜, 테마, 시간 존재로 인한 실패: date=${date} / themeId=${themeId} / time=${time}" "[validateAlreadyExists] 동일한 날짜, 테마, 시간 존재로 인한 실패: date=${date} / themeId=${themeId} / time=${time}"
} }
throw ScheduleException(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS) throw ScheduleException(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS)
@ -71,7 +71,7 @@ class ScheduleValidator(
val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES) val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES)
if (inputDateTime.isBefore(now)) { if (inputDateTime.isBefore(now)) {
log.debug { log.info {
"[validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}" "[validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}"
} }
throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME)
@ -82,7 +82,7 @@ class ScheduleValidator(
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date, themeId) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date, themeId)
.firstOrNull { it.containsTime(time) } .firstOrNull { it.containsTime(time) }
?.let { ?.let {
log.debug { "[validateTimeNotConflict] 시간이 겹치는 일정 존재: conflictSchedule(Id=${it.id}, time=${it.time}~${it.getEndAt()})" } log.info { "[validateTimeNotConflict] 시간이 겹치는 일정 존재: conflictSchedule(Id=${it.id}, time=${it.time}~${it.getEndAt()})" }
throw ScheduleException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT) throw ScheduleException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT)
} }
} }

View File

@ -35,7 +35,7 @@ class StoreService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getDetail(id: Long): StoreDetailResponse { fun getDetail(id: Long): StoreDetailResponse {
log.debug { "[getDetail] 매장 상세 조회 시작: id=${id}" } log.info { "[getDetail] 매장 상세 조회 시작: id=${id}" }
val store: StoreEntity = findOrThrow(id) val store: StoreEntity = findOrThrow(id)
val region = regionService.findRegionInfo(store.regionCode) val region = regionService.findRegionInfo(store.regionCode)
@ -47,7 +47,7 @@ class StoreService(
@Transactional @Transactional
fun register(request: StoreRegisterRequest): StoreRegisterResponse { fun register(request: StoreRegisterRequest): StoreRegisterResponse {
log.debug { "[register] 매장 등록 시작: name=${request.name}" } log.info { "[register] 매장 등록 시작: name=${request.name}" }
storeValidator.validateCanRegister(request) storeValidator.validateCanRegister(request)
@ -70,7 +70,7 @@ class StoreService(
@Transactional @Transactional
fun update(id: Long, request: StoreUpdateRequest) { fun update(id: Long, request: StoreUpdateRequest) {
log.debug { "[update] 매장 수정 시작: id=${id}, request=${request}" } log.info { "[update] 매장 수정 시작: id=${id}, request=${request}" }
storeValidator.validateCanUpdate(request) storeValidator.validateCanUpdate(request)
@ -83,7 +83,7 @@ class StoreService(
@Transactional @Transactional
fun disableById(id: Long) { fun disableById(id: Long) {
log.debug { "[inactive] 매장 비활성화 시작: id=${id}" } log.info { "[inactive] 매장 비활성화 시작: id=${id}" }
findOrThrow(id).apply { findOrThrow(id).apply {
this.disable() this.disable()
@ -94,7 +94,7 @@ class StoreService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): StoreNameListResponse { fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): StoreNameListResponse {
log.debug { "[getAllActiveStores] 전체 매장 조회 시작" } log.info { "[getAllActiveStores] 전체 매장 조회 시작" }
val regionCode: String? = when { val regionCode: String? = when {
sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED) sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED)
@ -108,7 +108,7 @@ class StoreService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStoreInfo(id: Long): StoreInfoResponse { fun findStoreInfo(id: Long): StoreInfoResponse {
log.debug { "[findStoreInfo] 매장 정보 조회 시작: id=${id}" } log.info { "[findStoreInfo] 매장 정보 조회 시작: id=${id}" }
val store: StoreEntity = findOrThrow(id) val store: StoreEntity = findOrThrow(id)
@ -117,7 +117,7 @@ class StoreService(
} }
private fun getAuditInfo(store: StoreEntity): AuditingInfo { private fun getAuditInfo(store: StoreEntity): AuditingInfo {
log.debug { "[getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" } log.info { "[getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" }
val createdBy = adminService.findOperatorOrUnknown(store.createdBy) val createdBy = adminService.findOperatorOrUnknown(store.createdBy)
val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy) val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy)
@ -132,7 +132,7 @@ class StoreService(
} }
private fun findOrThrow(id: Long): StoreEntity { private fun findOrThrow(id: Long): StoreEntity {
log.debug { "[findOrThrow] 매장 조회 시작: id=${id}" } log.info { "[findOrThrow] 매장 조회 시작: id=${id}" }
return storeRepository.findActiveStoreById(id) return storeRepository.findActiveStoreById(id)
?.also { ?.also {

View File

@ -31,21 +31,21 @@ class StoreValidator(
private fun validateDuplicateNameExist(name: String) { private fun validateDuplicateNameExist(name: String) {
if (storeRepository.existsByName(name)) { if (storeRepository.existsByName(name)) {
log.debug { "[StoreValidator.validateDuplicateNameExist] 이름 중복: name=${name}" } log.info { "[StoreValidator.validateDuplicateNameExist] 이름 중복: name=${name}" }
throw StoreException(StoreErrorCode.STORE_NAME_DUPLICATED) throw StoreException(StoreErrorCode.STORE_NAME_DUPLICATED)
} }
} }
private fun validateDuplicateContactExist(contact: String) { private fun validateDuplicateContactExist(contact: String) {
if (storeRepository.existsByContact(contact)) { if (storeRepository.existsByContact(contact)) {
log.debug { "[StoreValidator.validateDuplicateContact] 연락처 중복: contact=${contact}" } log.info { "[StoreValidator.validateDuplicateContact] 연락처 중복: contact=${contact}" }
throw StoreException(StoreErrorCode.STORE_CONTACT_DUPLICATED) throw StoreException(StoreErrorCode.STORE_CONTACT_DUPLICATED)
} }
} }
private fun validateDuplicateAddressExist(address: String) { private fun validateDuplicateAddressExist(address: String) {
if (storeRepository.existsByAddress(address)) { if (storeRepository.existsByAddress(address)) {
log.debug { "[StoreValidator.validateDuplicateAddress] 주소 중복: address=${address}" } log.info { "[StoreValidator.validateDuplicateAddress] 주소 중복: address=${address}" }
throw StoreException(StoreErrorCode.STORE_ADDRESS_DUPLICATED) throw StoreException(StoreErrorCode.STORE_ADDRESS_DUPLICATED)
} }
} }

View File

@ -3,18 +3,22 @@ package com.sangdol.roomescape.theme.business
import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.admin.business.AdminService import com.sangdol.roomescape.admin.business.AdminService
import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.dto.* import com.sangdol.roomescape.theme.dto.ThemeDetailResponse
import com.sangdol.roomescape.theme.dto.ThemeSummaryListResponse
import com.sangdol.roomescape.theme.dto.ThemeNameListResponse
import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.dto.ThemeCreateResponse
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.exception.ThemeException
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.mapper.toDetailResponse import com.sangdol.roomescape.theme.mapper.toDetailResponse
import com.sangdol.roomescape.theme.mapper.toSummaryListResponse
import com.sangdol.roomescape.theme.mapper.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.theme.mapper.toNameListResponse import com.sangdol.roomescape.theme.mapper.toNameListResponse
import com.sangdol.roomescape.theme.mapper.toSummaryListResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.cache.annotation.CacheEvict
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -30,7 +34,7 @@ class AdminThemeService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemeSummaries(): ThemeSummaryListResponse { fun findThemeSummaries(): ThemeSummaryListResponse {
log.debug { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } log.info { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findAll() return themeRepository.findAll()
.toSummaryListResponse() .toSummaryListResponse()
@ -39,7 +43,7 @@ class AdminThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemeDetail(id: Long): ThemeDetailResponse { fun findThemeDetail(id: Long): ThemeDetailResponse {
log.debug { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" } log.info { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id) val theme: ThemeEntity = findOrThrow(id)
@ -53,7 +57,7 @@ class AdminThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findActiveThemes(): ThemeNameListResponse { fun findActiveThemes(): ThemeNameListResponse {
log.debug { "[findActiveThemes] open 상태인 모든 테마 조회 시작" } log.info { "[findActiveThemes] open 상태인 모든 테마 조회 시작" }
return themeRepository.findActiveThemes() return themeRepository.findActiveThemes()
.toNameListResponse() .toNameListResponse()
@ -65,7 +69,7 @@ class AdminThemeService(
@Transactional @Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse { fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
log.debug { "[createTheme] 테마 생성 시작: name=${request.name}" } log.info { "[createTheme] 테마 생성 시작: name=${request.name}" }
themeValidator.validateCanCreate(request) themeValidator.validateCanCreate(request)
@ -77,10 +81,10 @@ class AdminThemeService(
} }
} }
@CacheEvict(cacheNames = ["theme-details"], key = "#id")
@Transactional @Transactional
fun deleteTheme(id: Long) { fun deleteTheme(id: Long) {
log.debug { "[deleteTheme] 테마 삭제 시작: id=${id}" } log.info { "[deleteTheme] 테마 삭제 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id) val theme: ThemeEntity = findOrThrow(id)
@ -89,10 +93,9 @@ class AdminThemeService(
} }
} }
@CacheEvict(cacheNames = ["theme-details"], key = "#id")
@Transactional @Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) { fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.debug { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" } log.info { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) { if (request.isAllParamsNull()) {
log.info { "[updateTheme] 테마 변경 사항 없음: id=${id}" } log.info { "[updateTheme] 테마 변경 사항 없음: id=${id}" }
@ -121,7 +124,7 @@ class AdminThemeService(
} }
private fun findOrThrow(id: Long): ThemeEntity { private fun findOrThrow(id: Long): ThemeEntity {
log.debug { "[findOrThrow] 테마 조회 시작: id=$id" } log.info { "[findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id) return themeRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } } ?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } }

View File

@ -10,8 +10,6 @@ import com.sangdol.roomescape.theme.mapper.toInfoResponse
import com.sangdol.roomescape.theme.mapper.toListResponse import com.sangdol.roomescape.theme.mapper.toListResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -23,19 +21,13 @@ private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class ThemeService( class ThemeService(
private val themeRepository: ThemeRepository, private val themeRepository: ThemeRepository
meterRegistry: MeterRegistry
) { ) {
private val themeDetailQueryRequestCount = meterRegistry.counter("theme.detail.query.requested")
@Cacheable(cacheNames = ["theme-details"], key="#id")
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findInfoById(id: Long): ThemeInfoResponse { fun findInfoById(id: Long): ThemeInfoResponse {
log.debug { "[findInfoById] 테마 조회 시작: id=$id" } log.info { "[findInfoById] 테마 조회 시작: id=$id" }
val theme = themeRepository.findByIdOrNull(id)?.also { val theme = themeRepository.findByIdOrNull(id) ?: run {
themeDetailQueryRequestCount.increment()
} ?: run {
log.warn { "[updateTheme] 테마 조회 실패: id=$id" } log.warn { "[updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND) throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
} }
@ -46,7 +38,7 @@ class ThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse { fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse {
log.debug { "[findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" } log.info { "[findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" }
val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()) val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(KoreaDate.today())
val previousWeekSaturday = previousWeekSunday.plusDays(6) val previousWeekSaturday = previousWeekSunday.plusDays(6)

View File

@ -1,6 +1,7 @@
package com.sangdol.roomescape.user.business package com.sangdol.roomescape.user.business
import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.user.dto.UserContactResponse import com.sangdol.roomescape.user.dto.UserContactResponse
import com.sangdol.roomescape.user.dto.UserCreateRequest import com.sangdol.roomescape.user.dto.UserCreateRequest
import com.sangdol.roomescape.user.dto.UserCreateResponse import com.sangdol.roomescape.user.dto.UserCreateResponse
@ -27,9 +28,20 @@ class UserService(
private val userValidator: UserValidator, private val userValidator: UserValidator,
private val idGenerator: IDGenerator private val idGenerator: IDGenerator
) { ) {
@Transactional(readOnly = true)
fun findContextById(id: Long): CurrentUserContext {
log.info { "[findContextById] 현재 로그인된 회원 조회 시작: id=${id}" }
val user: UserEntity = findOrThrow(id)
return CurrentUserContext(user.id, user.name)
.also {
log.info { "[findContextById] 현재 로그인된 회원 조회 완료: id=${id}" }
}
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findCredentialsByAccount(email: String): UserLoginCredentials { fun findCredentialsByAccount(email: String): UserLoginCredentials {
log.debug { "[findCredentialsByAccount] 회원 조회 시작: email=${email}" } log.info { "[findCredentialsByAccount] 회원 조회 시작: email=${email}" }
return userRepository.findByEmail(email) return userRepository.findByEmail(email)
?.let { ?.let {
@ -37,13 +49,14 @@ class UserService(
it.toCredentials() it.toCredentials()
} }
?: run { ?: run {
log.info { "[findCredentialsByAccount] 회원 조회 실패" }
throw UserException(UserErrorCode.USER_NOT_FOUND) throw UserException(UserErrorCode.USER_NOT_FOUND)
} }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findContactById(id: Long): UserContactResponse { fun findContactById(id: Long): UserContactResponse {
log.debug { "[findContactById] 회원 연락 정보 조회 시작: id=${id}" } log.info { "[findContactById] 회원 연락 정보 조회 시작: id=${id}" }
val user = findOrThrow(id) val user = findOrThrow(id)
@ -55,7 +68,7 @@ class UserService(
@Transactional @Transactional
fun signup(request: UserCreateRequest): UserCreateResponse { fun signup(request: UserCreateRequest): UserCreateResponse {
log.debug { "[signup] 회원가입 시작: request:$request" } log.info { "[signup] 회원가입 시작: request:$request" }
userValidator.validateCanSignup(request.email, request.phone) userValidator.validateCanSignup(request.email, request.phone)

View File

@ -15,14 +15,14 @@ class UserValidator(
) { ) {
fun validateCanSignup(email: String, phone: String) { fun validateCanSignup(email: String, phone: String) {
log.debug { "[UserValidator.validateCanSignup] 회원가입 가능 여부 검증 시작: email:$email / phone:$phone" } log.info { "[UserValidator.validateCanSignup] 회원가입 가능 여부 검증 시작: email:$email / phone:$phone" }
if (userRepository.existsByEmail(email)) { if (userRepository.existsByEmail(email)) {
log.debug { "[UserValidator.validateCanSignup] 중복된 이메일 입력으로 인한 실패: email:$email" } log.info { "[UserValidator.validateCanSignup] 중복된 이메일 입력으로 인한 실패: email:$email" }
throw UserException(UserErrorCode.EMAIL_ALREADY_EXISTS) throw UserException(UserErrorCode.EMAIL_ALREADY_EXISTS)
} }
if (userRepository.existsByPhone(phone)) { if (userRepository.existsByPhone(phone)) {
log.debug { "[UserValidator.validateCanSignup] 중복된 휴대폰 번호 입력으로 인한 실패: phone:$phone" } log.info { "[UserValidator.validateCanSignup] 중복된 휴대폰 번호 입력으로 인한 실패: phone:$phone" }
throw UserException(UserErrorCode.PHONE_ALREADY_EXISTS) throw UserException(UserErrorCode.PHONE_ALREADY_EXISTS)
} }
} }

View File

@ -16,9 +16,6 @@ spring:
jdbc: jdbc:
batch_size: ${JDBC_BATCH_SIZE:100} batch_size: ${JDBC_BATCH_SIZE:100}
order_inserts: true order_inserts: true
cache:
type: caffeine
cache-names: ${CACHE_NAMES:theme-details}
management: management:
endpoints: endpoints:

View File

@ -1,4 +1,4 @@
package com.sangdol.roomescape.auth package com.sangdol.roomescape.auth.business
import com.sangdol.roomescape.auth.business.domain.PrincipalType import com.sangdol.roomescape.auth.business.domain.PrincipalType
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity
@ -68,4 +68,4 @@ class LoginHistoryEventListenerTest : FunSpec() {
return batch return batch
} }
} }

View File

@ -43,6 +43,16 @@ class PaymentAPITest(
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
requestBody = PaymentFixture.cancelRequest,
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
)
}
} }
test("정상 취소") { test("정상 취소") {

View File

@ -49,6 +49,15 @@ class ReservationApiTest(
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
)
}
} }
@ -176,6 +185,15 @@ class ReservationApiTest(
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
} }
test("정상 응답") { test("정상 응답") {
@ -220,6 +238,15 @@ class ReservationApiTest(
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
} }
test("정상 응답") { test("정상 응답") {
@ -288,6 +315,15 @@ class ReservationApiTest(
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
} }
test("정상 응답") { test("정상 응답") {
@ -341,6 +377,15 @@ class ReservationApiTest(
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
} }
context("정상 응답") { context("정상 응답") {

View File

@ -108,7 +108,7 @@ class ReservationConcurrencyTest(
private fun createPendingReservation(user: UserEntity, schedule: ScheduleEntity): Long { private fun createPendingReservation(user: UserEntity, schedule: ScheduleEntity): Long {
return reservationService.createPendingReservation( return reservationService.createPendingReservation(
user = CurrentUserContext(id = user.id), user = CurrentUserContext(id = user.id, name = user.name),
request = PendingReservationCreateRequest( request = PendingReservationCreateRequest(
scheduleId = schedule.id, scheduleId = schedule.id,
reserverName = user.name, reserverName = user.name,

View File

@ -1,13 +1,12 @@
package com.sangdol.roomescape.reservation package com.sangdol.roomescape.reservation.business.event
import com.sangdol.roomescape.reservation.business.event.ReservationConfirmEvent
import com.sangdol.roomescape.reservation.business.event.ReservationEventListener
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import io.kotest.assertions.assertSoftly import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
@ -43,4 +42,4 @@ class ReservationEventListenerTest(
} }
} }
} }
} }

View File

@ -6,28 +6,21 @@ import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import com.sangdol.roomescape.supports.* import com.sangdol.roomescape.supports.*
import com.sangdol.roomescape.supports.ThemeFixture.createRequest import com.sangdol.roomescape.supports.ThemeFixture.createRequest
import com.sangdol.roomescape.theme.business.AdminThemeService
import com.sangdol.roomescape.theme.business.MIN_DURATION import com.sangdol.roomescape.theme.business.MIN_DURATION
import com.sangdol.roomescape.theme.business.MIN_PARTICIPANTS import com.sangdol.roomescape.theme.business.MIN_PARTICIPANTS
import com.sangdol.roomescape.theme.business.MIN_PRICE import com.sangdol.roomescape.theme.business.MIN_PRICE
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.springframework.cache.CacheManager
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
class AdminThemeApiTest( class AdminThemeApiTest(
private val themeRepository: ThemeRepository, private val themeRepository: ThemeRepository
private val themeService: ThemeService,
private val cacheManager: CacheManager
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
@ -489,19 +482,14 @@ class AdminThemeApiTest(
} }
} }
test("정상 삭제 및 캐시 제거 확인") { test("정상 삭제") {
val token = testAuthUtil.defaultHqAdminLogin().second val token = testAuthUtil.defaultHqAdminLogin().second
val createdTheme = initialize("테스트를 위한 테마 생성") { val createdTheme = initialize("테스트를 위한 테마 생성") {
dummyInitializer.createTheme() dummyInitializer.createTheme()
} }
initialize("테마 캐시 추가") {
themeService.findInfoById(createdTheme.id)
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java).shouldNotBeNull()
}
runTest( runTest(
token = token, token = testAuthUtil.defaultHqAdminLogin().second,
on = { on = {
delete("/admin/themes/${createdTheme.id}") delete("/admin/themes/${createdTheme.id}")
}, },
@ -510,7 +498,6 @@ class AdminThemeApiTest(
} }
).also { ).also {
themeRepository.findByIdOrNull(createdTheme.id) shouldBe null themeRepository.findByIdOrNull(createdTheme.id) shouldBe null
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java) shouldBe null
} }
} }
@ -579,7 +566,7 @@ class AdminThemeApiTest(
val updateRequest = ThemeUpdateRequest(name = "modified") val updateRequest = ThemeUpdateRequest(name = "modified")
test("정상 수정 및 감사 정보 & 캐시 변경 확인") { test("정상 수정 및 감사 정보 변경 확인") {
val createdThemeId: Long = initialize("테스트를 위한 관리자1의 테마 생성") { val createdThemeId: Long = initialize("테스트를 위한 관리자1의 테마 생성") {
runTest( runTest(
token = testAuthUtil.defaultHqAdminLogin().second, token = testAuthUtil.defaultHqAdminLogin().second,
@ -595,11 +582,6 @@ class AdminThemeApiTest(
).extract().path("data.id") ).extract().path("data.id")
} }
initialize("테마 캐시 추가") {
themeService.findInfoById(createdThemeId)
cacheManager.getCache("theme-details")?.get(createdThemeId, ThemeInfoResponse::class.java).shouldNotBeNull()
}
val (otherAdmin, otherAdminToken) = initialize("감사 정보 변경 확인을 위한 관리자2 로그인") { val (otherAdmin, otherAdminToken) = initialize("감사 정보 변경 확인을 위한 관리자2 로그인") {
testAuthUtil.adminLogin( testAuthUtil.adminLogin(
AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.WRITABLE) AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.WRITABLE)
@ -622,12 +604,6 @@ class AdminThemeApiTest(
updatedTheme.name shouldBe updateRequest.name updatedTheme.name shouldBe updateRequest.name
updatedTheme.updatedBy shouldBe otherAdmin.id updatedTheme.updatedBy shouldBe otherAdmin.id
// 캐시 제거 확인
assertSoftly(cacheManager.getCache("theme-details")?.get(createdThemeId, ThemeInfoResponse::class.java)) {
this shouldBe null
}
} }
} }

View File

@ -10,32 +10,23 @@ import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.mapper.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.comparables.shouldBeLessThan import io.kotest.matchers.comparables.shouldBeLessThan
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.springframework.cache.CacheManager
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import java.time.LocalDate import java.time.LocalDate
class ThemeApiTest( class ThemeApiTest(
private val themeRepository: ThemeRepository, private val themeRepository: ThemeRepository
private val cacheManager: CacheManager
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
context("ID로 테마 정보를 조회한다.") { context("ID로 테마 정보를 조회한다.") {
test("정상 응답 및 캐시 저장 확인") { test("정상 응답") {
val createdTheme: ThemeEntity = initialize("조회를 위한 테마 생성") { val createdTheme: ThemeEntity = initialize("조회를 위한 테마 생성") {
dummyInitializer.createTheme() dummyInitializer.createTheme()
} }
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java).also {
it shouldBe null
}
runTest( runTest(
on = { on = {
get("/themes/${createdTheme.id}") get("/themes/${createdTheme.id}")
@ -52,15 +43,6 @@ class ThemeApiTest(
) )
} }
) )
assertSoftly(cacheManager.getCache("theme-details")) {
this.shouldNotBeNull()
val themeFromCache = this.get(createdTheme.id, ThemeInfoResponse::class.java)
themeFromCache.shouldNotBeNull()
themeFromCache.id shouldBe createdTheme.id
}
} }
test("테마가 없으면 실패한다.") { test("테마가 없으면 실패한다.") {

View File

@ -1,68 +0,0 @@
package com.sangdol.roomescape.theme
import com.ninjasquad.springmockk.MockkBean
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import com.sangdol.roomescape.supports.IDGenerator
import com.sangdol.roomescape.supports.initialize
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import io.mockk.every
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.springframework.data.repository.findByIdOrNull
import java.util.concurrent.CountDownLatch
class ThemeConcurrencyTest(
private val themeService: ThemeService,
@MockkBean private val themeRepository: ThemeRepository,
) : FunSpecSpringbootTest() {
init {
test("동일한 테마에 대한 반복 조회 요청시, DB 요청은 1회만 발생한다.") {
val entity = ThemeEntity(
id = IDGenerator.create(),
name = "테스트입니다.",
description = "테스트에요!",
thumbnailUrl = "http://localhost:8080/hello",
difficulty = Difficulty.VERY_EASY,
price = 10000,
minParticipants = 3,
maxParticipants = 5,
availableMinutes = 90,
expectedMinutesFrom = 70,
expectedMinutesTo = 80,
isActive = true
)
every {
themeRepository.findByIdOrNull(entity.id)
} returns entity
initialize("캐시 등록") {
themeService.findInfoById(entity.id)
}
val requestCount = 64
withContext(Dispatchers.IO) {
val latch = CountDownLatch(requestCount)
(1..requestCount).map {
async {
latch.countDown()
latch.await()
themeService.findInfoById(entity.id)
}
}
}
verify(exactly = 1) {
themeRepository.findByIdOrNull(entity.id)
}
}
}
}

View File

@ -148,6 +148,15 @@ class UserApiTest(
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
) )
} }
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultStoreAdminLogin().second,
method = HttpMethod.GET,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
)
}
} }
test("정상 응답") { test("정상 응답") {

View File

@ -18,9 +18,6 @@ spring:
init: init:
mode: always mode: always
schema-locations: classpath:schema/schema-mysql.sql schema-locations: classpath:schema/schema-mysql.sql
cache:
type: caffeine
cache-names: ${CACHE_NAMES:theme-details}
security: security:
jwt: jwt: