Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
9545fdb
[Docs] README.md 생성
msk226 Dec 26, 2025
3b98320
[Feature] 스터디 일정 도메인 엔티티 정의
msk226 Dec 27, 2025
01a63bc
[Refactor] 스터디 모듈 내, 하위 도메인들과 구분을 위해 패키지 분리 진행
msk226 Dec 27, 2025
f9ca319
[Refactor] Q 클래스 import 오류 수정
msk226 Dec 27, 2025
9f65555
[Feature] 일정 생성 및 삭제 API 구현
msk226 Dec 27, 2025
a3beaaa
[Feature] 일정 생성 및 삭제 테스트 코드 작성
msk226 Dec 27, 2025
c8ea59f
[Feature] 일정 생성 및 삭제 API 명세 작성
msk226 Dec 27, 2025
a45bcd3
[Feature] 일정 조회 (월, 주, 다가오는 일정) API 구현
msk226 Dec 27, 2025
87de581
[Feature] 일정 조회 (월, 주, 다가오는 일정) 테스트 코드 작성
msk226 Dec 27, 2025
9f3bd82
[Refactor] 스터디 관련 테스트 코드 패키지 분리
msk226 Dec 27, 2025
0ddba92
[Refactor] 지역 도메인 관련 클래스 충돌 해결
msk226 Dec 27, 2025
ab253c7
[Refactor] 시간대 입력 포맷 관련 명세 추가 작성
msk226 Dec 27, 2025
4c18a47
[Merge] Merge pull request #82 from SPOTeam/feature/76
msk226 Dec 27, 2025
7f2fd0d
[Refactor] 조회수 카운터 공통 모듈로 추상화
msk226 Dec 27, 2025
a8e8978
[Refactor] Redis 조회수 구현체를 infra/view-redis 모듈로 분리
msk226 Dec 27, 2025
c0d1a1d
[Refactor] Redis 조회수 구현체를 infra/view-redis 모듈로 분리
msk226 Dec 27, 2025
a5c2a6b
[Merge] Merge pull request #84 from SPOTeam/refactor/83
msk226 Dec 27, 2025
03020b9
[Feature] 스터디 게시판 관련 도메인 엔티티 정의
msk226 Dec 28, 2025
857cd45
[Feature] 스터디 게시판 관련 도메인 엔티티 domain 하위 패키지로 이동
msk226 Dec 28, 2025
0c59e9f
[Feature] 스터디 게시글 생성/수정/삭제 API 구현
msk226 Dec 28, 2025
55d9391
[Feature] 스터디 게시글 핀 API 구현
msk226 Dec 28, 2025
47f1e9f
[Feature] 스터디 게시글 command 로직 관련 테스트 코드 작성
msk226 Dec 28, 2025
2effadc
[Feature] 스터디 게시글 command 로직 스웨거 명세서 작성
msk226 Dec 28, 2025
7e1ffd9
[Feature] post 모듈과 빈 네임 충돌 방지를 위해 별도 빈 네임 지정
msk226 Dec 28, 2025
713d9d1
[Feature] 작성자 정보 관련 애노테이션 스웨거에서 숨기도록 설정
msk226 Dec 28, 2025
8b62a81
[Feature] 해당 스터디의 회원인 경우만 게시판에 접근할 수 있도록 검증 로직 추가
msk226 Dec 28, 2025
96cdf85
[Merge] Merge pull request #85 from SPOTeam/feature/78
msk226 Dec 28, 2025
4ec853d
[Feature] 스터디 게시판 댓글 생성/수정/삭제 관련 기능 구현
msk226 Dec 28, 2025
33ce5a2
[Feature] 스터디 게시판 댓글 생성/수정/삭제 검증 테스트 코드 작성
msk226 Dec 28, 2025
103928d
[Feature] 스터디 게시판 조회 API 구현
msk226 Dec 29, 2025
783b51f
[Feature] 스터디 게시판 조회 API 검증 테스트 코드 작성
msk226 Dec 29, 2025
bdfe015
[Feature] 스터디 게시글 상세 조회 시 검증 로직 구체화 진행
msk226 Dec 29, 2025
ba8b86c
[Merge] Merge pull request #86 from SPOTeam/feature/78
msk226 Dec 29, 2025
08027ab
[Feature] 스터디 게시글 조회수 배치 반영 로직 구현
msk226 Dec 29, 2025
d1e45bd
[Merge] Merge pull request #87 from SPOTeam/feature/78
msk226 Dec 29, 2025
041bca7
[Feature] 스터디 게시글 좋아요 및 삭제 로직 구현
msk226 Dec 29, 2025
32ecfa1
[Merge] Merge pull request #88 from SPOTeam/feature/78
msk226 Dec 29, 2025
4757672
[Feature] 스터디 투두 관련 API 구현
msk226 Dec 29, 2025
e11d326
[Feature] 스터디 투두 관련 API 검증 테스트 코드 작성
msk226 Dec 30, 2025
a1b5c35
[Merge] Merge pull request #89 from SPOTeam/feature/80
msk226 Dec 30, 2025
1402fb5
[Feature] 스터디 회고록 도메인 엔티티 구조 설계
msk226 Dec 30, 2025
e2b23e9
[Feature] 스터디 회고록 관련 API 로직 구현
msk226 Dec 30, 2025
6d7aa36
[Feature] 스터디 회고록 관련 API 검증 테스트 코드 작성
msk226 Dec 30, 2025
8e3288b
[Feature] 스터디 회고록 리액션 API 멱등적 설계
msk226 Dec 30, 2025
0001d73
[Feature] 스터디 회고록 리액션 엔티티 누락된 Enum 속성 추가
msk226 Dec 30, 2025
12a7642
[Refactor] 미사용 메서드 삭제
msk226 Dec 30, 2025
828e23a
[Feature] 스터디 투두 관련 API 구현
msk226 Dec 29, 2025
71e7f59
[Feature] 스터디 투두 관련 API 검증 테스트 코드 작성
msk226 Dec 30, 2025
5ccd4e0
[Feature] 스터디 회고록 관련 API 로직 구현
msk226 Dec 30, 2025
e35c603
[Refactor] 로직 리팩토링으로 인한 테스트 코드 수정
msk226 Dec 30, 2025
a08a846
Merge branch 'develop' into feature/79
msk226 Dec 30, 2025
102a733
[Refactor] 충돌로 인해 롤백된 변경사항 재반영
msk226 Dec 30, 2025
132623e
[Merge] Merge pull request #92 from SPOTeam/feature/79
msk226 Dec 30, 2025
c20c1cc
[Feature] 게시글 신고 API 구현
msk226 Dec 30, 2025
a49a501
[Feature] 게시글 신고 API 검증 테스트 코드 작성
msk226 Dec 30, 2025
83de49a
[Merge] Merge pull request #93 from SPOTeam/feature/90
msk226 Dec 30, 2025
96a5093
[Feature] 스터디 신청 플로우 변경으로 인한 로직 수정
msk226 Dec 30, 2025
fbfea5e
[Feature] 스터디 신청 시, 알림 발송 로직 구현
msk226 Dec 30, 2025
f0cd91d
[Feature] 스터디 승인 플로우 변경으로 인한 테스트 코드 수정
msk226 Dec 30, 2025
6984817
[Merge] Merge pull request #94 from SPOTeam/feature/91
msk226 Dec 30, 2025
5b0888f
[Feature] 스터디 일정 출석체크 관련 기능 구현
msk226 Dec 30, 2025
a324698
[Feature] 스터디 일정 출석체크 관련 기능 검증 테스트 코드 작성
msk226 Dec 30, 2025
62cde20
[Feature] 스터디 일정 출석체크 로직 변경
msk226 Dec 30, 2025
86ef197
[Feature] 스터디장의 경우, 출석 시작 시 바로 출석으로 처리
msk226 Dec 30, 2025
e8c8ab7
[Feature] 로직 변경으로 인한 테스트 코드 수정
msk226 Dec 30, 2025
0dbbf80
[Feature] 스웨거 명세 작성
msk226 Dec 30, 2025
8cfe828
[Feature] 테스트 코드 오류 해결
msk226 Dec 30, 2025
910fb4b
[Merge] Merge pull request #95 from SPOTeam/feature/77
msk226 Dec 30, 2025
e0fa100
[Feature] 회원 메타데이터 조회 API 구현
msk226 Dec 31, 2025
80fe54e
[Feature] 회원 메타데이터 조회 API 검증 테스트 코드 작성
msk226 Dec 31, 2025
0befc23
[Feature] 스터디 카운트 포트 구현체 검증 테스트 코드 작성
msk226 Dec 31, 2025
c27e64d
[Feature] 스웨거 명세 작성
msk226 Dec 31, 2025
af1eb8b
[Feature] 누락된 스웨거 명세 작성
msk226 Dec 31, 2025
114f315
[Merge] Merge pull request #102 from SPOTeam/feature/98
msk226 Dec 31, 2025
7943458
[Feature] 스터디 상세 정보 조회 API 구현
msk226 Dec 31, 2025
afad976
[Feature] 스터디 조회 수 배치 반영 스케줄러 구현
msk226 Dec 31, 2025
55448d4
[Feature] 스터디 게시글 리스트 조회 시, 작성자 정보는 제외
msk226 Dec 31, 2025
032f6fb
[Feature] 스터디 상세 정보 조회 검증 테스트 코드 작성
msk226 Dec 31, 2025
0e941ed
[Feature] 스터디 조회수 중복 방지 및 배치 업데이트 검증 테스트 코드 작성
msk226 Dec 31, 2025
3e7d3a7
[Merge] Merge pull request #103 from SPOTeam/feature/75
msk226 Dec 31, 2025
1850505
[Feature] 스터디 필터링 API와 상세 조회 API 클래스 분리
msk226 Dec 31, 2025
8758330
[Feature] 스터디 좋아요 도메인 엔티티 정의
msk226 Dec 31, 2025
864485a
[Feature] 내가 좋아요 한 스터디 조회 API 엔드포인트 설계
msk226 Dec 31, 2025
4f77e62
[Feature] 스터디 좋아요 및 취소 API 구현
msk226 Dec 31, 2025
250efd8
[Feature] 스터디 좋아요 및 취소 API 검증 테스트 코드 작성
msk226 Dec 31, 2025
8192e61
[Feature] 내가 좋아요 한 스터디 조회 기능 구현
msk226 Dec 31, 2025
0c3d26c
[Feature] 코딩 컨벤션에 맞게 메서드 순서 이동
msk226 Dec 31, 2025
61ee639
[Merge] Merge pull request #104 from SPOTeam/feature/100
msk226 Dec 31, 2025
cbd625c
[Feature] 로그아웃 API 구현
msk226 Dec 31, 2025
b53eab0
[Feature] 로그아웃 API 테스트 코드 작성
msk226 Dec 31, 2025
0ab2187
[Feature] 회원 탈퇴 API 구현
msk226 Dec 31, 2025
50c0acc
[Feature] 회원 탈퇴 이벤트 리스너 관련 테스트 코드 작성
msk226 Dec 31, 2025
9858dce
[Merge] Merge pull request #105 from SPOTeam/feature/101
msk226 Dec 31, 2025
f398ce5
[Feature] 추천 스터디 조회 기능 구현 및 검증
msk226 Dec 31, 2025
d22f0f2
[Feature] 모집 중, 카테고리 별 스터디 조회 기능 구현
msk226 Dec 31, 2025
a2e89c1
[Feature] 스터디 생성 시, 온/오프라인 유무 입력 받도록 수정
msk226 Dec 31, 2025
7849a2d
[Feature] 스터디 조회 시, 내가 좋아요 한 스터디인지 여부 함께 응답
msk226 Dec 31, 2025
7ec866d
[Feature] 정책 변경으로 사라진 API 엔드포인트 삭제
msk226 Dec 31, 2025
6f58c5c
[Feature] CurrentMember 애노테이션 주입 필드 타입 변경
msk226 Dec 31, 2025
1b3cf49
[Feature] 검증 테스트 코드 작성
msk226 Dec 31, 2025
3e24223
[Merge] Merge pull request #106 from SPOTeam/feature/96
msk226 Dec 31, 2025
8cf6610
[Feature] 테스트 유저 생성 API 구현
msk226 Jan 1, 2026
b1d8cdc
[Merge] Merge pull request #107 from SPOTeam/feature/96
msk226 Jan 1, 2026
2399aac
[Feature] 스터디 승인 정책 변경으로 인해 사용하지 않는 API 비활성화 처리
msk226 Jan 1, 2026
ef2bb4d
[Feature] 투두 조회 API 내 MemberId hidden 설정 삭제
msk226 Jan 1, 2026
075ff00
[Feature] 회원 관련 스터디 조회 시, 스터디 식별자 입력 방식 변경
msk226 Jan 1, 2026
5ef8da5
[Feature] 회고록 생성 시, 프라이빗 유무 입력 필드 중복 제거
msk226 Jan 1, 2026
652ab6e
[Feature] 스터디 일정 조회 시, 현재 활동 중인 활동도 포함해서 조회
msk226 Jan 1, 2026
d73a188
[Feature] 스터디 신청 승인 시, 정원 검증 및 업데이트 로직 추가
msk226 Jan 1, 2026
e0ae2e2
[Feature] 스터디 일정 출석체크 마감 시, 미결된 인원은 모두 결석 처리
msk226 Jan 1, 2026
121594f
[Feature] 스웨거 상 Long 타입이 반올림되어 표기되는 이슈 해결
msk226 Jan 1, 2026
8d220ed
[Feature] 스웨거 상 Long 타입이 반올림되어 표기되는 이슈 해결
msk226 Jan 2, 2026
9ab9175
[Merge] Merge pull request #109 from SPOTeam/feature/108
msk226 Jan 2, 2026
87f035b
[Feature] 엔티티 생성 API 호출 시, 해당 엔티티 식별자 응답
msk226 Jan 3, 2026
b27aa27
[Feature] 엔티티 생성 API 호출 시, 해당 엔티티 식별자 응답
msk226 Jan 3, 2026
f79a0b2
[Merge] Merge pull request #110 from SPOTeam/feature/108
msk226 Jan 3, 2026
c431c7f
[Feature] 마이페이지 스터디 조회 시, 회원 상태를 리스트로 입력
msk226 Jan 6, 2026
ace5b1d
[Merge] Merge pull request #111 from SPOTeam/feature/108
msk226 Jan 6, 2026
48a01d6
[Feature] 알림 기능 설계
msk226 Jan 7, 2026
b5d8d38
[Refactor] 스터디 필터링 조회 성능 향상을 위한 통계성 데이터 비정규화 및 쿼리 개선
msk226 Jan 7, 2026
71e6ef2
[Merge] Merge pull request #113 from SPOTeam/refactor/112
msk226 Jan 7, 2026
8c25a93
[Merge] Merge pull request #114 from SPOTeam/feature/97
msk226 Jan 8, 2026
37fe8d7
[Feature] Jacoco 의존성 추가
msk226 Jan 8, 2026
6b9f096
[Feature] 스터디 조회 시, 호스트 여부 함께 응답
msk226 Jan 21, 2026
c65fe32
[Feature] 테스트 코드 반영
msk226 Jan 21, 2026
cb70f1f
[Merge] Merge pull request #116 from SPOTeam/feature/115
msk226 Jan 21, 2026
4db659e
[Feature] 내가 호스트인 스터디 조회 쿼리 타입이 일치하지 않는 문제 해결
msk226 Jan 21, 2026
ff35a49
[Merge] Merge pull request #117 from SPOTeam/feature/115
msk226 Jan 21, 2026
e577cdb
[Feature] 회원 탈퇴 API 내 회원 식별자 주입 시 기본형 대신 Wrapper 타입 사용
msk226 Jan 21, 2026
c44aa2d
[Merge] Merge pull request #118 from SPOTeam/feature/115
msk226 Jan 21, 2026
68b6126
[Feature] 스터디 탈퇴 기능 구현
msk226 Jan 22, 2026
d0ed81a
[Feature] 스터디원 신고 기능 구현
msk226 Jan 22, 2026
fe5c3a2
[Feature] 검증 테스트 코드 작성
msk226 Jan 22, 2026
e4994f4
[Merge] Merge pull request #120 from SPOTeam/feature/115
msk226 Jan 22, 2026
4a904c3
[Feature] 스터디 조회 시, 해당 스터디에 혼자 남아 있는지 나타내는 isAlone 필드 추가
msk226 Jan 23, 2026
ed04588
[Feature] 스터디 탈퇴 시, 회원 수 차감
msk226 Jan 23, 2026
3fa6201
[Feature] 검증 테스트 코드 작성
msk226 Jan 23, 2026
a427683
[Merge] Merge pull request #122 from SPOTeam/feature/121
msk226 Jan 23, 2026
21ac3e4
[Feature] 회고록 조회 시, 생성 일자 함께 응답
msk226 Jan 27, 2026
49c7618
[Merge] Merge pull request #123 from SPOTeam/feature/121
msk226 Jan 27, 2026
ac0402b
[Feature] 이미 탈퇴한 스터디에 재가입 뒤 탈퇴하는 경우도 체크
msk226 Jan 27, 2026
4f0cd5b
[Merge] Merge pull request #124 from SPOTeam/feature/121
msk226 Jan 27, 2026
87e99b8
[Feature] 스터디 탈퇴 관련 테스트 코드 정상화
msk226 Jan 27, 2026
bcb854a
[Feature] 스터디 탈퇴 관련 테스트 코드 정상화
msk226 Jan 27, 2026
e79d111
[Feature] 스터디 탈퇴 시 대상 회원 조회 로직 수정
msk226 Jan 27, 2026
100f341
Merge branch 'develop' into feature/121
msk226 Jan 27, 2026
869b7fc
[Merge] Merge pull request #125 from SPOTeam/feature/121
msk226 Jan 27, 2026
95be84e
[Feature] 요구사항 반영
msk226 Jan 29, 2026
9340f2a
[Feature] 불필요한 테스트 코드 비활성화
msk226 Jan 29, 2026
6f876c5
[Feature] 로직 수정으로 인한 테스트 코드 반영
msk226 Jan 29, 2026
fdbf6d2
[Merge] Merge pull request #126 from SPOTeam/feature/121
msk226 Jan 29, 2026
4481057
[Feature] 소셜 로그인 성공 시, 회원 ID 함께 응답
msk226 Jan 29, 2026
b69f2ef
[Feature] 변경사항 테스트 fixture에도 반영
msk226 Jan 29, 2026
d7382f0
[Merge] Merge pull request #127 from SPOTeam/feature/121
msk226 Jan 29, 2026
4f63e3b
[Feature] 읽지 않은 알림 존재 유무 확인 API 구현
msk226 Jan 29, 2026
3ecd333
[Feature] 알림 읽음 처리 API 구현
msk226 Jan 29, 2026
d2b032f
[Merge] Merge pull request #128 from SPOTeam/feature/121
msk226 Jan 29, 2026
c581a30
[Feature] 회고록 관련 요구사항 반영
msk226 Mar 12, 2026
cc0a3c9
[Merge] Merge pull request #130 from SPOTeam/feature/129
msk226 Mar 12, 2026
e4a06b1
[Feature] 스터디 일정 충돌 처리 로직 추가
msk226 Mar 13, 2026
a051e1f
[Merge] Merge pull request #131 from SPOTeam/feature/129
msk226 Mar 13, 2026
b869514
[Docs] 리드미 업데이트
msk226 Mar 14, 2026
f87737d
[Feature] isNow 필드 로직을 출석체크 진행 여부로 변경
msk226 Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPOT

## Summary

- 스터디 탐색, 운영, 커뮤니티 기능을 하나의 서비스로 묶은 멀티 모듈 백엔드 프로젝트
- `Java 21`, `Spring Boot`, `JPA`, `QueryDSL`, `MySQL`, `Redis`, `AWS S3`
- 모듈러 모놀리식 구조 기반으로 도메인 경계를 분리하고, 이벤트 기반 후처리와 배치 구조를 함께 운영
- 백엔드 개발 및 인프라 운영 전반 담당

---

## What I Did

- 대규모 데이터 환경에서 스터디 검색 API의 병목을 분석하고, 데이터 구조와 인덱스 설계를 조정해 응답 시간을 큰 폭으로 단축
- 조회수와 좋아요를 같은 통계성 데이터로 보지 않고, 쓰기 패턴 차이에 맞춰 Redis Write-Behind와 RDB 즉시 반영 전략으로 분리
- 외부 알림 호출이 비즈니스 API 안정성에 영향을 주지 않도록 트랜잭셔널 아웃박스 기반 비동기 발송 구조 설계
- OAuth 로그인 로직에 전략 패턴을 적용해 소셜 로그인 타입별 구현을 분리하고, 공통 처리 흐름을 재구성

---

## Highlights

- 검색 성능 개선: `10,986ms → 2.86ms`
- 대규모 조건 검색 환경에서 `300 RPS` 수준 처리
- 조회수 반영 구조 개선으로 DB 부하 대폭 감소
- 좋아요 처리에서 정합성을 유지하면서 동시성 처리 성능 개선
- 알림 시스템을 비동기화해 외부 장애 전파 차단
- 확장 가능한 로그인 구조로 유지보수성과 테스트 용이성 개선

---

## Architecture

- `Modular Monolith`
- 도메인별 모듈 분리: `auth`, `member`, `study`, `post`, `notification`, `region`
- 모듈 간 계약은 `*-api` 모듈로 분리
- Redis 기반 캐시/집계/쓰기 완충
- 스케줄러와 이벤트 리스너를 활용한 비동기 후처리

---

## Role & Contribution

- 백엔드 설계와 구현 전반 담당
- 성능 병목 분석, 데이터 구조 개선, 동시성 처리 전략 수립 주도
- Redis, 배치, 비동기 이벤트 기반 운영 구조 설계 및 적용
- 인증, 검색, 알림, 통계성 데이터 처리까지 핵심 기능 직접 구현
1 change: 1 addition & 0 deletions app/boot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation project(":modules:member")
implementation project(":modules:post")
implementation project(":modules:region")
implementation project(":modules:notification")

implementation project(":modules:member-api")
implementation project(":modules:auth-api")
Expand Down
11 changes: 11 additions & 0 deletions app/config/src/main/java/kr/spot/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,15 @@ public Executor imageUploadExecutor() {
executor.initialize();
return executor;
}

@Bean(name = "notificationExecutor")
public Executor notificationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notification-");
executor.initialize();
return executor;
}
}
17 changes: 17 additions & 0 deletions app/config/src/main/java/kr/spot/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.spot.config;

import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

@Bean
public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() {
return builder -> {
builder.serializerByType(Long.class, ToStringSerializer.instance);
};
}
}
58 changes: 57 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id "io.spring.dependency-management" version "1.1.6" apply false
id "org.springframework.boot" version "3.3.2" apply false
id "jacoco"
}

allprojects {
Expand All @@ -12,6 +13,7 @@ allprojects {
subprojects {
apply plugin: "java"
apply plugin: "io.spring.dependency-management"
apply plugin: "jacoco"

dependencyManagement {
imports { mavenBom "org.springframework.boot:spring-boot-dependencies:3.3.2" }
Expand All @@ -23,12 +25,66 @@ subprojects {
options.compilerArgs += "-parameters"
}

tasks.test { useJUnitPlatform() }
tasks.test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacoco {
toolVersion = "0.8.12"
}

jacocoTestReport {
dependsOn test
reports {
xml.required = true
html.required = true
}
afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, includes: [
'**/domain/**',
'**/application/**'
], excludes: [
'**/infrastructure/**',
'**/presentation/**',
'**/config/**'
])
}))
}
}

dependencies {
compileOnly "org.projectlombok:lombok"
annotationProcessor "org.projectlombok:lombok"
testCompileOnly "org.projectlombok:lombok"
testAnnotationProcessor "org.projectlombok:lombok"
}
}

// 전체 프로젝트 통합 커버리지 리포트
tasks.register('jacocoRootReport', JacocoReport) {
dependsOn subprojects.findAll { it.plugins.hasPlugin('jacoco') }.collect { it.tasks.named('test') }

additionalSourceDirs.from(subprojects.findAll { it.plugins.hasPlugin('java') }.collect { it.sourceSets.main.allSource.srcDirs })
sourceDirectories.from(subprojects.findAll { it.plugins.hasPlugin('java') }.collect { it.sourceSets.main.allSource.srcDirs })

def allClassDirs = subprojects.findAll { it.plugins.hasPlugin('java') }.collect { it.sourceSets.main.output }
classDirectories.from(files(allClassDirs).asFileTree.matching {
include '**/domain/**'
include '**/application/**'
exclude '**/infrastructure/**'
exclude '**/presentation/**'
exclude '**/config/**'
})

executionData.from(subprojects.findAll { it.plugins.hasPlugin('jacoco') }.collect {
it.tasks.named('test').map { task -> task.extensions.getByType(JacocoTaskExtension).destinationFile }
})

reports {
xml.required = true
html.required = true
html.outputLocation = layout.buildDirectory.dir('reports/jacoco/aggregated')
}
}
43 changes: 42 additions & 1 deletion common/api/src/main/java/kr/spot/code/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum ErrorStatus implements BaseErrorCode {
_MEMBER_UNSUPPORTED_LOGIN_TYPE(400, "MEMBER4003", "지원하지 않는 로그인 타입입니다."),
_MEMBER_EMAIL_EXIST(400, "MEMBER4004", "이미 해당 방식으로 가입 내역이 존재하는 이메일입니다."),
_FAIL_TO_UPDATE_NAME(400, "MEMBER4005", "이름 변경에 실패했습니다."),
_CANNOT_WITHDRAW_WITH_ACTIVE_STUDY(400, "MEMBER4006", "운영 중인 스터디가 있어 탈퇴할 수 없습니다."),

// 스터디 관련
_MAX_MEMBERS_MUST_BE_POSITIVE(400, "STUDY4000", "최대 인원 수는 양수여야 합니다."),
Expand All @@ -48,19 +49,59 @@ public enum ErrorStatus implements BaseErrorCode {
"셀프 승인을 위한 유효하지 않은 스터디 멤버 상태입니다."),
_STUDY_ALREADY_APPLIED(400, "STUDY4006", "이미 해당 스터디에 지원한 상태입니다."),
_STUDY_ACCESS_DENIED(403, "STUDY4032", "스터디에 접근할 권한이 없습니다."),
_INVALID_STUDY_ACCESS(403, "STUDY4033", "유효하지 않은 스터디 접근입니다."),
_STUDY_IS_FULL(400, "STUDY4007", "스터디 정원이 가득 찼습니다."),
_NEXT_OWNER_ID_REQUIRED_FOR_OWNER_WITHDRAWAL(400, "STUDY4008",
"스터디장 탈퇴 시 다음 스터디장 ID가 필요합니다."),
_NEXT_OWNER_NOT_EXIST_IN_STUDY(400, "STUDY4009",
"다음 스터디장이 스터디 멤버에 존재하지 않습니다."),
_CANNOT_DELETE_STUDY_WITH_MEMBERS(400, "STUDY4010",
"스터디 멤버가 있는 상태에서는 스터디를 삭제할 수 없습니다."),

// 게시글 관련
_POST_NOT_FOUND(404, "POST404", "게시글을 찾을 수 없습니다."),
_COMMENT_NOT_FOUND(404, "COMMENT404", "댓글을 찾을 수 없습니다."),
_ONLY_AUTHOR_CAN_MODIFY(403, "POST403", "게시글 및 댓글 수정은 작성자만 가능합니다."),
_PRIVATE_POST_ACCESS_DENIED(403, "POST4030", "스터디원 전용 게시글입니다."),
_ALREADY_LIKED(400, "POST4000", "이미 좋아요를 누른 게시글입니다."),
_ALREADY_UNLIKED(400, "POST4001", "좋아요를 누르지 않은 게시글입니다."),
_SORT_TYPE_INVALID(400, "POST4002", "유효하지 않은 정렬 타입입니다."),

// 지역 관련
_REGION_NOT_FOUND(404, "REGION404", "지역을 찾을 수 없습니다."),
_NO_SUCH_REGION(400, "REGION4000", "존재하지 않는 지역 코드입니다."),
;

// 일정 관련
_SCHEDULE_NOT_FOUND(404, "SCHEDULE404", "일정을 찾을 수 없습니다."),
_SCHEDULE_ACCESS_DENIED(403, "SCHEDULE403", "해당 스터디에 속하는 일정이 아닙니다."),
_SCHEDULE_TIME_CONFLICT(400, "SCHEDULE4001", "동일한 시간대에 이미 일정이 존재합니다."),
_SCHEDULE_QR_CODE_ALREADY_ASSIGNED(400, "SCHEDULE4000", "이미 출석 QR 코드가 할당된 일정입니다."),

// 출석 관련
_ATTENDANCE_NOT_IN_SCHEDULE_TIME(400, "ATTENDANCE4000", "일정 시간 외에는 출석체크를 진행할 수 없습니다."),
_ATTENDANCE_NOT_STARTED(400, "ATTENDANCE4001", "출석체크가 시작되지 않았습니다."),
_ATTENDANCE_ALREADY_CHECKED(400, "ATTENDANCE4002", "이미 출석체크를 완료했습니다."),
_INVALID_ATTENDANCE_TOKEN(400, "ATTENDANCE4003", "유효하지 않은 출석 토큰입니다."),

// 투두 관련
_TODO_NOT_FOUND(404, "TODO404", "투두를 찾을 수 없습니다."),
_TODO_ACCESS_DENIED(403, "TODO403", "해당 스터디에 속하는 투두가 아닙니다."),
_ONLY_TODO_OWNER_CAN_MODIFY(403, "TODO4030", "투두 수정은 작성자만 가능합니다."),

// 회고 관련
_REVIEW_NOT_FOUND(404, "REVIEW404", "회고를 찾을 수 없습니다."),
_REVIEW_ACCESS_DENIED(403, "REVIEW403", "해당 스터디에 속하는 회고가 아닙니다."),
_ALREADY_REACTED(400, "REVIEW4000", "이미 반응을 누른 회고입니다."),
_REACTION_NOT_FOUND(404, "REVIEW4040", "반응을 찾을 수 없습니다."),

// 알림 관련
_NOTIFICATION_NOT_FOUND(404, "NOTIFICATION404", "알림을 찾을 수 없습니다."),
_INVALID_NOTIFICATION_TYPE(400, "NOTIFICATION4000", "유효하지 않은 알림 유형입니다."),
_NOTIFICATION_PAYLOAD_MISSING_MEMBER_ID(400, "NOTIFICATION4001", "알림 페이로드에 memberId가 필요합니다."),
_NOTIFICATION_PAYLOAD_MISSING_STUDY_ID(400, "NOTIFICATION4002", "알림 페이로드에 studyId가 필요합니다."),
_NOTIFICATION_TEMPLATE_NOT_FOUND(500, "NOTIFICATION5000", "알림 템플릿을 찾을 수 없습니다."),
_PUSH_NOTIFICATION_FAILED(500, "NOTIFICATION5001", "푸시 알림 발송에 실패했습니다."),
_NOTIFICATION_ACCESS_DENIED(403, "NOTIFICATION403", "해당 알림에 접근할 권한이 없습니다.");

private final int httpStatus;
private final String code;
Expand Down
2 changes: 2 additions & 0 deletions common/view/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies {
}
6 changes: 6 additions & 0 deletions common/view/src/main/java/kr/spot/view/ViewAbuseGuard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.spot.view;

public interface ViewAbuseGuard {

boolean shouldCount(ViewableType type, long targetId, long viewerId);
}
8 changes: 8 additions & 0 deletions common/view/src/main/java/kr/spot/view/ViewCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.spot.view;

public interface ViewCounter {

long incrementAndGet(ViewableType type, long targetId);

long currentDelta(ViewableType type, long targetId);
}
15 changes: 15 additions & 0 deletions common/view/src/main/java/kr/spot/view/ViewableType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.spot.view;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ViewableType {

POST("post"),
STUDY("study"),
STUDY_BOARD("study-board");

private final String keyPrefix;
}
15 changes: 15 additions & 0 deletions infra/view-redis/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
id "io.spring.dependency-management"
id "java"
}

dependencies {
implementation project(":common:view")

// Spring Boot Starters
implementation "org.springframework.boot:spring-boot-starter-data-redis"

// Testing
testImplementation "org.springframework.boot:spring-boot-starter-test"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package kr.spot.infrastructure.redis;
package kr.spot.view.redis;

import java.time.Duration;
import kr.spot.application.ports.ViewAbuseGuard;
import kr.spot.view.ViewAbuseGuard;
import kr.spot.view.ViewableType;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
Expand All @@ -10,19 +11,19 @@
@RequiredArgsConstructor
public class RedisViewAbuseGuard implements ViewAbuseGuard {

public static final String GUARD_KEY = "view:guard:%d:%d";
public static final String VALUE = "1";
private static final String GUARD_KEY_FORMAT = "view:guard:%s:%d:%d";
private static final String VALUE = "1";
private static final Duration WINDOW = Duration.ofMinutes(10);

private final StringRedisTemplate redis;

private static String guardKey(long postId, long viewerId) {
return GUARD_KEY.formatted(postId, viewerId);
private static String guardKey(ViewableType type, long targetId, long viewerId) {
return GUARD_KEY_FORMAT.formatted(type.getKeyPrefix(), targetId, viewerId);
}

@Override
public boolean shouldCount(long postId, long viewerId) {
String key = guardKey(postId, viewerId);
public boolean shouldCount(ViewableType type, long targetId, long viewerId) {
String key = guardKey(type, targetId, viewerId);
Boolean ok = redis.opsForValue().setIfAbsent(key, VALUE, WINDOW);
return Boolean.TRUE.equals(ok);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.spot.view.redis;

import kr.spot.view.ViewCounter;
import kr.spot.view.ViewableType;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RedisViewCounter implements ViewCounter {

private static final String DELTA_KEY_FORMAT = "view:%s:%d";

private final StringRedisTemplate redis;

private static String deltaKey(ViewableType type, long targetId) {
return DELTA_KEY_FORMAT.formatted(type.getKeyPrefix(), targetId);
}

@Override
public long incrementAndGet(ViewableType type, long targetId) {
Long v = redis.opsForValue().increment(deltaKey(type, targetId));
return v == null ? 0L : v;
}

@Override
public long currentDelta(ViewableType type, long targetId) {
String v = redis.opsForValue().get(deltaKey(type, targetId));
return (v == null) ? 0L : Long.parseLong(v);
}
}
Loading
Loading