21
.opencode/commands/commit.md
Normal file
21
.opencode/commands/commit.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
description: commit-policy 스킬을 로드해 커밋 메시지 생성과 전후 검증을 수행한다
|
||||||
|
agent: build
|
||||||
|
subtask: true
|
||||||
|
---
|
||||||
|
|
||||||
|
작업 목표:
|
||||||
|
현재 변경사항을 안전하게 커밋한다.
|
||||||
|
|
||||||
|
필수 시작 단계:
|
||||||
|
1. `skill` 도구로 `commit-policy` 스킬을 먼저 로드한다.
|
||||||
|
- `skill({ name: "commit-policy" })`
|
||||||
|
|
||||||
|
실행 단계:
|
||||||
|
1. 로드한 `commit-policy` 스킬의 Hard Requirements와 Execution Flow를 그대로 수행한다.
|
||||||
|
2. `AGENTS.md`의 최소 정책(형식/한글 description/검증 스크립트)을 항상 만족한다.
|
||||||
|
3. `$ARGUMENTS`가 있으면 scope 또는 description 의도에 반영하되, 스킬 규칙과 형식을 깨지 않는다.
|
||||||
|
4. 마지막에 실행 명령과 pre-check/post-check PASS/FAIL 핵심 결과를 간단히 보고한다.
|
||||||
|
|
||||||
|
추가 사용자 의도:
|
||||||
|
$ARGUMENTS
|
||||||
46
.opencode/skills/commit-policy/SKILL.md
Normal file
46
.opencode/skills/commit-policy/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: commit-policy
|
||||||
|
description: Apply this skill for any git commit task in this repository. It enforces commit message format and validation flow defined in AGENTS.md and work/scripts/check-commit-message-rules.sh, including pre-commit and post-commit verification.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Commit Policy Skill
|
||||||
|
|
||||||
|
Use this workflow whenever the task includes creating a commit.
|
||||||
|
|
||||||
|
## Required References
|
||||||
|
|
||||||
|
- `@AGENTS.md`
|
||||||
|
- `@work/scripts/check-commit-message-rules.sh`
|
||||||
|
|
||||||
|
## Hard Requirements
|
||||||
|
|
||||||
|
1. Use commit subject format: `<type>(scope): <description>`.
|
||||||
|
2. `type` must be lowercase (for example `feat`, `fix`, `chore`, `docs`, `refactor`, `test`).
|
||||||
|
3. `description` must include Korean text and stay concise in imperative present tone.
|
||||||
|
4. Optional footer must use `Refs: #123` or `Refs: #123, #456` format.
|
||||||
|
5. Never commit secret files (`.env`, key/token/secret credential files).
|
||||||
|
6. Never bypass hooks with `--no-verify`.
|
||||||
|
|
||||||
|
## Execution Flow
|
||||||
|
|
||||||
|
1. Inspect context with:
|
||||||
|
- `git status`
|
||||||
|
- `git diff --cached`
|
||||||
|
- `git diff`
|
||||||
|
- `git log -5 --oneline`
|
||||||
|
2. Stage commit target files only. Exclude suspicious secret-bearing files.
|
||||||
|
3. Draft commit message from the change intent (focus on why, not only what).
|
||||||
|
4. Run pre-commit validation with the full draft message:
|
||||||
|
- `./work/scripts/check-commit-message-rules.sh --message "<full message>"`
|
||||||
|
5. If validation fails, revise message and re-run until PASS.
|
||||||
|
6. Commit using the validated message.
|
||||||
|
7. Run post-commit validation:
|
||||||
|
- `./work/scripts/check-commit-message-rules.sh`
|
||||||
|
8. Report executed commands and PASS/FAIL summary.
|
||||||
|
|
||||||
|
## Output Checklist
|
||||||
|
|
||||||
|
- Final commit subject.
|
||||||
|
- Whether pre-check passed.
|
||||||
|
- Whether post-check passed.
|
||||||
|
- Any excluded files and reason.
|
||||||
165
AGENTS.md
165
AGENTS.md
@@ -1,37 +1,148 @@
|
|||||||
> 이 문서는 본 저장소에서 **AI Coding Agent가 반드시 따라야 할 개발 헌법(운영 규칙)**이다.
|
# AGENTS.md
|
||||||
> 모든 신규 코드는 본 문서를 최우선 기준으로 작성한다.
|
|
||||||
|
|
||||||
---
|
## 문서 목적
|
||||||
|
- 이 문서는 `/Users/klaus/Develop/sodalive/Server/sodalive` 저장소에서 작업하는 에이전트용 실행 가이드다.
|
||||||
|
- 목표는 "추측 최소화 + 기존 패턴 준수 + 검증 우선"이다.
|
||||||
|
- 이 문서의 규칙은 코드/테스트/문서 변경 모두에 적용한다.
|
||||||
|
|
||||||
## 0. 전제
|
## 커뮤니케이션 규칙
|
||||||
질문에 대한 답변과 설명은 한국어로 한다.
|
- **"질문에 대한 답변과 설명은 한국어로 한다."**
|
||||||
|
- 이 저장소에서 사용자에게 전달하는 설명, 진행 상황, 결과 보고는 한국어로 작성한다.
|
||||||
|
- 코드 식별자, 경로, 명령어는 원문(영문) 그대로 유지한다.
|
||||||
|
|
||||||
---
|
## 프로젝트 개요
|
||||||
|
- 빌드 도구: Gradle Wrapper (`./gradlew`)
|
||||||
|
- 언어/런타임: Kotlin + Java 17
|
||||||
|
- 프레임워크: Spring Boot 2.7.14
|
||||||
|
- 주요 플러그인: `org.jlleitschuh.gradle.ktlint`
|
||||||
|
- 단일 루트 프로젝트: `settings.gradle.kts`의 `rootProject.name = "sodalive"`
|
||||||
|
|
||||||
## 15. Commit Standards
|
## 실행 명령어 (Build/Lint/Test)
|
||||||
|
아래 명령은 저장소 루트(`/Users/klaus/Develop/sodalive/Server/sodalive`)에서 실행한다.
|
||||||
|
|
||||||
1. 커밋 메시지는 **반드시 한국어로 작성한다.**
|
```bash
|
||||||
2. 제목(subject)은 **현재형**으로 작성한다. (예: “기능 추가”, “기능 추가함” 금지)
|
./gradlew tasks --all
|
||||||
3. 제목은 **50자 이내**로 작성한다.
|
./gradlew bootRun
|
||||||
4. 제목과 본문 사이에는 **반드시 한 줄 공백**을 둔다.
|
./gradlew build
|
||||||
5. 본문은 **한 줄당 72자 이내**로 작성한다.
|
./gradlew clean build
|
||||||
6. 하나의 문단에서는 72자를 초과할 때만 줄바꿈한다.
|
./gradlew test
|
||||||
7. **공개 API 변경 사항만 설명**하며, 패키지 프라이빗 구현 상세는 포함하지 않는다.
|
./gradlew check
|
||||||
8. **테스트 코드에 대한 언급은 커밋 메시지에 포함하지 않는다.**
|
./gradlew ktlintCheck
|
||||||
9. 제목에 `fix:`, `feat:`, `docs:` 등의 **접두어를 사용하지 않는다.**
|
./gradlew ktlintFormat
|
||||||
10. 제목은 **첫 글자를 대문자로 시작**한다. 단, 함수명 등 소문자가 합당한 경우만 예외를 허용한다.
|
```
|
||||||
11. 도구 광고, 브랜딩, 홍보성 문구를 **절대 포함하지 않는다.**
|
|
||||||
12. 커밋 전에는 **반드시 파일을 개별 stage 한다.**
|
|
||||||
13. 커밋 전 **`work/scripts/check-commit-message-rules.sh` 검증을 통과해야 한다.**
|
|
||||||
|
|
||||||
---
|
## 코드 스타일 규칙
|
||||||
|
|
||||||
## 16. AI 사용 규칙 (AI Interaction Rules)
|
### 1) 포맷/기본 규칙
|
||||||
|
- `.editorconfig` 기준을 준수한다.
|
||||||
|
- 인덴트: 공백 4칸.
|
||||||
|
- 줄바꿈: LF.
|
||||||
|
- 최대 라인 길이: 130.
|
||||||
|
- 파일 끝 개행 유지, trailing whitespace 제거.
|
||||||
|
|
||||||
- 매우 작은 단위의 변경만 수행한다.
|
### 2) import 규칙
|
||||||
- 대규모 리팩터링은 반드시 사전 승인을 요청한다.
|
- 와일드카드 import(`*`)를 사용하지 않는다.
|
||||||
|
- 사용하지 않는 import를 남기지 않는다.
|
||||||
|
- import alias(`as`)는 현재 코드베이스에서 사용 사례가 없으므로 지양한다.
|
||||||
|
- 기존 파일의 import 정렬/그룹 스타일을 그대로 맞춘다.
|
||||||
|
|
||||||
---
|
### 3) 네이밍 규칙
|
||||||
|
- 클래스/인터페이스/enum: PascalCase.
|
||||||
|
- 함수/변수/파라미터: camelCase.
|
||||||
|
- 상수: UPPER_SNAKE_CASE (`companion object` 내부 `const val`).
|
||||||
|
- Request/Response DTO는 `...Request`, `...Response` 접미사를 유지한다.
|
||||||
|
- 서비스/컨트롤러/리포지토리 명명은 역할 접미사(`Service`, `Controller`, `Repository`)를 유지한다.
|
||||||
|
|
||||||
✅ 본 문서는 **AI와 사람이 동일한 개발 규칙을 공유하기 위한 최상위 기준 문서**이며,
|
### 4) 타입/널 처리
|
||||||
✅ 모든 신규 코드는 본 문서를 기준으로 검토된다.
|
- Kotlin 타입 시스템을 활용하고 nullable(`?`)를 명시한다.
|
||||||
|
- 불필요한 `Any`/약한 타입을 피하고 구체 타입을 우선한다.
|
||||||
|
- 기존 코드에서 `!!` 사용이 많지만, 신규 코드는 가능한 안전 호출/가드절/명시적 예외로 대체를 우선 고려한다.
|
||||||
|
|
||||||
|
### 5) API/응답 규칙
|
||||||
|
- API 응답은 `ApiResponse.ok(...)`, `ApiResponse.error(...)` 패턴을 따른다.
|
||||||
|
- 컨트롤러는 도메인 예외를 직접 포맷하지 말고 `SodaException`을 던진다.
|
||||||
|
- 인증 사용자 필요 시 `@AuthenticationPrincipal(... ) member: Member?` 패턴 + null 가드절을 사용한다.
|
||||||
|
|
||||||
|
### 6) 예외 처리 규칙
|
||||||
|
- 비즈니스 예외는 `SodaException(messageKey = "...")` 우선 사용.
|
||||||
|
- 사용자 노출 문구는 하드코딩보다 `messageKey` 기반 i18n을 우선한다.
|
||||||
|
- 공통 예외 변환은 `SodaExceptionHandler`에서 수행하므로, 개별 컨트롤러에서 중복 처리하지 않는다.
|
||||||
|
- 예외를 삼키는 빈 `catch` 블록을 금지한다.
|
||||||
|
|
||||||
|
### 7) 트랜잭션 규칙
|
||||||
|
- 서비스 계층에서 `@Transactional`을 사용한다.
|
||||||
|
- 조회 위주 메서드는 `@Transactional(readOnly = true)`를 우선한다.
|
||||||
|
- 쓰기 로직은 메서드 단위 `@Transactional`로 경계를 명확히 한다.
|
||||||
|
|
||||||
|
### 8) 비동기/동시성 규칙
|
||||||
|
- 비동기 처리는 Kotlin Coroutines 패턴을 따른다.
|
||||||
|
- `CoroutineScope(Dispatchers.IO)` + `launch` + 예외 처리 패턴을 일관되게 유지한다.
|
||||||
|
- 생명주기 종료 시 scope 정리(`@PreDestroy`) 패턴을 참고한다.
|
||||||
|
|
||||||
|
### 9) 의존성 주입
|
||||||
|
- 생성자 주입(primary constructor + `private val`)을 기본으로 사용한다.
|
||||||
|
- 필드 주입보다 명시적 생성자 주입을 우선한다.
|
||||||
|
|
||||||
|
## 테스트 스타일 규칙
|
||||||
|
- 테스트 프레임워크: JUnit 5 (`useJUnitPlatform()`)
|
||||||
|
- 목킹: Mockito 사용 패턴 존재 (`Mockito.mock`, ``Mockito.`when`(...)``)
|
||||||
|
- 검증: `assertEquals`, `assertThrows` 패턴 준수.
|
||||||
|
- 테스트 이름은 의도가 드러나는 영어 문장형(`should...`)을 유지한다.
|
||||||
|
|
||||||
|
## 설정/보안 유의사항
|
||||||
|
- `application.yml`은 다수의 `${ENV_VAR}`를 사용한다.
|
||||||
|
- 비밀값(API Key, Secret, Token, DB 비밀번호)을 코드/문서/로그에 평문으로 남기지 않는다.
|
||||||
|
- 환경변수/시크릿 파일은 커밋 대상에서 제외한다.
|
||||||
|
|
||||||
|
## Cursor/Copilot 규칙 반영
|
||||||
|
`/.cursorrules`, `/.cursor/rules/`, `/.github/copilot-instructions.md` 파일은 현재 없다.
|
||||||
|
별도 규칙 파일이 추가되면 본 문서보다 해당 규칙을 우선 반영한다.
|
||||||
|
|
||||||
|
## 커밋 메시지 규칙 (표준 Conventional Commits)
|
||||||
|
- 커밋 상세 가이드/절차는 `.opencode/skills/commit-policy/SKILL.md`를 단일 기준으로 사용한다.
|
||||||
|
- 커밋 작업 시작 시 `skill` 도구로 `commit-policy`를 먼저 로드한다.
|
||||||
|
- 기본 형식은 `<type>(scope): <description>`를 사용한다.
|
||||||
|
- `type`은 소문자(`feat`, `fix`, `chore`, `docs`, `refactor`, `test` 등)를 사용한다.
|
||||||
|
- 제목(description)은 한글로 작성하고, 명령형/간결한 현재형으로 작성한다.
|
||||||
|
- 이슈 참조 footer는 `Refs: #123` 또는 `Refs: #123, #456` 형식을 사용한다.
|
||||||
|
|
||||||
|
### 커밋 메시지 검증 절차
|
||||||
|
- `git commit` 실행 직전에 `work/scripts/check-commit-message-rules.sh`를 실행해 규칙 준수 여부를 확인한다.
|
||||||
|
- `git commit` 실행 직후에도 `work/scripts/check-commit-message-rules.sh`를 다시 실행해 최종 메시지를 재검증한다.
|
||||||
|
- 스크립트 결과가 `[FAIL]`이면 커밋 메시지를 규칙에 맞게 수정한 뒤 다시 검증한다.
|
||||||
|
|
||||||
|
## 작업 절차 체크리스트
|
||||||
|
- 변경 전: 유사 기능 코드를 먼저 찾아 네이밍/예외/응답 패턴을 맞춘다.
|
||||||
|
- 변경 중: 공개 API 스키마를 임의 변경하지 말고, 작은 단위로 안전하게 수정한다.
|
||||||
|
- 변경 후: 최소 단일 테스트 또는 `./gradlew test`를 실행하고, 필요 시 `./gradlew ktlintCheck`를 수행한다.
|
||||||
|
- 커밋 전/후: `commit-policy` 스킬을 먼저 로드하고, `git commit` 직전과 직후에 `work/scripts/check-commit-message-rules.sh`를 실행해 커밋 메시지 규칙 준수 여부를 확인한다.
|
||||||
|
|
||||||
|
## 작업 계획 문서 규칙 (docs)
|
||||||
|
- 모든 작업 시작 전에 `docs` 폴더 아래에 계획 문서를 먼저 생성하고, 해당 문서를 기준으로 구현을 진행한다.
|
||||||
|
- 계획 문서 파일명은 `[날짜]_구현할내용한글.md` 형식을 사용한다.
|
||||||
|
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||||
|
- 파일명 예시: `20260101_구글계정으로로그인.md`
|
||||||
|
- 구현 항목은 기능/작업 단위로 분리해 체크박스(`- [ ]`) 목록으로 작성한다.
|
||||||
|
- 구현 완료 시마다 체크박스를 `- [x]`로 갱신하고, 각 항목이 정상 구현되었는지 확인한다.
|
||||||
|
- 작업 도중 범위가 변경되면 계획 문서의 체크박스 항목을 먼저 업데이트한 뒤 구현을 진행한다.
|
||||||
|
- 모든 구현이 끝난 후 결과 보고 시 계획 문서 맨 아래에 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 기록한다.
|
||||||
|
- 후속 수정이 발생해도 기존 검증 기록은 삭제/덮어쓰지 않고 누적한다(예: `1차 구현`, `2차 수정`).
|
||||||
|
- 검증 기록은 단계별로 `무엇을/왜/어떻게`를 유지해 작성하고, 이전 단계와 구분이 되도록 명시한다.
|
||||||
|
- 단계별 `어떻게`에는 실제 실행한 검증 명령과 결과(성공/실패/불가 사유)를 함께 기록한다.
|
||||||
|
- 기존 기록 정정이 필요하면 원문을 지우지 말고 `정정` 항목을 추가해 사유와 변경 내용을 남긴다.
|
||||||
|
|
||||||
|
## 문서 유지보수 규칙
|
||||||
|
- `build.gradle.kts` 변경 시 실행 명령 섹션을 함께 갱신한다.
|
||||||
|
- 테스트 클래스 추가/이동 시 단일 테스트 실행 예시를 최신 상태로 유지한다.
|
||||||
|
- `.editorconfig` 변경 시 포맷 규칙 섹션을 동기화한다.
|
||||||
|
- Cursor/Copilot 규칙 파일이 생기면 해당 내용을 이 문서에 반영한다.
|
||||||
|
- 문서 변경 후 최소 한 번 `./gradlew tasks --all`로 명령 유효성을 확인한다.
|
||||||
|
- 불확실한 규칙은 추측으로 채우지 말고 근거 파일 경로를 먼저 확인한다.
|
||||||
|
- 에이전트 안내 문구는 한국어 중심으로 유지한다.
|
||||||
|
- 커밋 규칙 예시는 팀 컨벤션 변경 시 즉시 업데이트한다.
|
||||||
|
|
||||||
|
## 에이전트 동작 원칙
|
||||||
|
- 추측하지 말고, 근거 파일을 읽고 결정한다.
|
||||||
|
- 기존 관례를 깨는 변경은 이유가 명확할 때만 수행한다.
|
||||||
|
- 불필요한 리팩터링 확장은 피하고 요청 범위를 우선 충족한다.
|
||||||
|
- 결과 보고 시 무엇을, 왜, 어떻게 검증했는지 한국어로 간단히 남긴다.
|
||||||
|
|||||||
14
docs/20260220_lsp설정추가.md
Normal file
14
docs/20260220_lsp설정추가.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 20260220 LSP 설정 추가
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
- [x] oh-my-opencode 설정 파일에서 현재 LSP 매핑을 확인한다.
|
||||||
|
- [x] `.md` 확장자에 `remark-language-server` 매핑을 추가하고, `.sh`는 기존 `bash` 서버 설정이 정상 동작하는지 확인한다.
|
||||||
|
- [x] 수정 후 `lsp_diagnostics`로 Bash/Markdown 파일 진단이 가능한지 검증한다.
|
||||||
|
- [x] 저장소 문서 변경 검증을 위해 `./gradlew tasks --all`을 실행한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- [x] 작업 완료 후 검증 결과를 기록한다.
|
||||||
|
|
||||||
|
- 무엇을: `/Users/klaus/.config/opencode/oh-my-opencode.json`에 `remark-language-server --stdio` 기반 `.md` 매핑을 추가했다.
|
||||||
|
- 왜: Bash는 설치 후 즉시 진단 가능했지만 Markdown은 LSP 매핑이 없어 `lsp_diagnostics`가 실패했기 때문이다.
|
||||||
|
- 어떻게 검증했는지: `work/scripts/check-commit-message-rules.sh`와 `docs/20260220_lsp설정추가.md`에 대해 `lsp_diagnostics`를 실행해 모두 `No diagnostics found`를 확인했다. 추가로 `./gradlew tasks --all`, `./gradlew build`를 실행해 `BUILD SUCCESSFUL`을 확인했다.
|
||||||
4
docs/20260220_member_fancimm_x_url_ddl.sql
Normal file
4
docs/20260220_member_fancimm_x_url_ddl.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE member
|
||||||
|
ADD fancimm_url VARCHAR(255) DEFAULT NULL COMMENT '팬심M url' AFTER instagram_url,
|
||||||
|
ADD x_url VARCHAR(255) DEFAULT NULL COMMENT 'X url' AFTER fancimm_url
|
||||||
|
;
|
||||||
22
docs/20260220_삭제닉네임접두사표시정리.md
Normal file
22
docs/20260220_삭제닉네임접두사표시정리.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 20260220 삭제 닉네임 접두사 표시 정리
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
- [x] 콘텐츠 댓글, 팬톡 응원, 커뮤니티 댓글의 닉네임 표시 흐름(조회/매핑/응답 DTO)을 각각 식별한다.
|
||||||
|
- [x] 닉네임이 `deleted_`로 시작하는지 판별하고 표시 시 접두사만 제거하는 공통 처리 지점을 설계한다.
|
||||||
|
- [x] 콘텐츠 댓글 표시 로직에 `deleted_` 접두사 제거 규칙을 적용한다.
|
||||||
|
- [x] 팬톡 응원 표시 로직에 `deleted_` 접두사 제거 규칙을 적용한다.
|
||||||
|
- [x] 커뮤니티 댓글 표시 로직에 `deleted_` 접두사 제거 규칙을 적용한다.
|
||||||
|
- [x] `deleted_` 미포함 닉네임, `deleted_` 포함 닉네임, 접두사만 존재하는 경계 케이스를 기준으로 테스트 케이스를 추가/보강한다.
|
||||||
|
|
||||||
|
## 검증 계획
|
||||||
|
- [x] 닉네임 표시에 영향이 있는 테스트를 우선 실행하고 실패 시 원인을 보정한다.
|
||||||
|
- [x] `./gradlew test`를 실행해 회귀 여부를 확인한다.
|
||||||
|
- [x] 필요 시 `./gradlew ktlintCheck`로 스타일 규칙 위반 여부를 확인한다.
|
||||||
|
- [x] `./gradlew build`를 실행해 전체 빌드 성공을 확인한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- [x] 작업 완료 후 검증 결과를 기록한다.
|
||||||
|
|
||||||
|
- 무엇을: `String.removeDeletedNicknamePrefix()` 공통 확장 함수를 추가하고, 콘텐츠 댓글(`AudioContentCommentRepository`), 팬톡 응원(`ExplorerQueryRepository#getCheersList`), 커뮤니티 댓글(`CreatorCommunityCommentRepository`) 응답 닉네임에 동일 규칙을 적용했다.
|
||||||
|
- 왜: 탈퇴/비활성 사용자 닉네임 저장 정책(`deleted_` 접두사 유지)과 화면 표시 정책(접두사 제거)을 분리해, 사용자에게는 일관된 표시값을 제공하기 위해서다.
|
||||||
|
- 어떻게 검증했는지: `./gradlew test --tests "kr.co.vividnext.sodalive.extensions.StringExtensionsTest"`, `./gradlew test`, `./gradlew ktlintCheck`, `./gradlew build`를 실행해 모두 `BUILD SUCCESSFUL`을 확인했다. 또한 경계 케이스(`deleted_testUser`, `testUser`, `deleted_`) 단위 테스트를 추가해 기대 출력이 각각 `testUser`, `testUser`, `""`인지 검증했다.
|
||||||
15
docs/20260220_커밋규칙스킬분리.md
Normal file
15
docs/20260220_커밋규칙스킬분리.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 20260220 커밋 규칙 스킬 분리
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
- [x] 커밋 메시지 정책의 최소 필수 항목을 `AGENTS.md`에 유지한다.
|
||||||
|
- [x] 커밋 상세 절차와 실행 가이드를 `.opencode/skills/commit-policy/SKILL.md`로 분리한다.
|
||||||
|
- [x] `/commit` 커맨드가 커밋 작업 시작 시 `commit-policy` 스킬을 우선 로드하도록 갱신한다.
|
||||||
|
- [x] 커밋 검증 강제 수단(`work/scripts/check-commit-message-rules.sh`)이 유지되는지 확인한다.
|
||||||
|
- [x] 문서 변경 검증을 위해 `./gradlew tasks --all`을 실행한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- [x] 작업 완료 후 검증 결과를 기록한다.
|
||||||
|
|
||||||
|
- 무엇을: `AGENTS.md`의 커밋 섹션을 최소 정책(형식, 한글 description, 검증 절차, 스킬 로드 지침) 중심으로 정리하고, 상세 절차를 `.opencode/skills/commit-policy/SKILL.md`로 분리했다. `/commit` 커맨드(`.opencode/commands/commit.md`)는 실행 시 `commit-policy` 스킬을 먼저 로드하도록 변경했다.
|
||||||
|
- 왜: 커밋 상세 규칙을 상시 컨텍스트에서 분리해 토큰 사용량을 줄이면서도, 커밋 시점에는 스킬 로드로 동일한 절차를 강제하기 위해서다.
|
||||||
|
- 어떻게 검증했는지: `AGENTS.md`, `.opencode/commands/commit.md`, `.opencode/skills/commit-policy/SKILL.md`, `docs/20260220_커밋규칙스킬분리.md`에 대해 `lsp_diagnostics`를 실행해 모두 `No diagnostics found`를 확인했다. 추가로 `./gradlew tasks --all`과 `./gradlew build`를 실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
|
||||||
15
docs/20260220_커밋메시지검증규칙추가.md
Normal file
15
docs/20260220_커밋메시지검증규칙추가.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 20260220 커밋 메시지 검증 규칙 추가
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
- [x] AGENTS.md의 커밋 메시지 규칙 섹션에 커밋 전/후 검증 절차를 추가한다.
|
||||||
|
- [x] AGENTS.md의 작업 절차 체크리스트에 커밋 전/후 스크립트 실행 규칙을 추가한다.
|
||||||
|
- [x] 문서 변경 검증을 위해 `./gradlew tasks --all`을 실행한다.
|
||||||
|
- [x] AGENTS.md 커밋 메시지 규칙과 불일치하는 `work/scripts/check-commit-message-rules.sh` 검증 로직을 정합성 있게 수정한다.
|
||||||
|
- [x] 수정한 스크립트에 대해 문법 및 실행 검증을 수행한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- [x] 검증 결과를 작업 완료 후 기록한다.
|
||||||
|
|
||||||
|
- 무엇을: `AGENTS.md`에 커밋 전/후 검증 절차를 추가했고, `work/scripts/check-commit-message-rules.sh`를 AGENTS.md 기준(Conventional Commit 형식, 소문자 type, 한글 description, `Refs:` footer 형식)으로 정합성 있게 수정했다.
|
||||||
|
- 왜: 문서 규칙과 실제 검증 로직이 어긋나면 커밋 메시지 정책이 일관되게 강제되지 않기 때문이다.
|
||||||
|
- 어떻게 검증했는지: `bash -n ./work/scripts/check-commit-message-rules.sh`, 유효/무효 메시지 실행 검증(`--message`), `Refs` footer 유효/무효 케이스 검증을 수행했다. 추가로 `./gradlew tasks --all`과 `./gradlew build`를 실행해 저장소 명령 유효성과 전체 빌드 성공(`BUILD SUCCESSFUL`)을 확인했다.
|
||||||
15
docs/20260220_커스텀커맨드커밋추가.md
Normal file
15
docs/20260220_커스텀커맨드커밋추가.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 20260220 커스텀 커맨드 /commit 추가
|
||||||
|
|
||||||
|
## 구현 계획
|
||||||
|
- [x] `.opencode/commands/` 디렉터리에 `/commit` 커맨드 파일을 추가한다.
|
||||||
|
- [x] `/commit` 커맨드가 AGENTS.md 커밋 메시지 규칙(`type(scope): description`, 소문자 type, 한글 description)을 따르도록 지시한다.
|
||||||
|
- [x] `/commit` 커맨드가 커밋 직전 `./work/scripts/check-commit-message-rules.sh --message` 검증을 수행하도록 지시한다.
|
||||||
|
- [x] `/commit` 커맨드가 커밋 직후 `./work/scripts/check-commit-message-rules.sh` 재검증을 수행하도록 지시한다.
|
||||||
|
- [x] 문서 변경 검증을 위해 `./gradlew tasks --all`을 실행한다.
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- [x] 작업 완료 후 검증 결과를 기록한다.
|
||||||
|
|
||||||
|
- 무엇을: `.opencode/commands/commit.md`에 `/commit` 커스텀 커맨드를 추가해 변경사항 분석, AGENTS.md 규칙 기반 커밋 메시지 생성, 커밋 전/후 검증 스크립트 실행 절차를 일관되게 지시하도록 구성했다.
|
||||||
|
- 왜: 저장소의 커밋 메시지 컨벤션(Conventional Commit + 한글 description + Refs footer 규칙)과 검증 절차를 반복 작업마다 동일하게 강제하기 위해서다.
|
||||||
|
- 어떻게 검증했는지: `.opencode/commands/commit.md`, `docs/20260220_커스텀커맨드커밋추가.md`에 대해 `lsp_diagnostics`를 실행해 모두 `No diagnostics found`를 확인했고, `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`을 확인했다.
|
||||||
14
docs/20260220_팬심M와XURL추가.md
Normal file
14
docs/20260220_팬심M와XURL추가.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 팬심M/X URL 추가 작업 계획
|
||||||
|
|
||||||
|
- [x] `Member` 엔티티 SNS 필드에 팬심M URL, X URL 속성 추가
|
||||||
|
- [x] 인스타그램 URL 수정 흐름 분석 후 동일한 수정 요청 DTO 반영
|
||||||
|
- [x] 서비스의 프로필 수정 로직에 팬심M URL, X URL 수정 처리 추가
|
||||||
|
- [x] 관련 응답 DTO에 신규 URL 필드 반영 및 매핑 연결
|
||||||
|
- [x] 후속 요청 반영: `fansimMUrl` 필드명을 `fancimmUrl`로 일괄 변경
|
||||||
|
- [x] `ddl-auto: validate` 대응을 위한 DB 컬럼 추가 SQL 파일 생성
|
||||||
|
- [x] 진단/테스트/빌드 검증 실행 후 결과 기록
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 무엇을: 팬심M/X URL 필드 추가, 인스타그램 URL 수정 흐름과 동일한 수정/응답 매핑 반영, `fansimMUrl` -> `fancimmUrl` 명칭 변경을 검증했다.
|
||||||
|
- 왜: 프로필 수정 API에서 두 URL이 저장되고, 주요 응답 DTO에서 값이 일관되게 내려가야 하기 때문이다.
|
||||||
|
- 어떻게: `./gradlew ktlintCheck test build`를 팬심M/X URL 추가 시점과 `fancimmUrl` 명칭 변경 시점에 각각 실행해 정적 검사, 테스트, 빌드 성공(Exit code 0)을 확인했다. 또한 `docs/20260220_member_fancimm_x_url_ddl.sql`에 운영 DB 반영용 DDL을 추가했다. Kotlin LSP 미구성으로 `lsp_diagnostics`는 수행할 수 없었다.
|
||||||
18
docs/20260223_channel_donation_message_ddl.sql
Normal file
18
docs/20260223_channel_donation_message_ddl.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE channel_donation_message
|
||||||
|
(
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'PK',
|
||||||
|
member_id BIGINT NOT NULL COMMENT '후원한 유저',
|
||||||
|
creator_id BIGINT NOT NULL COMMENT '후원 받은 채널 크리에이터',
|
||||||
|
can INT NOT NULL COMMENT '후원한 캔',
|
||||||
|
is_secret TINYINT(1) NOT NULL DEFAULT 0 COMMENT '비밀후원 여부(false=0, true=1)',
|
||||||
|
additional_message TEXT NULL COMMENT '추가 메시지',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_channel_donation_message_creator_created_at (creator_id, created_at),
|
||||||
|
KEY idx_channel_donation_message_member (member_id),
|
||||||
|
CONSTRAINT fk_channel_donation_message_member
|
||||||
|
FOREIGN KEY (member_id) REFERENCES member (id),
|
||||||
|
CONSTRAINT fk_channel_donation_message_creator
|
||||||
|
FOREIGN KEY (creator_id) REFERENCES member (id)
|
||||||
|
) COMMENT ='채널 후원 메시지';
|
||||||
17
docs/20260223_차단유저댓글및크리에이터노출차단.md
Normal file
17
docs/20260223_차단유저댓글및크리에이터노출차단.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 차단 유저 댓글 및 크리에이터 노출 차단 구현
|
||||||
|
|
||||||
|
- [x] 차단(`BlockMember`) 데이터 접근 패턴 및 기존 필터 지점 확인
|
||||||
|
- [x] 콘텐츠 댓글 목록에서 차단한 유저 댓글 비노출 적용
|
||||||
|
- [x] 채널 응원 목록에서 차단한 유저 댓글 비노출 적용
|
||||||
|
- [x] 커뮤니티 댓글 목록에서 차단한 유저 댓글 비노출 적용
|
||||||
|
- [x] 차단한 크리에이터의 콘텐츠/라이브 비노출 동작 보강
|
||||||
|
- [x] 변경 파일 진단 및 테스트/빌드 검증
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
- 무엇을: 리뷰에서 지적된 단방향 차단 누락을 기준으로 콘텐츠/라이브/콘텐츠 댓글/커뮤니티 댓글/채널 응원(cheers) 노출 경로를 재점검해, 한쪽이라도 차단 관계면 조회·검색·상세 접근에서 숨겨지도록 양방향 차단 로직으로 보강했다. `/explorer/profile/{id}/cheers`의 우회 접근도 양방향 차단으로 막았다.
|
||||||
|
- 왜: 사용자 차단 정책을 일관되게 적용해 차단한 유저와 차단한 크리에이터의 활동이 조회 결과에 보이지 않도록 하기 위함이다.
|
||||||
|
- 어떻게 검증했는가:
|
||||||
|
- `lsp_diagnostics`를 수정 Kotlin 파일들에 대해 실행했으나, 현재 환경에 `.kt` LSP 서버가 설정되어 있지 않아 진단 불가를 확인했다.
|
||||||
|
- `./gradlew test` 실행 성공.
|
||||||
|
- `./gradlew build -x test` 실행 성공(ktlint/check 포함).
|
||||||
67
docs/20260223_채널후원기능추가.md
Normal file
67
docs/20260223_채널후원기능추가.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 채널 후원 기능 추가 작업 계획
|
||||||
|
|
||||||
|
## 메시지 저장 전략 선택
|
||||||
|
- 선택: 기본 메시지는 DB에 저장하지 않고, 후원 이력에는 `can`, `isSecret`, `additionalMessage`를 저장한 뒤 리스트 조회 시 메시지를 생성한다.
|
||||||
|
- 이유: 일반/비밀 구분과 캔 수 노출 요구를 구조화 필드로 충족할 수 있고, 문구 변경/다국어 확장 시 DB 마이그레이션 없이 대응 가능하다.
|
||||||
|
- 메시지 생성 규칙:
|
||||||
|
- 일반 후원: `OO캔을 후원하셨습니다.`
|
||||||
|
- 비밀 후원: `OO캔을 비밀후원하셨습니다.`
|
||||||
|
- 추가 메시지 입력 시: 기본 메시지 + `\n` + `"사용자 추가 메시지"`
|
||||||
|
|
||||||
|
- [x] 채널 후원 도메인 모델/저장소 설계 (`ChannelDonationMessage` 성격의 별도 엔티티, creator/sponsor/can/isSecret/additionalMessage/createdAt)
|
||||||
|
- [x] `CanUsage`에 채널 후원 전용 값 1종 추가 및 영향 범위 정의 (`CanPaymentService`, 사용내역 타이틀 매핑)
|
||||||
|
- [x] 채널 후원 API 요청/응답 스펙 확정 (필드: `creatorId`, `can`, `isSecret`, `message`, `container`)
|
||||||
|
- [x] 채널 후원 API 서비스 플로우 설계 (인증/크리에이터 검증 -> 캔 차감 -> 후원 메시지 DB 저장)
|
||||||
|
- [x] 채널 후원 리스트 API 스펙 확정 (최근 1개월, `createdAt` 내림차순, 페이징)
|
||||||
|
- [x] 채널 후원 리스트 조회 권한 규칙 반영
|
||||||
|
- 크리에이터: 모든 후원 내역 조회
|
||||||
|
- 유저: 일반 후원 + 본인이 한 비밀 후원 내역 조회
|
||||||
|
- [x] 리스트 응답 메시지 조합 규칙 반영 (일반/비밀 기본 메시지 + 추가 메시지 쌍따옴표 처리)
|
||||||
|
- [x] `explorer/profile/{id}` 응답 확장 설계 (최근 1개월 채널 후원 내역 최대 5건 포함)
|
||||||
|
- [x] QueryDSL 조회 조건 확정 (`createdAt >= now().minusMonths(1)`, `orderBy(createdAt.desc(), id.desc())`, `limit 5`)
|
||||||
|
- [x] 테스트 계획 수립 (서비스 단위 테스트 + 리포지토리 날짜 필터/정렬 테스트 + 컨트롤러 통합 테스트)
|
||||||
|
- [x] 정산 로직 제외 범위 명시 (정산 비율 변경 작업은 미포함, 채널 후원 기능만 구현)
|
||||||
|
- [x] 구현 후 검증 계획 확정 (`./gradlew test`, `./gradlew build`, 필요 시 `./gradlew ktlintCheck`)
|
||||||
|
- [x] 운영 반영용 DDL 파일 추가 (`docs/20260223_channel_donation_message_ddl.sql`)
|
||||||
|
- [x] 채널 후원 회귀 테스트 구현
|
||||||
|
- 서비스: `ChannelDonationServiceTest`
|
||||||
|
- 리포지토리: `ChannelDonationMessageRepositoryTest`
|
||||||
|
- 컨트롤러: `ChannelDonationControllerTest`
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 무엇을:
|
||||||
|
- 1차 계획 수립: 채널 후원 기능의 API/도메인/조회 범위를 정의하고, 메시지 저장 전략을 선택해 계획 문서로 고정했다.
|
||||||
|
- 2차 수정: 채널 후원 리스트 API의 조회 권한 규칙(크리에이터 전체 조회, 유저는 일반 후원+본인 비밀 후원 조회)을 계획 항목에 추가했다.
|
||||||
|
- 3차 구현: 채널 후원 API/리스트 API/Explorer 프로필 확장, `CanUsage.CHANNEL_DONATION`, 메시지 엔티티 저장, 권한별 노출 필터를 구현했다.
|
||||||
|
- 왜:
|
||||||
|
- 기존 코드 패턴(Explorer/CanUsage/후원 조회)을 따르는 구현 범위를 먼저 고정해 불필요한 확장과 API 불일치를 방지하기 위해.
|
||||||
|
- 리스트 조회 시 요청자 역할에 따라 비밀 후원 노출 범위가 달라지므로, 구현 전 권한 규칙을 계획 단계에서 명확히 고정하기 위해.
|
||||||
|
- 채널 후원은 기존 라이브/콘텐츠 후원과 정산 분리를 위해 별도 `CanUsage`와 별도 메시지 저장소가 필요하고, 프로필 화면에 최근 내역 노출 요구가 있어 Explorer 응답 확장이 필요하기 때문에.
|
||||||
|
- 어떻게:
|
||||||
|
- 내부 탐색: `ExplorerController`, `ExplorerService`, `ExplorerQueryRepository`, `CanUsage`, `CanPaymentService`, `LiveRoomService`, `LiveRoomRepository`를 확인했다.
|
||||||
|
- 병렬 조사: `explore` 2건(`bg_07537536`, `bg_5be8611b`)과 `librarian` 1건(`bg_bfe81033`) 결과를 수집해 근거를 보강했다.
|
||||||
|
- 추가 확인: `AudioContentCommentRepository`, `CreatorCommunityCommentRepository`, `LiveRoomRepository`의 비밀/본인 공개 조건 패턴(`isSecret.isFalse.or(writerId.eq(memberId))`)을 확인해 문서 규칙에 반영했다.
|
||||||
|
- 구현 파일: `explorer/profile/channelDonation/*`, `CanUsage.kt`, `CanPaymentService.kt`, `CanService.kt`, `ExplorerService.kt`, `GetCreatorProfileResponse.kt`를 수정/추가했다.
|
||||||
|
- 검증 명령:
|
||||||
|
- `./gradlew test` -> 성공
|
||||||
|
- `./gradlew build` -> 최초 1회 `GetCreatorProfileResponse.kt` import 정렬 실패(ktlint), 정렬 수정 후 재실행 성공
|
||||||
|
- `./gradlew ktlintCheck` -> 성공
|
||||||
|
|
||||||
|
### 4차 보완(리뷰 지적사항 반영)
|
||||||
|
- 무엇을:
|
||||||
|
- 누락됐던 운영 반영용 DDL 파일 `docs/20260223_channel_donation_message_ddl.sql`을 추가했다.
|
||||||
|
- 채널 후원 회귀 테스트 3종(서비스/리포지토리/컨트롤러)을 신규 추가했다.
|
||||||
|
- 왜:
|
||||||
|
- `ddl-auto: validate` 환경에서 신규 엔티티 스키마 누락 시 부팅 실패 위험이 있어 적용 스크립트를 분리 관리해야 했기 때문이다.
|
||||||
|
- 권한별 비밀후원 노출, 1개월 필터, 정렬/페이징 규칙을 자동 검증해 회귀를 방지하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 추가 파일:
|
||||||
|
- `docs/20260223_channel_donation_message_ddl.sql`
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationServiceTest.kt`
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationMessageRepositoryTest.kt`
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationControllerTest.kt`
|
||||||
|
- 검증 명령:
|
||||||
|
- `lsp_diagnostics` -> Kotlin LSP 미설정으로 실행 불가(환경 제약 확인)
|
||||||
|
- `./gradlew test --tests "*ChannelDonation*"` -> 성공
|
||||||
|
- `./gradlew test` -> 성공
|
||||||
|
- `./gradlew build` -> 성공
|
||||||
22
docs/20260223_크리에이터상세정보조회api추가.md
Normal file
22
docs/20260223_크리에이터상세정보조회api추가.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 크리에이터 상세정보 조회 API 추가 작업 계획
|
||||||
|
|
||||||
|
- [x] `ExplorerController`에 크리에이터 상세정보 조회 엔드포인트 추가
|
||||||
|
- [x] `ExplorerService`에 상세정보 조회 비즈니스 로직 추가
|
||||||
|
- [x] `ExplorerQueryRepository`에 데뷔일/활동요약 조회 쿼리 추가
|
||||||
|
- [x] 응답 DTO 추가 및 `Member` SNS URL 매핑 연결
|
||||||
|
- [x] 3차 수정: 미래 라이브만 있는 크리에이터의 음수 `D+` 노출 방지
|
||||||
|
- [x] 정적 진단/테스트/빌드 검증 및 결과 기록
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 무엇을:
|
||||||
|
- 1차 구현: 크리에이터 상세정보 조회 API(`/explorer/profile/{id}/detail`)와 응답 DTO를 추가하고, 데뷔일(라이브 `beginDateTime`/콘텐츠 `releaseDate` 최솟값), `D+N`, 활동요약, SNS URL 반환을 구현했다.
|
||||||
|
- 2차 수정: 상세 조회에 차단 관계 검사를 추가하고, 활동요약의 `contentCount`를 오픈된 콘텐츠(`releaseDate <= now`) 기준으로 집계하도록 기존 쿼리를 보정했다.
|
||||||
|
- 3차 수정: 라이브 데뷔 후보 조회에서 미래 `beginDateTime`을 제외하고, `D+` 계산 결과가 음수인 경우 `""`을 반환하도록 상세 조회 로직을 보정했다.
|
||||||
|
- 왜:
|
||||||
|
- 1차 구현: 탐색 화면에서 크리에이터 기본 정보·활동 통계·데뷔 정보·SNS를 한 번에 조회할 수 있어야 했다.
|
||||||
|
- 2차 수정: 차단 관계에서도 상세정보가 노출되는 우회가 있었고, 예약 공개 콘텐츠가 포함되어 요구사항의 “오픈한 콘텐츠 수”와 불일치할 수 있었다.
|
||||||
|
- 3차 수정: 오픈된 콘텐츠 없이 미래 예약 라이브만 있을 때 `D+-N`이 내려가 요구사항의 “오늘 기준 데뷔일로부터 며칠째(D+N)” 표현과 불일치했다.
|
||||||
|
- 어떻게:
|
||||||
|
- 1차 구현/2차 수정 모두 Kotlin LSP 부재로 `lsp_diagnostics`는 불가를 확인했다.
|
||||||
|
- 1차 구현 시점과 2차 수정 시점에 각각 `./gradlew ktlintCheck test build`를 실행해 정적검사/테스트/빌드 성공(Exit code 0)을 확인했다.
|
||||||
|
- 3차 수정 시점에도 Kotlin LSP 부재로 `lsp_diagnostics`는 불가를 확인했고, `./gradlew ktlintCheck test build`를 실행해 정적검사/테스트/빌드 성공(Exit code 0)을 확인했다.
|
||||||
17
docs/20260223_회원차단동일본인인증확장.md
Normal file
17
docs/20260223_회원차단동일본인인증확장.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 회원 차단 동일 본인인증 확장 구현
|
||||||
|
|
||||||
|
- [x] `memberBlock` 기존 단일 유저 차단 동작 확인
|
||||||
|
- [x] 차단 대상 유저가 본인인증(`Auth`)된 유저인지 확인
|
||||||
|
- [x] 본인인증 유저일 경우 동일 `di`를 가진 유저 id 목록 조회
|
||||||
|
- [x] 요청 유저(`memberId`)가 목록에 포함된 경우 제외
|
||||||
|
- [x] 대상 유저 + 동일 본인인증 유저 전체에 대해 차단 활성화 처리
|
||||||
|
- [x] 변경 파일 LSP 진단 및 관련 테스트 실행
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
- 무엇을: `MemberService.memberBlock`을 확장해 차단 대상 1명 + 동일 `Auth.di`를 가진 모든 계정을 일괄 차단하도록 수정했다.
|
||||||
|
- 왜: 본인인증 기반 다중 계정 우회 차단을 방지하고, 요청된 정책(동일 본인인증 정보 보유 계정 전체 차단)을 반영하기 위함이다.
|
||||||
|
- 어떻게 검증했는가:
|
||||||
|
- `lsp_diagnostics` 실행 시 `.kt` LSP 서버 미구성으로 진단 불가를 확인했다.
|
||||||
|
- `./gradlew test` 실행 성공.
|
||||||
|
- `./gradlew build -x test` 실행 성공(ktlint/check 포함).
|
||||||
30
docs/20260224_SNS카카오오픈채팅전환.md
Normal file
30
docs/20260224_SNS카카오오픈채팅전환.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
## 구현 항목
|
||||||
|
|
||||||
|
- [x] SNS 응답/요청 DTO 전수 점검 후 `blogUrl` 제거
|
||||||
|
- [x] SNS 응답/요청 DTO에 `kakaoOpenChatUrl` 추가
|
||||||
|
- [x] 기존 `websiteUrl` 입력/반환 값을 `kakaoOpenChatUrl`로 동일 매핑
|
||||||
|
- [x] 회원 정보 수정 API(`ProfileUpdateRequest`, `MemberService.profileUpdate`) 반영
|
||||||
|
- [x] SNS 정보를 반환하는 API 응답(`ProfileResponse`, `MyPageResponse`, `CreatorResponse`, `GetCreatorDetailResponse`, `GetLiveRoomUserProfileResponse`, `GetRoomDetailManager`) 반영
|
||||||
|
- [x] LSP 진단/테스트/빌드 검증 및 결과 기록
|
||||||
|
- [x] 2차 수정: non-null Response 호환성을 위해 `GetCreatorDetailResponse`의 `websiteUrl`, `blogUrl` 복구
|
||||||
|
- [x] 2차 수정: non-null Response 호환성을 위해 `GetLiveRoomUserProfileResponse`의 `websiteUrl`, `blogUrl` 복구
|
||||||
|
- [x] 2차 수정 검증: 테스트/빌드 재실행 및 결과 기록
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
- 1차 구현
|
||||||
|
- 무엇을: SNS 필드를 `instagramUrl`, `fancimmUrl`, `xUrl`, `youtubeUrl`, `kakaoOpenChatUrl` 구조로 통일하고 `blogUrl`을 API 요청/응답 계층에서 제거했다. `kakaoOpenChatUrl`은 기존 `member.websiteUrl` 컬럼 값을 그대로 사용하도록 매핑했다.
|
||||||
|
- 왜: DB/Entity 변경 없이 기존 `websiteUrl` 저장 데이터를 카카오 오픈채팅 링크로 재해석해 노출하고, 더 이상 사용하지 않는 `blogUrl`을 API 스펙에서 제거하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 코드 반영: `ProfileUpdateRequest`, `ProfileResponse`, `MyPageResponse`, `CreatorResponse`, `GetCreatorDetailResponse`, `GetLiveRoomUserProfileResponse`, `GetRoomDetailResponse`, `MemberService`, `ExplorerService`, `LiveRoomService`
|
||||||
|
- 정적 진단: `lsp_diagnostics` 실행 시 `.kt` LSP 미구성으로 불가(환경 제약 확인)
|
||||||
|
- 동작 검증: `./gradlew test && ./gradlew build` 실행
|
||||||
|
- 결과: `BUILD SUCCESSFUL` (test 성공 후 build 성공)
|
||||||
|
|
||||||
|
- 2차 수정
|
||||||
|
- 무엇을: non-null Response에서 제거되었던 `websiteUrl`, `blogUrl` 필드를 `GetCreatorDetailResponse`, `GetLiveRoomUserProfileResponse`에 복구했다. 동시에 각 서비스 매핑에서 해당 필드를 다시 응답에 포함했다.
|
||||||
|
- 왜: 필수 응답 키 제거로 인한 하위 호환성 이슈를 해소하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 코드 반영: `GetCreatorDetailResponse`, `ExplorerService`, `GetLiveRoomUserProfileResponse`, `LiveRoomService`
|
||||||
|
- 동작 검증: `./gradlew test && ./gradlew build` 실행
|
||||||
|
- 결과: `BUILD SUCCESSFUL` (test 성공 후 build 성공)
|
||||||
13
docs/20260225_인기크리에이터차단필터링.md
Normal file
13
docs/20260225_인기크리에이터차단필터링.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
- [x] 홈 인기 크리에이터 조회 경로 확인 및 차단 필터 적용 지점 확정
|
||||||
|
- [x] 인기 크리에이터 목록에서 내가 차단한 크리에이터 제외 로직 적용
|
||||||
|
- [x] 변경 파일 진단 및 테스트/빌드 검증 수행
|
||||||
|
- [x] 검증 결과 기록
|
||||||
|
|
||||||
|
## 1차 구현 검증 기록
|
||||||
|
- 무엇: 홈 인기 크리에이터 조회 시 차단 관계 조건을 양방향으로 반영해 내가 차단한 크리에이터가 노출되지 않도록 수정했다.
|
||||||
|
- 왜: 일반 유저가 차단한 크리에이터가 인기 크리에이터 목록에 계속 노출되는 문제를 해결하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- `lsp_diagnostics`: Kotlin LSP 서버가 환경에 구성되지 않아 해당 도구 기반 진단은 수행 불가.
|
||||||
|
- `./gradlew ktlintCheck`: 성공.
|
||||||
|
- `./gradlew test`: 성공.
|
||||||
|
- `./gradlew build -x test`: 성공.
|
||||||
21
docs/20260225_채널후원메시지_캔_천단위콤마추가.md
Normal file
21
docs/20260225_채널후원메시지_캔_천단위콤마추가.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 20260225_채널후원메시지_캔_천단위콤마추가
|
||||||
|
|
||||||
|
## 구현 항목
|
||||||
|
- [x] `ChannelDonationService.kt`의 `buildMessage` 함수 수정 (캔 수량 천단위 콤마 추가)
|
||||||
|
- [x] 관련 테스트 코드를 통한 검증
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
### 1차 구현
|
||||||
|
- **무엇을**: `buildMessage` 함수 내에서 `can` 변수를 `String.format("%,d", can)`으로 포맷팅하도록 수정
|
||||||
|
- **왜**: 후원 메시지 표시 시 캔 수량에 천단위 콤마를 추가하여 가독성을 높이기 위함
|
||||||
|
- **어떻게**:
|
||||||
|
- `ChannelDonationService.kt` 수정
|
||||||
|
- `./gradlew test` 실행 후 결과 확인
|
||||||
|
|
||||||
|
### 2차 수정
|
||||||
|
- **무엇을**: `ChannelDonationServiceTest`에 `can = 1000`일 때 메시지가 `1,000캔` 형식으로 생성되는지 검증하는 테스트(`shouldFormatCanWithCommaInDonationMessage`)를 추가하고 문서 체크박스를 완료 처리
|
||||||
|
- **왜**: 기존 테스트는 천단위 콤마 포맷을 직접 검증하지 않아 문서의 "관련 테스트 코드를 통한 검증" 항목을 충족하기 어려웠기 때문
|
||||||
|
- **어떻게**:
|
||||||
|
- `src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationServiceTest.kt`에 메시지 포맷 검증 테스트 추가
|
||||||
|
- `./gradlew test --tests "kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationServiceTest"` 실행: 성공
|
||||||
|
- `./gradlew build` 실행: 성공
|
||||||
15
docs/20260225_회원차단동일인판별조건강화.md
Normal file
15
docs/20260225_회원차단동일인판별조건강화.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
- [x] 기존 `memberBlock` 동일인 판별 로직(`di` 단일 조건)과 연관 Repository 조회 경로 확인
|
||||||
|
- [x] `AuthRepository`에 `name + birth + di + gender` AND 조건 조회 메서드 추가
|
||||||
|
- [x] `MemberService.memberBlock`에서 다중 조건 조회 메서드 사용으로 변경
|
||||||
|
- [x] 변경 파일 정적 진단 및 테스트 실행
|
||||||
|
- [x] 구현 결과/검증 기록 문서 반영
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: `memberBlock`의 동일인 확장 조회를 `di` 단일 조건에서 `name + birth + di + gender` AND 조건으로 변경했다.
|
||||||
|
- 왜: 동일인 판단 정밀도를 높여, `di`만 일치하는 케이스로 과차단되는 가능성을 줄이기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 코드 변경: `src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt`에 `getMemberIdsByNameAndBirthAndDiAndGender(...)` QueryDSL 조회를 추가했다.
|
||||||
|
- 코드 변경: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`의 `memberBlock`에서 `blockedMember.auth`의 `name/birth/di/gender`를 사용해 신규 조회 메서드를 호출하도록 바꿨다.
|
||||||
|
- 검증: `lsp_diagnostics`는 `.kt` LSP 서버 미구성으로 실행 불가(도구 에러 확인). 대신 `./gradlew test` 성공, `./gradlew build -x test` 성공으로 테스트/빌드 및 `ktlint` 체크 통과를 확인했다.
|
||||||
@@ -53,6 +53,7 @@ class LiveApiService(
|
|||||||
val latestFinishedLiveList = liveService.getLatestFinishedLive(member)
|
val latestFinishedLiveList = liveService.getLatestFinishedLive(member)
|
||||||
|
|
||||||
val replayLive = contentService.getLatestContentByTheme(
|
val replayLive = contentService.getLatestContentByTheme(
|
||||||
|
memberId = memberId,
|
||||||
theme = listOf("다시듣기"),
|
theme = listOf("다시듣기"),
|
||||||
contentType = contentType,
|
contentType = contentType,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
@@ -60,7 +61,8 @@ class LiveApiService(
|
|||||||
)
|
)
|
||||||
.filter { content ->
|
.filter { content ->
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = content.creatorId)
|
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = content.creatorId) &&
|
||||||
|
!blockMemberRepository.isBlocked(blockedMemberId = content.creatorId, memberId = memberId)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
|||||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
import kr.co.vividnext.sodalive.common.CountryContext
|
import kr.co.vividnext.sodalive.common.CountryContext
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -13,7 +14,8 @@ import java.time.format.DateTimeFormatter
|
|||||||
@Service
|
@Service
|
||||||
class CanService(
|
class CanService(
|
||||||
private val repository: CanRepository,
|
private val repository: CanRepository,
|
||||||
private val countryContext: CountryContext
|
private val countryContext: CountryContext,
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
) {
|
) {
|
||||||
fun getCans(isNotSelectedCurrency: Boolean): List<CanResponse> {
|
fun getCans(isNotSelectedCurrency: Boolean): List<CanResponse> {
|
||||||
val currency = if (isNotSelectedCurrency) {
|
val currency = if (isNotSelectedCurrency) {
|
||||||
@@ -40,7 +42,7 @@ class CanService(
|
|||||||
timezone: String,
|
timezone: String,
|
||||||
container: String
|
container: String
|
||||||
): List<GetCanUseStatusResponseItem> {
|
): List<GetCanUseStatusResponseItem> {
|
||||||
return repository.getCanUseStatus(member, pageable)
|
val useCanList = repository.getCanUseStatus(member, pageable)
|
||||||
.filter { (it.can + it.rewardCan) > 0 }
|
.filter { (it.can + it.rewardCan) > 0 }
|
||||||
.filter {
|
.filter {
|
||||||
when (container) {
|
when (container) {
|
||||||
@@ -66,6 +68,21 @@ class CanService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val channelDonationCreatorIds = useCanList
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.canUsage == CanUsage.CHANNEL_DONATION }
|
||||||
|
.mapNotNull { it.useCanCalculates.firstOrNull()?.recipientCreatorId }
|
||||||
|
.distinct()
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val channelDonationCreatorNicknameMap = if (channelDonationCreatorIds.isEmpty()) {
|
||||||
|
emptyMap()
|
||||||
|
} else {
|
||||||
|
memberRepository.findAllById(channelDonationCreatorIds).associate { it.id!! to it.nickname }
|
||||||
|
}
|
||||||
|
|
||||||
|
return useCanList
|
||||||
.map {
|
.map {
|
||||||
val title: String = when (it.canUsage) {
|
val title: String = when (it.canUsage) {
|
||||||
CanUsage.HEART, CanUsage.DONATION, CanUsage.SPIN_ROULETTE -> {
|
CanUsage.HEART, CanUsage.DONATION, CanUsage.SPIN_ROULETTE -> {
|
||||||
@@ -78,6 +95,17 @@ class CanService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CanUsage.CHANNEL_DONATION -> {
|
||||||
|
val creatorId = it.useCanCalculates.firstOrNull()?.recipientCreatorId
|
||||||
|
val creatorNickname = creatorId?.let { id -> channelDonationCreatorNicknameMap[id] }
|
||||||
|
|
||||||
|
if (creatorNickname.isNullOrBlank()) {
|
||||||
|
"[채널 후원]"
|
||||||
|
} else {
|
||||||
|
"[채널 후원] $creatorNickname"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CanUsage.LIVE -> {
|
CanUsage.LIVE -> {
|
||||||
"[라이브] ${it.room!!.title}"
|
"[라이브] ${it.room!!.title}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class CanPaymentService(
|
|||||||
characterId: Long? = null,
|
characterId: Long? = null,
|
||||||
isSecret: Boolean = false,
|
isSecret: Boolean = false,
|
||||||
liveRoom: LiveRoom? = null,
|
liveRoom: LiveRoom? = null,
|
||||||
|
creator: Member? = null,
|
||||||
order: Order? = null,
|
order: Order? = null,
|
||||||
audioContent: AudioContent? = null,
|
audioContent: AudioContent? = null,
|
||||||
communityPost: CreatorCommunity? = null,
|
communityPost: CreatorCommunity? = null,
|
||||||
@@ -93,6 +94,9 @@ class CanPaymentService(
|
|||||||
recipientId = liveRoom.member!!.id!!
|
recipientId = liveRoom.member!!.id!!
|
||||||
useCan.room = liveRoom
|
useCan.room = liveRoom
|
||||||
useCan.member = member
|
useCan.member = member
|
||||||
|
} else if (canUsage == CanUsage.CHANNEL_DONATION && creator != null) {
|
||||||
|
recipientId = creator.id!!
|
||||||
|
useCan.member = member
|
||||||
} else if (canUsage == CanUsage.ORDER_CONTENT && order != null) {
|
} else if (canUsage == CanUsage.ORDER_CONTENT && order != null) {
|
||||||
recipientId = order.creator!!.id!!
|
recipientId = order.creator!!.id!!
|
||||||
useCan.order = order
|
useCan.order = order
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ enum class CanUsage {
|
|||||||
LIVE,
|
LIVE,
|
||||||
HEART,
|
HEART,
|
||||||
DONATION,
|
DONATION,
|
||||||
|
CHANNEL_DONATION, // 채널 후원
|
||||||
CHANGE_NICKNAME,
|
CHANGE_NICKNAME,
|
||||||
ORDER_CONTENT,
|
ORDER_CONTENT,
|
||||||
SPIN_ROULETTE,
|
SPIN_ROULETTE,
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
|
|
||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.getLatestContentByTheme(
|
service.getLatestContentByTheme(
|
||||||
|
memberId = member.id!!,
|
||||||
theme = if (theme == null) listOf() else listOf(theme),
|
theme = if (theme == null) listOf() else listOf(theme),
|
||||||
contentType = contentType ?: ContentType.ALL,
|
contentType = contentType ?: ContentType.ALL,
|
||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
@@ -272,6 +273,7 @@ class AudioContentController(private val service: AudioContentService) {
|
|||||||
) = run {
|
) = run {
|
||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.getLatestContentByTheme(
|
service.getLatestContentByTheme(
|
||||||
|
memberId = member?.id,
|
||||||
theme = listOf("다시듣기"),
|
theme = listOf("다시듣기"),
|
||||||
contentType = contentType ?: ContentType.ALL,
|
contentType = contentType ?: ContentType.ALL,
|
||||||
isFree = false,
|
isFree = false,
|
||||||
|
|||||||
@@ -469,9 +469,15 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
limit: Long,
|
limit: Long,
|
||||||
isFree: Boolean
|
isFree: Boolean
|
||||||
): List<GetAudioContentMainItem> {
|
): List<GetAudioContentMainItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val orderBy = when (sortType) {
|
val orderBy = when (sortType) {
|
||||||
SortType.NEWEST -> listOf(audioContent.releaseDate.desc(), audioContent.id.desc())
|
SortType.NEWEST -> listOf(audioContent.releaseDate.desc(), audioContent.id.desc())
|
||||||
@@ -562,9 +568,15 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): List<GetAudioContentMainItem> {
|
): List<GetAudioContentMainItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val orderBy = listOf(audioContent.releaseDate.desc(), audioContent.id.desc())
|
val orderBy = listOf(audioContent.releaseDate.desc(), audioContent.id.desc())
|
||||||
|
|
||||||
@@ -630,9 +642,15 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): Int {
|
): Int {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = audioContent.isActive.isTrue
|
var where = audioContent.isActive.isTrue
|
||||||
.and(audioContent.duration.isNotNull)
|
.and(audioContent.duration.isNotNull)
|
||||||
@@ -960,9 +978,15 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): List<GetAudioContentMainItem> {
|
): List<GetAudioContentMainItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = audioContentCuration.isActive.isTrue
|
var where = audioContentCuration.isActive.isTrue
|
||||||
.and(audioContentCurationItem.isActive.isTrue)
|
.and(audioContentCurationItem.isActive.isTrue)
|
||||||
@@ -1337,9 +1361,15 @@ class AudioContentQueryRepositoryImpl(
|
|||||||
locale: String?
|
locale: String?
|
||||||
): List<AudioContentMainItem> {
|
): List<AudioContentMainItem> {
|
||||||
val blockMemberCondition = if (memberId != null) {
|
val blockMemberCondition = if (memberId != null) {
|
||||||
blockMember.member.id.eq(member.id)
|
blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -524,6 +524,10 @@ class AudioContentService(
|
|||||||
val creator = explorerQueryRepository.getMember(creatorId)
|
val creator = explorerQueryRepository.getMember(creatorId)
|
||||||
?: throw SodaException(messageKey = "content.error.user_not_found")
|
?: throw SodaException(messageKey = "content.error.user_not_found")
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = member.id!!, creatorId = creatorId)) {
|
||||||
|
throw SodaException(messageKey = "content.error.invalid_content_retry")
|
||||||
|
}
|
||||||
|
|
||||||
val creatorFollowing = explorerQueryRepository.getCreatorFollowing(
|
val creatorFollowing = explorerQueryRepository.getCreatorFollowing(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
memberId = member.id!!
|
memberId = member.id!!
|
||||||
@@ -534,12 +538,6 @@ class AudioContentService(
|
|||||||
contentId = audioContent.id!!
|
contentId = audioContent.id!!
|
||||||
)
|
)
|
||||||
|
|
||||||
// 차단된 사용자 체크
|
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
|
||||||
if (isBlocked && !isExistsAudioContent) {
|
|
||||||
throw SodaException(formatMessage("content.error.access_restricted_by_creator", creator.nickname))
|
|
||||||
}
|
|
||||||
|
|
||||||
val orderSequence = if (isExistsAudioContent) {
|
val orderSequence = if (isExistsAudioContent) {
|
||||||
limitedEditionOrderRepository.getOrderSequence(
|
limitedEditionOrderRepository.getOrderSequence(
|
||||||
contentId = audioContent.id!!,
|
contentId = audioContent.id!!,
|
||||||
@@ -886,6 +884,10 @@ class AudioContentService(
|
|||||||
): GetAudioContentListItem? {
|
): GetAudioContentListItem? {
|
||||||
val isAdult = member.auth != null && isAdultContentVisible
|
val isAdult = member.auth != null && isAdultContentVisible
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = member.id!!, creatorId = creatorId)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
val audioContent = repository.findLatestContentByCreatorId(creatorId, isAdult) ?: return null
|
val audioContent = repository.findLatestContentByCreatorId(creatorId, isAdult) ?: return null
|
||||||
|
|
||||||
val commentCount = commentRepository
|
val commentCount = commentRepository
|
||||||
@@ -957,6 +959,10 @@ class AudioContentService(
|
|||||||
val isAdult = member.auth != null && isAdultContentVisible
|
val isAdult = member.auth != null && isAdultContentVisible
|
||||||
val isCreator = member.id == creatorId
|
val isCreator = member.id == creatorId
|
||||||
|
|
||||||
|
if (!isCreator && isBlockedBetweenMembers(memberId = member.id!!, creatorId = creatorId)) {
|
||||||
|
return GetAudioContentListResponse(totalCount = 0, items = listOf())
|
||||||
|
}
|
||||||
|
|
||||||
val totalCount = repository.findTotalCountByCreatorId(
|
val totalCount = repository.findTotalCountByCreatorId(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
isCreator = isCreator,
|
isCreator = isCreator,
|
||||||
@@ -1313,8 +1319,8 @@ class AudioContentService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatMessage(key: String, vararg args: Any): String {
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
return String.format(template, *args)
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ class CategoryService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun getCategoryList(creatorId: Long, memberId: Long): List<GetCategoryListResponse> {
|
fun getCategoryList(creatorId: Long, memberId: Long): List<GetCategoryListResponse> {
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId)
|
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
if (isBlocked) throw SodaException(messageKey = "category.error.invalid_access")
|
if (isBlocked) throw SodaException(messageKey = "category.error.invalid_access")
|
||||||
|
|
||||||
// 기본 카테고리 목록 조회 (원본 언어 기준)
|
// 기본 카테고리 목록 조회 (원본 언어 기준)
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class AudioContentCommentController(
|
|||||||
return ApiResponse.ok(
|
return ApiResponse.ok(
|
||||||
service.getCommentReplyList(
|
service.getCommentReplyList(
|
||||||
commentId = commentId,
|
commentId = commentId,
|
||||||
|
memberId = member.id!!,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
pageable = pageable
|
pageable = pageable
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package kr.co.vividnext.sodalive.content.comment
|
package kr.co.vividnext.sodalive.content.comment
|
||||||
|
|
||||||
import com.querydsl.core.types.dsl.Expressions
|
import com.querydsl.core.types.dsl.Expressions
|
||||||
|
import com.querydsl.jpa.JPAExpressions
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||||
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
|
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
|
||||||
|
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||||
import kr.co.vividnext.sodalive.fcm.PushTokenInfo
|
import kr.co.vividnext.sodalive.fcm.PushTokenInfo
|
||||||
import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken
|
import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken
|
||||||
import kr.co.vividnext.sodalive.fcm.QPushTokenInfo
|
import kr.co.vividnext.sodalive.fcm.QPushTokenInfo
|
||||||
import kr.co.vividnext.sodalive.member.QMember.member
|
import kr.co.vividnext.sodalive.member.QMember.member
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -27,10 +30,11 @@ interface AudioContentCommentQueryRepository {
|
|||||||
): List<GetAudioContentCommentListItem>
|
): List<GetAudioContentCommentListItem>
|
||||||
|
|
||||||
fun totalCountCommentByContentId(contentId: Long, memberId: Long, isContentCreator: Boolean): Int
|
fun totalCountCommentByContentId(contentId: Long, memberId: Long, isContentCreator: Boolean): Int
|
||||||
fun commentReplyCountByAudioContentCommentId(commentId: Long): Int
|
fun commentReplyCountByAudioContentCommentId(commentId: Long, memberId: Long): Int
|
||||||
fun getAudioContentCommentReplyList(
|
fun getAudioContentCommentReplyList(
|
||||||
cloudFrontHost: String,
|
cloudFrontHost: String,
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int
|
limit: Int
|
||||||
@@ -59,6 +63,8 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
var where = audioContentComment.audioContent.id.eq(contentId)
|
var where = audioContentComment.audioContent.id.eq(contentId)
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
.and(audioContentComment.parent.isNull)
|
.and(audioContentComment.parent.isNull)
|
||||||
|
.and(audioContentComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(audioContentComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
|
|
||||||
if (!isContentCreator) {
|
if (!isContentCreator) {
|
||||||
where = where.and(
|
where = where.and(
|
||||||
@@ -103,8 +109,10 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
.orderBy(audioContentComment.createdAt.desc())
|
.orderBy(audioContentComment.createdAt.desc())
|
||||||
.fetch()
|
.fetch()
|
||||||
.map {
|
.map {
|
||||||
it.replyCount = commentReplyCountByAudioContentCommentId(it.id)
|
it.copy(
|
||||||
it
|
nickname = it.nickname.removeDeletedNicknamePrefix(),
|
||||||
|
replyCount = commentReplyCountByAudioContentCommentId(it.id, memberId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +120,8 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
var where = audioContentComment.audioContent.id.eq(contentId)
|
var where = audioContentComment.audioContent.id.eq(contentId)
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
.and(audioContentComment.parent.isNull)
|
.and(audioContentComment.parent.isNull)
|
||||||
|
.and(audioContentComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(audioContentComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
|
|
||||||
if (!isContentCreator) {
|
if (!isContentCreator) {
|
||||||
where = where.and(
|
where = where.and(
|
||||||
@@ -130,13 +140,15 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
.size
|
.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun commentReplyCountByAudioContentCommentId(commentId: Long): Int {
|
override fun commentReplyCountByAudioContentCommentId(commentId: Long, memberId: Long): Int {
|
||||||
return queryFactory.select(audioContentComment.id)
|
return queryFactory.select(audioContentComment.id)
|
||||||
.from(audioContentComment)
|
.from(audioContentComment)
|
||||||
.where(
|
.where(
|
||||||
audioContentComment.parent.isNotNull
|
audioContentComment.parent.isNotNull
|
||||||
.and(audioContentComment.parent.id.eq(commentId))
|
.and(audioContentComment.parent.id.eq(commentId))
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
|
.and(audioContentComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(audioContentComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
)
|
)
|
||||||
.fetch()
|
.fetch()
|
||||||
.size
|
.size
|
||||||
@@ -145,6 +157,7 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
override fun getAudioContentCommentReplyList(
|
override fun getAudioContentCommentReplyList(
|
||||||
cloudFrontHost: String,
|
cloudFrontHost: String,
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Int
|
limit: Int
|
||||||
@@ -182,11 +195,16 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
audioContentComment.parent.isNotNull
|
audioContentComment.parent.isNotNull
|
||||||
.and(audioContentComment.parent.id.eq(commentId))
|
.and(audioContentComment.parent.id.eq(commentId))
|
||||||
.and(audioContentComment.isActive.isTrue)
|
.and(audioContentComment.isActive.isTrue)
|
||||||
|
.and(audioContentComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(audioContentComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
)
|
)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit.toLong())
|
.limit(limit.toLong())
|
||||||
.orderBy(audioContentComment.createdAt.desc())
|
.orderBy(audioContentComment.createdAt.desc())
|
||||||
.fetch()
|
.fetch()
|
||||||
|
.map {
|
||||||
|
it.copy(nickname = it.nickname.removeDeletedNicknamePrefix())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPushTokenByContentIdAndCommentParentIdMyMemberId(
|
override fun findPushTokenByContentIdAndCommentParentIdMyMemberId(
|
||||||
@@ -238,4 +256,20 @@ class AudioContentCommentQueryRepositoryImpl(
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun blockedMemberIdSubQuery(memberId: Long) = JPAExpressions
|
||||||
|
.select(blockMember.blockedMember.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun blockingMemberIdSubQuery(memberId: Long) = JPAExpressions
|
||||||
|
.select(blockMember.member.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.blockedMember.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AudioContentCommentService(
|
|||||||
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
|
?: throw SodaException(messageKey = "content.error.invalid_content_retry")
|
||||||
|
|
||||||
val creator = audioContent.member!!
|
val creator = audioContent.member!!
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creator.id!!)
|
val isBlocked = isBlockedBetweenMembers(memberId = member.id!!, creatorId = creator.id!!)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
throw SodaException(formatMessage("content.comment.error.blocked_by_creator", creator.nickname))
|
throw SodaException(formatMessage("content.comment.error.blocked_by_creator", creator.nickname))
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,13 @@ class AudioContentCommentService(
|
|||||||
timezone: String,
|
timezone: String,
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): GetAudioContentCommentListResponse {
|
): GetAudioContentCommentListResponse {
|
||||||
|
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
|
||||||
|
?: return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = memberId, creatorId = audioContent.member!!.id!!)) {
|
||||||
|
return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
}
|
||||||
|
|
||||||
val isContentCreator = audioContentRepository.isContentCreator(audioContentId, memberId)
|
val isContentCreator = audioContentRepository.isContentCreator(audioContentId, memberId)
|
||||||
val commentList = repository.findByContentId(
|
val commentList = repository.findByContentId(
|
||||||
cloudFrontHost = cloudFrontHost,
|
cloudFrontHost = cloudFrontHost,
|
||||||
@@ -157,17 +164,26 @@ class AudioContentCommentService(
|
|||||||
|
|
||||||
fun getCommentReplyList(
|
fun getCommentReplyList(
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): GetAudioContentCommentListResponse {
|
): GetAudioContentCommentListResponse {
|
||||||
|
val parentComment = repository.findByIdOrNull(id = commentId)
|
||||||
|
?: return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = memberId, creatorId = parentComment.audioContent!!.member!!.id!!)) {
|
||||||
|
return GetAudioContentCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
}
|
||||||
|
|
||||||
val commentList = repository.getAudioContentCommentReplyList(
|
val commentList = repository.getAudioContentCommentReplyList(
|
||||||
cloudFrontHost = cloudFrontHost,
|
cloudFrontHost = cloudFrontHost,
|
||||||
commentId = commentId,
|
commentId = commentId,
|
||||||
|
memberId = memberId,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize
|
limit = pageable.pageSize
|
||||||
)
|
)
|
||||||
val totalCount = repository.commentReplyCountByAudioContentCommentId(commentId)
|
val totalCount = repository.commentReplyCountByAudioContentCommentId(commentId, memberId)
|
||||||
|
|
||||||
return GetAudioContentCommentListResponse(totalCount, commentList)
|
return GetAudioContentCommentListResponse(totalCount, commentList)
|
||||||
}
|
}
|
||||||
@@ -176,4 +192,9 @@ class AudioContentCommentService(
|
|||||||
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
|
val template = messageSource.getMessage(key, langContext.lang) ?: return ""
|
||||||
return String.format(template, *args)
|
return String.format(template, *args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class AudioContentMainService(
|
|||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize.toLong()
|
limit = pageable.pageSize.toLong()
|
||||||
)
|
)
|
||||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, creatorId = it.creatorId) }
|
||||||
|
|
||||||
val contentIds = contentList.map { it.contentId }
|
val contentIds = contentList.map { it.contentId }
|
||||||
val translatedContentList = if (contentIds.isNotEmpty()) {
|
val translatedContentList = if (contentIds.isNotEmpty()) {
|
||||||
@@ -198,20 +198,17 @@ class AudioContentMainService(
|
|||||||
cloudfrontHost = imageHost,
|
cloudfrontHost = imageHost,
|
||||||
isAdult = isAdult
|
isAdult = isAdult
|
||||||
)
|
)
|
||||||
.asSequence()
|
.filter { !isBlockedBetweenMembers(memberId = memberId, creatorId = it.creatorId) }
|
||||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) }
|
|
||||||
.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getAudioContentMainBannerList(memberId: Long, isAdult: Boolean) =
|
fun getAudioContentMainBannerList(memberId: Long, isAdult: Boolean) =
|
||||||
repository.getAudioContentMainBannerList(isAdult = isAdult)
|
repository.getAudioContentMainBannerList(isAdult = isAdult)
|
||||||
.asSequence()
|
|
||||||
.filter {
|
.filter {
|
||||||
if (it.type == AudioContentBannerType.CREATOR && it.creator != null) {
|
if (it.type == AudioContentBannerType.CREATOR && it.creator != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.creator!!.id!!)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.creator!!.id!!)
|
||||||
} else if (it.type == AudioContentBannerType.SERIES && it.series != null) {
|
} else if (it.type == AudioContentBannerType.SERIES && it.series != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.series!!.member!!.id!!)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.series!!.member!!.id!!)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -255,7 +252,6 @@ class AudioContentMainService(
|
|||||||
link = it.link
|
link = it.link
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toList()
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
@@ -281,9 +277,14 @@ class AudioContentMainService(
|
|||||||
contentType = contentType
|
contentType = contentType
|
||||||
)
|
)
|
||||||
.filter { content ->
|
.filter { content ->
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = content.creatorId)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = content.creatorId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.filter { it.contents.isNotEmpty() }
|
.filter { it.contents.isNotEmpty() }
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ class AudioContentBannerService(
|
|||||||
return repository.getAudioContentMainBannerList(tabId, isAdult)
|
return repository.getAudioContentMainBannerList(tabId, isAdult)
|
||||||
.filter {
|
.filter {
|
||||||
if (it.type == AudioContentBannerType.CREATOR && it.creator != null && memberId != null) {
|
if (it.type == AudioContentBannerType.CREATOR && it.creator != null && memberId != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.creator!!.id!!)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.creator!!.id!!)
|
||||||
} else if (it.type == AudioContentBannerType.SERIES && it.series != null && memberId != null) {
|
} else if (it.type == AudioContentBannerType.SERIES && it.series != null && memberId != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.series!!.member!!.id!!)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.series!!.member!!.id!!)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -64,4 +64,9 @@ class AudioContentBannerService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AudioContentCurationService(
|
|||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize.toLong()
|
limit = pageable.pageSize.toLong()
|
||||||
)
|
)
|
||||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, creatorId = it.creatorId) }
|
||||||
|
|
||||||
return GetCurationContentResponse(
|
return GetCurationContentResponse(
|
||||||
totalCount = totalCount,
|
totalCount = totalCount,
|
||||||
@@ -68,7 +68,7 @@ class AudioContentCurationService(
|
|||||||
contentType = contentType
|
contentType = contentType
|
||||||
).filter { item ->
|
).filter { item ->
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = item.creatorId)
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = item.creatorId)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -77,4 +77,9 @@ class AudioContentCurationService(
|
|||||||
}
|
}
|
||||||
.filter { it.items.isNotEmpty() }
|
.filter { it.items.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,15 @@ class AudioContentMainTabRepository(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<ContentCreatorResponse> {
|
): List<ContentCreatorResponse> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = member.isActive.isTrue
|
var where = member.isActive.isTrue
|
||||||
.and(member.role.eq(MemberRole.CREATOR))
|
.and(member.role.eq(MemberRole.CREATOR))
|
||||||
@@ -88,9 +94,15 @@ class AudioContentMainTabRepository(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<ContentCreatorResponse> {
|
): List<ContentCreatorResponse> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = member.isActive.isTrue
|
var where = member.isActive.isTrue
|
||||||
.and(member.role.eq(MemberRole.CREATOR))
|
.and(member.role.eq(MemberRole.CREATOR))
|
||||||
|
|||||||
@@ -62,9 +62,15 @@ class ContentMainTabTagCurationRepository(
|
|||||||
tag: String,
|
tag: String,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<GetAudioContentMainItem> {
|
): List<GetAudioContentMainItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = audioContent.isActive.isTrue
|
var where = audioContent.isActive.isTrue
|
||||||
.and(audioContent.duration.isNotNull)
|
.and(audioContent.duration.isNotNull)
|
||||||
|
|||||||
@@ -162,9 +162,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
@@ -230,9 +236,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
@@ -361,9 +373,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
@@ -419,9 +437,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
@@ -594,9 +618,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
@@ -829,9 +859,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
memberId: Long,
|
memberId: Long,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<GetSeriesGenreListResponse> {
|
): List<GetSeriesGenreListResponse> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = seriesGenre.isActive.isTrue
|
var where = seriesGenre.isActive.isTrue
|
||||||
.and(series.isActive.isTrue)
|
.and(series.isActive.isTrue)
|
||||||
@@ -884,9 +920,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
locale: String
|
locale: String
|
||||||
): List<GetSeriesListResponse.SeriesListItem> {
|
): List<GetSeriesListResponse.SeriesListItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = series.isActive.isTrue
|
var where = series.isActive.isTrue
|
||||||
.and(member.isActive.isTrue)
|
.and(member.isActive.isTrue)
|
||||||
@@ -1033,9 +1075,15 @@ class ContentSeriesQueryRepositoryImpl(
|
|||||||
.select(blockMember.id)
|
.select(blockMember.id)
|
||||||
.from(blockMember)
|
.from(blockMember)
|
||||||
.where(
|
.where(
|
||||||
blockMember.member.id.eq(series.member.id),
|
|
||||||
blockMember.blockedMember.id.eq(memberId),
|
|
||||||
blockMember.isActive.isTrue
|
blockMember.isActive.isTrue
|
||||||
|
.and(
|
||||||
|
blockMember.member.id.eq(series.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(series.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
where = where.and(blockedSubquery.exists().not())
|
where = where.and(blockedSubquery.exists().not())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ class ContentSeriesService(
|
|||||||
contentType = contentType
|
contentType = contentType
|
||||||
) ?: throw SodaException(messageKey = "series.error.invalid_series_retry")
|
) ?: throw SodaException(messageKey = "series.error.invalid_series_retry")
|
||||||
|
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = series.member!!.id!!)
|
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = series.member!!.id!!) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = series.member!!.id!!, memberId = member.id!!)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
throw SodaException(messageKey = "series.error.invalid_series_retry")
|
throw SodaException(messageKey = "series.error.invalid_series_retry")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ data class CreatorResponse(
|
|||||||
val tags: List<String>,
|
val tags: List<String>,
|
||||||
val introduce: String = "",
|
val introduce: String = "",
|
||||||
val instagramUrl: String? = null,
|
val instagramUrl: String? = null,
|
||||||
|
val fancimmUrl: String? = null,
|
||||||
|
val xUrl: String? = null,
|
||||||
val youtubeUrl: String? = null,
|
val youtubeUrl: String? = null,
|
||||||
val websiteUrl: String? = null,
|
val kakaoOpenChatUrl: String? = null,
|
||||||
val blogUrl: String? = null,
|
|
||||||
val isFollow: Boolean,
|
val isFollow: Boolean,
|
||||||
val isNotify: Boolean,
|
val isNotify: Boolean,
|
||||||
val isNotification: Boolean,
|
val isNotification: Boolean,
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ class ExplorerController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/profile/{id}/detail")
|
||||||
|
fun getCreatorDetail(
|
||||||
|
@PathVariable("id") creatorId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
ApiResponse.ok(service.getCreatorDetail(creatorId = creatorId, memberId = member.id!!))
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/profile/{id}/donation-rank")
|
@GetMapping("/profile/{id}/donation-rank")
|
||||||
fun getCreatorProfileDonationRanking(
|
fun getCreatorProfileDonationRanking(
|
||||||
@PathVariable("id") creatorId: Long,
|
@PathVariable("id") creatorId: Long,
|
||||||
@@ -133,6 +142,6 @@ class ExplorerController(
|
|||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
) = run {
|
) = run {
|
||||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
ApiResponse.ok(service.getCreatorProfileCheers(creatorId, timezone, pageable))
|
ApiResponse.ok(service.getCreatorProfileCheers(creatorId, timezone, member, pageable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
|
|||||||
import kr.co.vividnext.sodalive.explorer.profile.QChannelNotice.channelNotice
|
import kr.co.vividnext.sodalive.explorer.profile.QChannelNotice.channelNotice
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
|
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.TimeDifferenceResult
|
import kr.co.vividnext.sodalive.explorer.profile.TimeDifferenceResult
|
||||||
|
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||||
import kr.co.vividnext.sodalive.i18n.Lang
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
@@ -450,30 +451,52 @@ class ExplorerQueryRepository(
|
|||||||
.fetchFirst() ?: ""
|
.fetchFirst() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCheersList(creatorId: Long, timezone: String, offset: Long, limit: Long): GetCheersResponse {
|
fun getCheersList(creatorId: Long, memberId: Long, timezone: String, offset: Long, limit: Long): GetCheersResponse {
|
||||||
val cheersDatePattern = messageSource
|
val cheersDatePattern = messageSource
|
||||||
.getMessage("explorer.date.cheers.format", langContext.lang)
|
.getMessage("explorer.date.cheers.format", langContext.lang)
|
||||||
?: messageSource.getMessage("explorer.date.cheers.format", Lang.KO).orEmpty()
|
?: messageSource.getMessage("explorer.date.cheers.format", Lang.KO).orEmpty()
|
||||||
val cheersDateFormatter = DateTimeFormatter.ofPattern(cheersDatePattern)
|
val cheersDateFormatter = DateTimeFormatter.ofPattern(cheersDatePattern)
|
||||||
.withLocale(langContext.lang.locale)
|
.withLocale(langContext.lang.locale)
|
||||||
|
|
||||||
|
val blockedByMemberIdSet = queryFactory
|
||||||
|
.select(blockMember.blockedMember.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
val blockingMemberIdSet = queryFactory
|
||||||
|
.select(blockMember.member.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.blockedMember.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
val blockedMemberIdSet = blockedByMemberIdSet + blockingMemberIdSet
|
||||||
|
|
||||||
|
var where = creatorCheers.creator.id.eq(creatorId)
|
||||||
|
.and(creatorCheers.isActive.isTrue)
|
||||||
|
.and(creatorCheers.parent.isNull)
|
||||||
|
|
||||||
|
if (blockedMemberIdSet.isNotEmpty()) {
|
||||||
|
where = where.and(creatorCheers.member.id.notIn(blockedMemberIdSet))
|
||||||
|
}
|
||||||
|
|
||||||
val totalCount = queryFactory
|
val totalCount = queryFactory
|
||||||
.selectFrom(creatorCheers)
|
.selectFrom(creatorCheers)
|
||||||
.where(
|
.where(where)
|
||||||
creatorCheers.creator.id.eq(creatorId)
|
|
||||||
.and(creatorCheers.isActive.isTrue)
|
|
||||||
.and(creatorCheers.parent.isNull)
|
|
||||||
)
|
|
||||||
.fetch()
|
.fetch()
|
||||||
.count()
|
.count()
|
||||||
|
|
||||||
val cheers = queryFactory
|
val cheers = queryFactory
|
||||||
.selectFrom(creatorCheers)
|
.selectFrom(creatorCheers)
|
||||||
.where(
|
.where(where)
|
||||||
creatorCheers.creator.id.eq(creatorId)
|
|
||||||
.and(creatorCheers.isActive.isTrue)
|
|
||||||
.and(creatorCheers.parent.isNull)
|
|
||||||
)
|
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.orderBy(creatorCheers.id.desc())
|
.orderBy(creatorCheers.id.desc())
|
||||||
@@ -487,7 +510,7 @@ class ExplorerQueryRepository(
|
|||||||
GetCheersResponseItem(
|
GetCheersResponseItem(
|
||||||
cheersId = it.id!!,
|
cheersId = it.id!!,
|
||||||
memberId = it.member!!.id!!,
|
memberId = it.member!!.id!!,
|
||||||
nickname = it.member!!.nickname,
|
nickname = it.member!!.nickname.removeDeletedNicknamePrefix(),
|
||||||
profileUrl = if (it.member!!.profileImage != null) {
|
profileUrl = if (it.member!!.profileImage != null) {
|
||||||
"$cloudFrontHost/${it.member!!.profileImage}"
|
"$cloudFrontHost/${it.member!!.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
@@ -497,6 +520,9 @@ class ExplorerQueryRepository(
|
|||||||
languageCode = it.languageCode,
|
languageCode = it.languageCode,
|
||||||
date = date.format(cheersDateFormatter),
|
date = date.format(cheersDateFormatter),
|
||||||
replyList = it.children.asSequence()
|
replyList = it.children.asSequence()
|
||||||
|
.filterNot { cheers ->
|
||||||
|
cheers.member?.id != null && blockedMemberIdSet.contains(cheers.member!!.id!!)
|
||||||
|
}
|
||||||
.map { cheers ->
|
.map { cheers ->
|
||||||
val replyDate = cheers.createdAt!!
|
val replyDate = cheers.createdAt!!
|
||||||
.atZone(ZoneId.of("UTC"))
|
.atZone(ZoneId.of("UTC"))
|
||||||
@@ -505,7 +531,7 @@ class ExplorerQueryRepository(
|
|||||||
GetCheersResponseItem(
|
GetCheersResponseItem(
|
||||||
cheersId = cheers.id!!,
|
cheersId = cheers.id!!,
|
||||||
memberId = cheers.member!!.id!!,
|
memberId = cheers.member!!.id!!,
|
||||||
nickname = cheers.member!!.nickname,
|
nickname = cheers.member!!.nickname.removeDeletedNicknamePrefix(),
|
||||||
profileUrl = if (cheers.member!!.profileImage != null) {
|
profileUrl = if (cheers.member!!.profileImage != null) {
|
||||||
"$cloudFrontHost/${cheers.member!!.profileImage}"
|
"$cloudFrontHost/${cheers.member!!.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
@@ -539,6 +565,32 @@ class ExplorerQueryRepository(
|
|||||||
.fetchFirst()
|
.fetchFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFirstLiveBeginDateTime(creatorId: Long): LocalDateTime? {
|
||||||
|
return queryFactory
|
||||||
|
.select(liveRoom.beginDateTime.min())
|
||||||
|
.from(liveRoom)
|
||||||
|
.where(
|
||||||
|
liveRoom.member.id.eq(creatorId)
|
||||||
|
.and(liveRoom.channelName.isNotNull)
|
||||||
|
.and(liveRoom.beginDateTime.isNotNull)
|
||||||
|
.and(liveRoom.beginDateTime.loe(LocalDateTime.now()))
|
||||||
|
)
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFirstContentReleaseDate(creatorId: Long): LocalDateTime? {
|
||||||
|
return queryFactory
|
||||||
|
.select(audioContent.releaseDate.min())
|
||||||
|
.from(audioContent)
|
||||||
|
.where(
|
||||||
|
audioContent.member.id.eq(creatorId)
|
||||||
|
.and(audioContent.isActive.isTrue)
|
||||||
|
.and(audioContent.releaseDate.isNotNull)
|
||||||
|
.and(audioContent.releaseDate.loe(LocalDateTime.now()))
|
||||||
|
)
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
|
||||||
fun getLiveTime(creatorId: Long): Long {
|
fun getLiveTime(creatorId: Long): Long {
|
||||||
val diffs = queryFactory
|
val diffs = queryFactory
|
||||||
.select(
|
.select(
|
||||||
@@ -682,6 +734,8 @@ class ExplorerQueryRepository(
|
|||||||
.where(
|
.where(
|
||||||
audioContent.isActive.isTrue
|
audioContent.isActive.isTrue
|
||||||
.and(audioContent.member.id.eq(creatorId))
|
.and(audioContent.member.id.eq(creatorId))
|
||||||
|
.and(audioContent.releaseDate.isNotNull)
|
||||||
|
.and(audioContent.releaseDate.loe(LocalDateTime.now()))
|
||||||
)
|
)
|
||||||
.fetchFirst()
|
.fetchFirst()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository
|
|||||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest
|
import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
|
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||||
import kr.co.vividnext.sodalive.fcm.FcmEventType
|
import kr.co.vividnext.sodalive.fcm.FcmEventType
|
||||||
@@ -37,6 +38,7 @@ import java.time.DayOfWeek
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import java.time.temporal.TemporalAdjusters
|
import java.time.temporal.TemporalAdjusters
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ class ExplorerService(
|
|||||||
private val queryRepository: ExplorerQueryRepository,
|
private val queryRepository: ExplorerQueryRepository,
|
||||||
private val cheersRepository: CreatorCheersRepository,
|
private val cheersRepository: CreatorCheersRepository,
|
||||||
private val noticeRepository: ChannelNoticeRepository,
|
private val noticeRepository: ChannelNoticeRepository,
|
||||||
|
private val channelDonationService: ChannelDonationService,
|
||||||
private val communityService: CreatorCommunityService,
|
private val communityService: CreatorCommunityService,
|
||||||
private val seriesService: ContentSeriesService,
|
private val seriesService: ContentSeriesService,
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class ExplorerService(
|
|||||||
fun getCreatorRank(memberId: Long): GetExplorerSectionResponse {
|
fun getCreatorRank(memberId: Long): GetExplorerSectionResponse {
|
||||||
val creatorRankings = queryRepository
|
val creatorRankings = queryRepository
|
||||||
.getCreatorRankings()
|
.getCreatorRankings()
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = memberId, otherMemberId = it.id!!) }
|
||||||
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
||||||
|
|
||||||
val currentDateTime = LocalDateTime.now()
|
val currentDateTime = LocalDateTime.now()
|
||||||
@@ -101,7 +104,7 @@ class ExplorerService(
|
|||||||
// 인기 크리에이터
|
// 인기 크리에이터
|
||||||
val creatorRankings = queryRepository
|
val creatorRankings = queryRepository
|
||||||
.getCreatorRankings()
|
.getCreatorRankings()
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
|
||||||
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
||||||
|
|
||||||
val currentDateTime = LocalDateTime.now()
|
val currentDateTime = LocalDateTime.now()
|
||||||
@@ -134,7 +137,7 @@ class ExplorerService(
|
|||||||
// 새로 시작 (newCreators)
|
// 새로 시작 (newCreators)
|
||||||
val newCreators = queryRepository
|
val newCreators = queryRepository
|
||||||
.getNewCreators()
|
.getNewCreators()
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
|
||||||
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
||||||
|
|
||||||
val newCreatorsSection = GetExplorerSectionResponse(
|
val newCreatorsSection = GetExplorerSectionResponse(
|
||||||
@@ -153,7 +156,7 @@ class ExplorerService(
|
|||||||
color = "39abde",
|
color = "39abde",
|
||||||
creators = queryRepository
|
creators = queryRepository
|
||||||
.findCreatorByGender(1)
|
.findCreatorByGender(1)
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
|
||||||
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,7 +167,7 @@ class ExplorerService(
|
|||||||
color = "ffa517",
|
color = "ffa517",
|
||||||
creators = queryRepository
|
creators = queryRepository
|
||||||
.findCreatorByGender(0)
|
.findCreatorByGender(0)
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
|
||||||
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
.map { it.toExplorerSectionCreator(cloudFrontHost) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -187,11 +190,68 @@ class ExplorerService(
|
|||||||
|
|
||||||
return queryRepository.getSearchChannel(channel, member.id!!)
|
return queryRepository.getSearchChannel(channel, member.id!!)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
.filter { !isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = it.id!!) }
|
||||||
.map { GetRoomDetailUser(it, cloudFrontHost) }
|
.map { GetRoomDetailUser(it, cloudFrontHost) }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCreatorDetail(creatorId: Long, memberId: Long): GetCreatorDetailResponse {
|
||||||
|
val creatorAccount = queryRepository.getMember(creatorId)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = memberId, otherMemberId = creatorId)) {
|
||||||
|
val messageTemplate = messageSource
|
||||||
|
.getMessage("explorer.creator.blocked_access", langContext.lang)
|
||||||
|
.orEmpty()
|
||||||
|
throw SodaException(message = String.format(messageTemplate, creatorAccount.nickname))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creatorAccount.role != MemberRole.CREATOR) {
|
||||||
|
throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||||
|
}
|
||||||
|
|
||||||
|
val liveCount = queryRepository.getLiveCount(creatorId) ?: 0
|
||||||
|
val liveTime = queryRepository.getLiveTime(creatorId)
|
||||||
|
val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0
|
||||||
|
val contentCount = queryRepository.getContentCount(creatorId) ?: 0
|
||||||
|
val activitySummary = GetCreatorActivitySummary(
|
||||||
|
liveCount = liveCount,
|
||||||
|
liveTime = liveTime,
|
||||||
|
liveContributorCount = liveContributorCount,
|
||||||
|
contentCount = contentCount
|
||||||
|
)
|
||||||
|
|
||||||
|
val debutDateTime = listOfNotNull(
|
||||||
|
queryRepository.getFirstLiveBeginDateTime(creatorId),
|
||||||
|
queryRepository.getFirstContentReleaseDate(creatorId)
|
||||||
|
).minOrNull()
|
||||||
|
|
||||||
|
val debutDate = debutDateTime?.toLocalDate()
|
||||||
|
val elapsedDebutDays = debutDate?.let { ChronoUnit.DAYS.between(it, LocalDate.now()) }
|
||||||
|
val dDay = if (elapsedDebutDays != null && elapsedDebutDays >= 0) {
|
||||||
|
"D+$elapsedDebutDays"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetCreatorDetailResponse(
|
||||||
|
nickname = creatorAccount.nickname,
|
||||||
|
profileImageUrl = if (creatorAccount.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${creatorAccount.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
},
|
||||||
|
debutDate = debutDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "",
|
||||||
|
dDay = dDay,
|
||||||
|
activitySummary = activitySummary,
|
||||||
|
instagramUrl = creatorAccount.instagramUrl,
|
||||||
|
fancimmUrl = creatorAccount.fancimmUrl,
|
||||||
|
xUrl = creatorAccount.xUrl,
|
||||||
|
youtubeUrl = creatorAccount.youtubeUrl,
|
||||||
|
kakaoOpenChatUrl = creatorAccount.websiteUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getCreatorProfile(
|
fun getCreatorProfile(
|
||||||
creatorId: Long,
|
creatorId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
@@ -203,8 +263,9 @@ class ExplorerService(
|
|||||||
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||||
|
|
||||||
// 차단된 사용자 체크
|
// 차단된 사용자 체크
|
||||||
val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
val isBlockedByCreator = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
||||||
if (isBlocked) {
|
val isBlock = memberService.isBlocked(blockedMemberId = creatorId, memberId = member.id!!)
|
||||||
|
if (isBlockedByCreator || isBlock) {
|
||||||
val messageTemplate = messageSource
|
val messageTemplate = messageSource
|
||||||
.getMessage("explorer.creator.blocked_access", langContext.lang)
|
.getMessage("explorer.creator.blocked_access", langContext.lang)
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
@@ -240,7 +301,7 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 라이브
|
// 라이브
|
||||||
val liveRoomList = if (isCreator) {
|
val liveRoomList = if (isCreator && !isBlock) {
|
||||||
queryRepository.getLiveRoomList(
|
queryRepository.getLiveRoomList(
|
||||||
creatorId,
|
creatorId,
|
||||||
userMember = member,
|
userMember = member,
|
||||||
@@ -251,7 +312,7 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 오디오 콘텐츠
|
// 오디오 콘텐츠
|
||||||
val contentList = if (isCreator) {
|
val contentList = if (isCreator && !isBlock) {
|
||||||
audioContentService.getAudioContentList(
|
audioContentService.getAudioContentList(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
sortType = SortType.NEWEST,
|
sortType = SortType.NEWEST,
|
||||||
@@ -284,7 +345,7 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 크리에이터의 최신 오디오 콘텐츠 1개
|
// 크리에이터의 최신 오디오 콘텐츠 1개
|
||||||
val latestContent = if (isCreator) {
|
val latestContent = if (isCreator && !isBlock) {
|
||||||
audioContentService.getLatestCreatorAudioContent(creatorId, member, isAdultContentVisible)
|
audioContentService.getLatestCreatorAudioContent(creatorId, member, isAdultContentVisible)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -326,11 +387,25 @@ class ExplorerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 응원
|
// 응원
|
||||||
val cheers = queryRepository.getCheersList(creatorId, timezone = timezone, offset = 0, limit = 4)
|
val cheers = queryRepository.getCheersList(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = member.id!!,
|
||||||
|
timezone = timezone,
|
||||||
|
offset = 0,
|
||||||
|
limit = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
val channelDonationList = if (isCreator && !isBlock) {
|
||||||
|
channelDonationService.getChannelDonationListForProfile(
|
||||||
|
creatorId = creatorId,
|
||||||
|
member = member,
|
||||||
|
limit = 5
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
|
||||||
// 차단한 크리에이터 인지 체크
|
// 차단한 크리에이터 인지 체크
|
||||||
val isBlock = memberService.isBlocked(blockedMemberId = creatorId, memberId = member.id!!)
|
|
||||||
|
|
||||||
val activitySummary = if (isCreator) {
|
val activitySummary = if (isCreator) {
|
||||||
// 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수)
|
// 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수)
|
||||||
val liveCount = queryRepository.getLiveCount(creatorId) ?: 0
|
val liveCount = queryRepository.getLiveCount(creatorId) ?: 0
|
||||||
@@ -347,7 +422,7 @@ class ExplorerService(
|
|||||||
GetCreatorActivitySummary(0, 0, 0, 0)
|
GetCreatorActivitySummary(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val seriesList = if (isCreator) {
|
val seriesList = if (isCreator && !isBlock) {
|
||||||
seriesService
|
seriesService
|
||||||
.getSeriesList(
|
.getSeriesList(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
@@ -372,9 +447,10 @@ class ExplorerService(
|
|||||||
tags = creatorAccount.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(),
|
tags = creatorAccount.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(),
|
||||||
introduce = creatorAccount.introduce,
|
introduce = creatorAccount.introduce,
|
||||||
instagramUrl = creatorAccount.instagramUrl,
|
instagramUrl = creatorAccount.instagramUrl,
|
||||||
|
fancimmUrl = creatorAccount.fancimmUrl,
|
||||||
|
xUrl = creatorAccount.xUrl,
|
||||||
youtubeUrl = creatorAccount.youtubeUrl,
|
youtubeUrl = creatorAccount.youtubeUrl,
|
||||||
websiteUrl = creatorAccount.websiteUrl,
|
kakaoOpenChatUrl = creatorAccount.websiteUrl,
|
||||||
blogUrl = creatorAccount.blogUrl,
|
|
||||||
isFollow = creatorFollowing?.isFollow ?: false,
|
isFollow = creatorFollowing?.isFollow ?: false,
|
||||||
isNotify = creatorFollowing?.isNotify ?: false,
|
isNotify = creatorFollowing?.isNotify ?: false,
|
||||||
isNotification = creatorFollowing?.isFollow ?: false,
|
isNotification = creatorFollowing?.isFollow ?: false,
|
||||||
@@ -389,6 +465,7 @@ class ExplorerService(
|
|||||||
ownedContentCount = ownedContentCount,
|
ownedContentCount = ownedContentCount,
|
||||||
notice = notice,
|
notice = notice,
|
||||||
communityPostList = communityPostList,
|
communityPostList = communityPostList,
|
||||||
|
channelDonationList = channelDonationList,
|
||||||
cheers = cheers,
|
cheers = cheers,
|
||||||
activitySummary = activitySummary,
|
activitySummary = activitySummary,
|
||||||
seriesList = seriesList,
|
seriesList = seriesList,
|
||||||
@@ -514,7 +591,7 @@ class ExplorerService(
|
|||||||
val creator = queryRepository.getMember(request.creatorId)
|
val creator = queryRepository.getMember(request.creatorId)
|
||||||
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||||
|
|
||||||
val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = request.creatorId)
|
val isBlocked = isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = request.creatorId)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
val messageTemplate = messageSource
|
val messageTemplate = messageSource
|
||||||
.getMessage("explorer.creator.blocked_cheers", langContext.lang)
|
.getMessage("explorer.creator.blocked_cheers", langContext.lang)
|
||||||
@@ -553,10 +630,16 @@ class ExplorerService(
|
|||||||
fun getCreatorProfileCheers(
|
fun getCreatorProfileCheers(
|
||||||
creatorId: Long,
|
creatorId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
|
member: Member,
|
||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): GetCheersResponse {
|
): GetCheersResponse {
|
||||||
|
if (isBlockedBetweenMembers(memberId = member.id!!, otherMemberId = creatorId)) {
|
||||||
|
throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
}
|
||||||
|
|
||||||
return queryRepository.getCheersList(
|
return queryRepository.getCheersList(
|
||||||
creatorId = creatorId,
|
creatorId = creatorId,
|
||||||
|
memberId = member.id!!,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize.toLong()
|
limit = pageable.pageSize.toLong()
|
||||||
@@ -605,4 +688,9 @@ class ExplorerService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, otherMemberId: Long): Boolean {
|
||||||
|
return memberService.isBlocked(blockedMemberId = memberId, memberId = otherMemberId) ||
|
||||||
|
memberService.isBlocked(blockedMemberId = otherMemberId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
data class GetCreatorDetailResponse(
|
||||||
|
@JsonProperty("nickname") val nickname: String,
|
||||||
|
@JsonProperty("profileImageUrl") val profileImageUrl: String,
|
||||||
|
@JsonProperty("debutDate") val debutDate: String,
|
||||||
|
@JsonProperty("dDay") val dDay: String,
|
||||||
|
@JsonProperty("activitySummary") val activitySummary: GetCreatorActivitySummary,
|
||||||
|
@JsonProperty("instagramUrl") val instagramUrl: String,
|
||||||
|
@JsonProperty("fancimmUrl") val fancimmUrl: String,
|
||||||
|
@JsonProperty("xUrl") val xUrl: String,
|
||||||
|
@JsonProperty("youtubeUrl") val youtubeUrl: String,
|
||||||
|
@JsonProperty("kakaoOpenChatUrl") val kakaoOpenChatUrl: String
|
||||||
|
)
|
||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.explorer
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.content.GetAudioContentListItem
|
import kr.co.vividnext.sodalive.content.GetAudioContentListItem
|
||||||
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.GetChannelDonationListItem
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.GetCommunityPostListResponse
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.GetCommunityPostListResponse
|
||||||
|
|
||||||
data class GetCreatorProfileResponse(
|
data class GetCreatorProfileResponse(
|
||||||
@@ -15,6 +16,7 @@ data class GetCreatorProfileResponse(
|
|||||||
val ownedContentCount: Long,
|
val ownedContentCount: Long,
|
||||||
val notice: String,
|
val notice: String,
|
||||||
val communityPostList: List<GetCommunityPostListResponse>,
|
val communityPostList: List<GetCommunityPostListResponse>,
|
||||||
|
val channelDonationList: List<GetChannelDonationListItem>,
|
||||||
val cheers: GetCheersResponse,
|
val cheers: GetCheersResponse,
|
||||||
val activitySummary: GetCreatorActivitySummary,
|
val activitySummary: GetCreatorActivitySummary,
|
||||||
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
|
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/explorer/profile/channel-donation")
|
||||||
|
class ChannelDonationController(
|
||||||
|
private val channelDonationService: ChannelDonationService
|
||||||
|
) {
|
||||||
|
@PostMapping
|
||||||
|
fun donate(
|
||||||
|
@RequestBody request: PostChannelDonationRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
ApiResponse.ok(channelDonationService.donate(request, member))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun getChannelDonationList(
|
||||||
|
@RequestParam creatorId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
pageable: Pageable
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
ApiResponse.ok(
|
||||||
|
channelDonationService.getChannelDonationList(
|
||||||
|
creatorId = creatorId,
|
||||||
|
member = member,
|
||||||
|
offset = pageable.offset,
|
||||||
|
limit = pageable.pageSize.toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class ChannelDonationMessage(
|
||||||
|
val can: Int,
|
||||||
|
val isSecret: Boolean = false,
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
var additionalMessage: String? = null
|
||||||
|
) : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
var member: Member? = null
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "creator_id", nullable = false)
|
||||||
|
var creator: Member? = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface ChannelDonationMessageRepository :
|
||||||
|
JpaRepository<ChannelDonationMessage, Long>, ChannelDonationMessageQueryRepository
|
||||||
|
|
||||||
|
interface ChannelDonationMessageQueryRepository {
|
||||||
|
fun getChannelDonationMessageList(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
isCreator: Boolean,
|
||||||
|
offset: Long,
|
||||||
|
limit: Long,
|
||||||
|
startDateTime: LocalDateTime
|
||||||
|
): List<ChannelDonationMessage>
|
||||||
|
|
||||||
|
fun getChannelDonationMessageTotalCount(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
isCreator: Boolean,
|
||||||
|
startDateTime: LocalDateTime
|
||||||
|
): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChannelDonationMessageQueryRepositoryImpl(
|
||||||
|
private val queryFactory: JPAQueryFactory
|
||||||
|
) : ChannelDonationMessageQueryRepository {
|
||||||
|
override fun getChannelDonationMessageList(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
isCreator: Boolean,
|
||||||
|
offset: Long,
|
||||||
|
limit: Long,
|
||||||
|
startDateTime: LocalDateTime
|
||||||
|
): List<ChannelDonationMessage> {
|
||||||
|
val where = whereCondition(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = memberId,
|
||||||
|
isCreator = isCreator,
|
||||||
|
startDateTime = startDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(channelDonationMessage)
|
||||||
|
.where(where)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.orderBy(
|
||||||
|
channelDonationMessage.createdAt.desc(),
|
||||||
|
channelDonationMessage.id.desc()
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChannelDonationMessageTotalCount(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
isCreator: Boolean,
|
||||||
|
startDateTime: LocalDateTime
|
||||||
|
): Int {
|
||||||
|
val where = whereCondition(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = memberId,
|
||||||
|
isCreator = isCreator,
|
||||||
|
startDateTime = startDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(channelDonationMessage.id)
|
||||||
|
.from(channelDonationMessage)
|
||||||
|
.where(where)
|
||||||
|
.fetch()
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun whereCondition(
|
||||||
|
creatorId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
isCreator: Boolean,
|
||||||
|
startDateTime: LocalDateTime
|
||||||
|
) = channelDonationMessage.creator.id.eq(creatorId)
|
||||||
|
.and(channelDonationMessage.createdAt.goe(startDateTime))
|
||||||
|
.let {
|
||||||
|
if (isCreator) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it.and(
|
||||||
|
channelDonationMessage.isSecret.isFalse
|
||||||
|
.or(channelDonationMessage.member.id.eq(memberId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||||
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ChannelDonationService(
|
||||||
|
private val canPaymentService: CanPaymentService,
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
private val channelDonationMessageRepository: ChannelDonationMessageRepository,
|
||||||
|
private val messageSource: SodaMessageSource,
|
||||||
|
private val langContext: LangContext,
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) {
|
||||||
|
@Transactional
|
||||||
|
fun donate(request: PostChannelDonationRequest, member: Member) {
|
||||||
|
if (request.can < 1) {
|
||||||
|
throw SodaException(messageKey = "content.donation.error.minimum_can")
|
||||||
|
}
|
||||||
|
|
||||||
|
val creator = memberRepository.findCreatorByIdOrNull(request.creatorId)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||||
|
|
||||||
|
canPaymentService.spendCan(
|
||||||
|
memberId = member.id!!,
|
||||||
|
needCan = request.can,
|
||||||
|
canUsage = CanUsage.CHANNEL_DONATION,
|
||||||
|
creator = creator,
|
||||||
|
isSecret = request.isSecret,
|
||||||
|
container = request.container
|
||||||
|
)
|
||||||
|
|
||||||
|
val channelDonationMessage = ChannelDonationMessage(
|
||||||
|
can = request.can,
|
||||||
|
isSecret = request.isSecret,
|
||||||
|
additionalMessage = request.message.takeIf { it.isNotBlank() }
|
||||||
|
)
|
||||||
|
channelDonationMessage.member = member
|
||||||
|
channelDonationMessage.creator = creator
|
||||||
|
channelDonationMessageRepository.save(channelDonationMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChannelDonationList(
|
||||||
|
creatorId: Long,
|
||||||
|
member: Member,
|
||||||
|
offset: Long,
|
||||||
|
limit: Long
|
||||||
|
): GetChannelDonationListResponse {
|
||||||
|
memberRepository.findCreatorByIdOrNull(creatorId)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||||
|
|
||||||
|
val startDateTime = LocalDateTime.now().minusMonths(1)
|
||||||
|
val isCreator = member.role == MemberRole.CREATOR && creatorId == member.id
|
||||||
|
|
||||||
|
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = member.id!!,
|
||||||
|
isCreator = isCreator,
|
||||||
|
startDateTime = startDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
val items = channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
creatorId = creatorId,
|
||||||
|
memberId = member.id!!,
|
||||||
|
isCreator = isCreator,
|
||||||
|
offset = offset,
|
||||||
|
limit = limit,
|
||||||
|
startDateTime = startDateTime
|
||||||
|
).map {
|
||||||
|
GetChannelDonationListItem(
|
||||||
|
id = it.id!!,
|
||||||
|
memberId = it.member!!.id!!,
|
||||||
|
nickname = it.member!!.nickname,
|
||||||
|
profileUrl = if (it.member!!.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${it.member!!.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
},
|
||||||
|
can = it.can,
|
||||||
|
isSecret = it.isSecret,
|
||||||
|
message = buildMessage(it.can, it.isSecret, it.additionalMessage),
|
||||||
|
createdAt = it.createdAt!!.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetChannelDonationListResponse(totalCount = totalCount, items = items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChannelDonationListForProfile(
|
||||||
|
creatorId: Long,
|
||||||
|
member: Member,
|
||||||
|
limit: Long = 5
|
||||||
|
): List<GetChannelDonationListItem> {
|
||||||
|
return getChannelDonationList(
|
||||||
|
creatorId = creatorId,
|
||||||
|
member = member,
|
||||||
|
offset = 0,
|
||||||
|
limit = limit
|
||||||
|
).items
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMessage(can: Int, isSecret: Boolean, additionalMessage: String?): String {
|
||||||
|
val key = if (isSecret) {
|
||||||
|
"explorer.channel_donation.message.default.secret"
|
||||||
|
} else {
|
||||||
|
"explorer.channel_donation.message.default.public"
|
||||||
|
}
|
||||||
|
val formattedCan = String.format("%,d", can)
|
||||||
|
val defaultMessage = getMessage(key, formattedCan)
|
||||||
|
|
||||||
|
return if (additionalMessage.isNullOrBlank()) {
|
||||||
|
defaultMessage
|
||||||
|
} else {
|
||||||
|
"$defaultMessage\n\"$additionalMessage\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMessage(key: String, vararg args: Any): String {
|
||||||
|
val template = messageSource.getMessage(key, langContext.lang).orEmpty()
|
||||||
|
return if (args.isEmpty()) template else String.format(template, *args)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
data class GetChannelDonationListResponse(
|
||||||
|
val totalCount: Int,
|
||||||
|
val items: List<GetChannelDonationListItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetChannelDonationListItem(
|
||||||
|
val id: Long,
|
||||||
|
val memberId: Long,
|
||||||
|
val nickname: String,
|
||||||
|
val profileUrl: String,
|
||||||
|
val can: Int,
|
||||||
|
val isSecret: Boolean,
|
||||||
|
val message: String,
|
||||||
|
val createdAt: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
data class PostChannelDonationRequest(
|
||||||
|
val creatorId: Long,
|
||||||
|
val can: Int,
|
||||||
|
val isSecret: Boolean = false,
|
||||||
|
val message: String = "",
|
||||||
|
val container: String
|
||||||
|
)
|
||||||
@@ -179,6 +179,7 @@ class CreatorCommunityController(private val service: CreatorCommunityService) {
|
|||||||
ApiResponse.ok(
|
ApiResponse.ok(
|
||||||
service.getCommentReplyList(
|
service.getCommentReplyList(
|
||||||
commentId = commentId,
|
commentId = commentId,
|
||||||
|
memberId = member.id!!,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize.toLong()
|
limit = pageable.pageSize.toLong()
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class CreatorCommunityService(
|
|||||||
limit: Long,
|
limit: Long,
|
||||||
isAdult: Boolean
|
isAdult: Boolean
|
||||||
): List<GetCommunityPostListResponse> {
|
): List<GetCommunityPostListResponse> {
|
||||||
if (blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId)) {
|
if (isBlockedBetweenMembers(memberId = memberId, creatorId = creatorId)) {
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ class CreatorCommunityService(
|
|||||||
val post = repository.getCommunityPost(postId, isAdult = isAdult)
|
val post = repository.getCommunityPost(postId, isAdult = isAdult)
|
||||||
?: throw SodaException(messageKey = "creator.community.invalid_request_retry")
|
?: throw SodaException(messageKey = "creator.community.invalid_request_retry")
|
||||||
|
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = post.creatorId)
|
val isBlocked = isBlockedBetweenMembers(memberId = memberId, creatorId = post.creatorId)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
val messageTemplate = messageSource
|
val messageTemplate = messageSource
|
||||||
.getMessage("creator.community.blocked_access", langContext.lang)
|
.getMessage("creator.community.blocked_access", langContext.lang)
|
||||||
@@ -375,6 +375,11 @@ class CreatorCommunityService(
|
|||||||
) {
|
) {
|
||||||
val post = repository.findByIdOrNull(id = postId)
|
val post = repository.findByIdOrNull(id = postId)
|
||||||
?: throw SodaException(messageKey = "creator.community.invalid_post_retry")
|
?: throw SodaException(messageKey = "creator.community.invalid_post_retry")
|
||||||
|
|
||||||
|
if (isBlockedBetweenMembers(memberId = member.id!!, creatorId = post.member!!.id!!)) {
|
||||||
|
throw SodaException(messageKey = "creator.community.invalid_access_retry")
|
||||||
|
}
|
||||||
|
|
||||||
val isExistOrdered = useCanRepository.isExistCommunityPostOrdered(postId = postId, memberId = member.id!!)
|
val isExistOrdered = useCanRepository.isExistCommunityPostOrdered(postId = postId, memberId = member.id!!)
|
||||||
|
|
||||||
if (isSecret && !isExistOrdered) {
|
if (isSecret && !isExistOrdered) {
|
||||||
@@ -425,6 +430,11 @@ class CreatorCommunityService(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): GetCommunityPostCommentListResponse {
|
): GetCommunityPostCommentListResponse {
|
||||||
|
val post = repository.findByIdOrNull(id = postId)
|
||||||
|
if (post != null && isBlockedBetweenMembers(memberId = memberId, creatorId = post.member!!.id!!)) {
|
||||||
|
return GetCommunityPostCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
}
|
||||||
|
|
||||||
val commentList = commentRepository.findByPostId(
|
val commentList = commentRepository.findByPostId(
|
||||||
id = postId,
|
id = postId,
|
||||||
memberId = memberId,
|
memberId = memberId,
|
||||||
@@ -444,18 +454,28 @@ class CreatorCommunityService(
|
|||||||
|
|
||||||
fun getCommentReplyList(
|
fun getCommentReplyList(
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): GetCommunityPostCommentListResponse {
|
): GetCommunityPostCommentListResponse {
|
||||||
|
val parentComment = commentRepository.findByIdOrNull(id = commentId)
|
||||||
|
if (
|
||||||
|
parentComment != null &&
|
||||||
|
isBlockedBetweenMembers(memberId = memberId, creatorId = parentComment.creatorCommunity!!.member!!.id!!)
|
||||||
|
) {
|
||||||
|
return GetCommunityPostCommentListResponse(totalCount = 0, items = listOf())
|
||||||
|
}
|
||||||
|
|
||||||
val commentList = commentRepository.getCommunityCommentReplyList(
|
val commentList = commentRepository.getCommunityCommentReplyList(
|
||||||
commentId = commentId,
|
commentId = commentId,
|
||||||
|
memberId = memberId,
|
||||||
timezone = timezone,
|
timezone = timezone,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
limit = limit
|
limit = limit
|
||||||
)
|
)
|
||||||
|
|
||||||
val totalCount = commentRepository.commentReplyCountByCommentId(commentId)
|
val totalCount = commentRepository.commentReplyCountByCommentId(commentId, memberId)
|
||||||
return GetCommunityPostCommentListResponse(totalCount = totalCount, items = commentList)
|
return GetCommunityPostCommentListResponse(totalCount = totalCount, items = commentList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,10 +489,7 @@ class CreatorCommunityService(
|
|||||||
|
|
||||||
return postList
|
return postList
|
||||||
.filter {
|
.filter {
|
||||||
!blockMemberRepository.isBlocked(
|
!isBlockedBetweenMembers(memberId = memberId, creatorId = it.creatorId)
|
||||||
blockedMemberId = memberId,
|
|
||||||
memberId = it.creatorId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
val isLike =
|
val isLike =
|
||||||
@@ -541,7 +558,7 @@ class CreatorCommunityService(
|
|||||||
val post = repository.findByIdAndActive(postId, isAdult)
|
val post = repository.findByIdAndActive(postId, isAdult)
|
||||||
?: throw SodaException(messageKey = "creator.community.invalid_request_retry")
|
?: throw SodaException(messageKey = "creator.community.invalid_request_retry")
|
||||||
|
|
||||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = post.member!!.id!!)
|
val isBlocked = isBlockedBetweenMembers(memberId = memberId, creatorId = post.member!!.id!!)
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
val messageTemplate = messageSource
|
val messageTemplate = messageSource
|
||||||
.getMessage("creator.community.blocked_access", langContext.lang)
|
.getMessage("creator.community.blocked_access", langContext.lang)
|
||||||
@@ -616,4 +633,9 @@ class CreatorCommunityService(
|
|||||||
firstComment = firstComment
|
firstComment = firstComment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment
|
package kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment
|
||||||
|
|
||||||
import com.querydsl.core.types.dsl.Expressions
|
import com.querydsl.core.types.dsl.Expressions
|
||||||
|
import com.querydsl.jpa.JPAExpressions
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment
|
||||||
|
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||||
|
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -20,7 +23,7 @@ interface CreatorCommunityCommentQueryRepository {
|
|||||||
limit: Long
|
limit: Long
|
||||||
): List<GetCommunityPostCommentListItem>
|
): List<GetCommunityPostCommentListItem>
|
||||||
|
|
||||||
fun commentReplyCountByCommentId(commentId: Long): Int
|
fun commentReplyCountByCommentId(commentId: Long, memberId: Long): Int
|
||||||
|
|
||||||
fun totalCountCommentByPostId(
|
fun totalCountCommentByPostId(
|
||||||
postId: Long,
|
postId: Long,
|
||||||
@@ -30,6 +33,7 @@ interface CreatorCommunityCommentQueryRepository {
|
|||||||
|
|
||||||
fun getCommunityCommentReplyList(
|
fun getCommunityCommentReplyList(
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
@@ -65,6 +69,8 @@ class CreatorCommunityCommentQueryRepositoryImpl(
|
|||||||
var where = creatorCommunityComment.isActive.isTrue
|
var where = creatorCommunityComment.isActive.isTrue
|
||||||
.and(creatorCommunityComment.creatorCommunity.id.eq(id))
|
.and(creatorCommunityComment.creatorCommunity.id.eq(id))
|
||||||
.and(creatorCommunityComment.parent.isNull)
|
.and(creatorCommunityComment.parent.isNull)
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
|
|
||||||
if (!isContentCreator) {
|
if (!isContentCreator) {
|
||||||
where = where.and(
|
where = where.and(
|
||||||
@@ -93,18 +99,22 @@ class CreatorCommunityCommentQueryRepositoryImpl(
|
|||||||
.orderBy(creatorCommunityComment.createdAt.desc())
|
.orderBy(creatorCommunityComment.createdAt.desc())
|
||||||
.fetch()
|
.fetch()
|
||||||
.map {
|
.map {
|
||||||
it.replyCount = commentReplyCountByCommentId(it.id)
|
it.copy(
|
||||||
it
|
nickname = it.nickname.removeDeletedNicknamePrefix(),
|
||||||
|
replyCount = commentReplyCountByCommentId(it.id, memberId)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun commentReplyCountByCommentId(commentId: Long): Int {
|
override fun commentReplyCountByCommentId(commentId: Long, memberId: Long): Int {
|
||||||
return queryFactory.select(creatorCommunityComment.id)
|
return queryFactory.select(creatorCommunityComment.id)
|
||||||
.from(creatorCommunityComment)
|
.from(creatorCommunityComment)
|
||||||
.where(
|
.where(
|
||||||
creatorCommunityComment.isActive.isTrue
|
creatorCommunityComment.isActive.isTrue
|
||||||
.and(creatorCommunityComment.parent.isNotNull)
|
.and(creatorCommunityComment.parent.isNotNull)
|
||||||
.and(creatorCommunityComment.parent.id.eq(commentId))
|
.and(creatorCommunityComment.parent.id.eq(commentId))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
)
|
)
|
||||||
.fetch()
|
.fetch()
|
||||||
.size
|
.size
|
||||||
@@ -118,6 +128,8 @@ class CreatorCommunityCommentQueryRepositoryImpl(
|
|||||||
var where = creatorCommunityComment.creatorCommunity.id.eq(postId)
|
var where = creatorCommunityComment.creatorCommunity.id.eq(postId)
|
||||||
.and(creatorCommunityComment.isActive.isTrue)
|
.and(creatorCommunityComment.isActive.isTrue)
|
||||||
.and(creatorCommunityComment.parent.isNull)
|
.and(creatorCommunityComment.parent.isNull)
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
|
|
||||||
if (!isContentCreator) {
|
if (!isContentCreator) {
|
||||||
where = where.and(
|
where = where.and(
|
||||||
@@ -135,6 +147,7 @@ class CreatorCommunityCommentQueryRepositoryImpl(
|
|||||||
|
|
||||||
override fun getCommunityCommentReplyList(
|
override fun getCommunityCommentReplyList(
|
||||||
commentId: Long,
|
commentId: Long,
|
||||||
|
memberId: Long,
|
||||||
timezone: String,
|
timezone: String,
|
||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
@@ -169,10 +182,31 @@ class CreatorCommunityCommentQueryRepositoryImpl(
|
|||||||
creatorCommunityComment.isActive.isTrue
|
creatorCommunityComment.isActive.isTrue
|
||||||
.and(creatorCommunityComment.parent.isNotNull)
|
.and(creatorCommunityComment.parent.isNotNull)
|
||||||
.and(creatorCommunityComment.parent.id.eq(commentId))
|
.and(creatorCommunityComment.parent.id.eq(commentId))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockedMemberIdSubQuery(memberId)))
|
||||||
|
.and(creatorCommunityComment.member.id.notIn(blockingMemberIdSubQuery(memberId)))
|
||||||
)
|
)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.orderBy(creatorCommunityComment.createdAt.desc())
|
.orderBy(creatorCommunityComment.createdAt.desc())
|
||||||
.fetch()
|
.fetch()
|
||||||
|
.map {
|
||||||
|
it.copy(nickname = it.nickname.removeDeletedNicknamePrefix())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun blockedMemberIdSubQuery(memberId: Long) = JPAExpressions
|
||||||
|
.select(blockMember.blockedMember.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun blockingMemberIdSubQuery(memberId: Long) = JPAExpressions
|
||||||
|
.select(blockMember.member.id)
|
||||||
|
.from(blockMember)
|
||||||
|
.where(
|
||||||
|
blockMember.blockedMember.id.eq(memberId)
|
||||||
|
.and(blockMember.isActive.isTrue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import java.time.LocalDateTime
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
private const val DELETED_NICKNAME_PREFIX = "deleted_"
|
||||||
|
|
||||||
fun String.convertLocalDateTime(format: String): LocalDateTime {
|
fun String.convertLocalDateTime(format: String): LocalDateTime {
|
||||||
val dateTimeFormatter = DateTimeFormatter.ofPattern(format)
|
val dateTimeFormatter = DateTimeFormatter.ofPattern(format)
|
||||||
return LocalDateTime.parse(this, dateTimeFormatter)
|
return LocalDateTime.parse(this, dateTimeFormatter)
|
||||||
@@ -24,3 +26,11 @@ fun String.convertLocalDateTime(
|
|||||||
.withZoneSameInstant(ZoneId.of("UTC"))
|
.withZoneSameInstant(ZoneId.of("UTC"))
|
||||||
.toLocalDateTime()
|
.toLocalDateTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.removeDeletedNicknamePrefix(): String {
|
||||||
|
return if (startsWith(DELETED_NICKNAME_PREFIX)) {
|
||||||
|
removePrefix(DELETED_NICKNAME_PREFIX)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1770,6 +1770,16 @@ class SodaMessageSource {
|
|||||||
Lang.KO to "새 글이 등록되었습니다.",
|
Lang.KO to "새 글이 등록되었습니다.",
|
||||||
Lang.EN to "A new post has been added.",
|
Lang.EN to "A new post has been added.",
|
||||||
Lang.JA to "新しい投稿が登録されました。"
|
Lang.JA to "新しい投稿が登録されました。"
|
||||||
|
),
|
||||||
|
"explorer.channel_donation.message.default.public" to mapOf(
|
||||||
|
Lang.KO to "%s캔을 후원하셨습니다.",
|
||||||
|
Lang.EN to "You sponsored %s cans.",
|
||||||
|
Lang.JA to "%sCANを支援しました。"
|
||||||
|
),
|
||||||
|
"explorer.channel_donation.message.default.secret" to mapOf(
|
||||||
|
Lang.KO to "%s캔을 비밀후원하셨습니다.",
|
||||||
|
Lang.EN to "You secretly sponsored %s cans.",
|
||||||
|
Lang.JA to "%sCANをシークレット支援しました。"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class LiveRecommendService(
|
|||||||
return repository.getRecommendLive(
|
return repository.getRecommendLive(
|
||||||
isBlocked = {
|
isBlocked = {
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it)
|
isBlockedBetweenMembers(memberId = member.id!!, creatorId = it)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class LiveRecommendService(
|
|||||||
val onAirChannelList = repository.getOnAirRecommendChannelList(
|
val onAirChannelList = repository.getOnAirRecommendChannelList(
|
||||||
isBlocked = {
|
isBlocked = {
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it)
|
isBlockedBetweenMembers(memberId = member.id!!, creatorId = it)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ class LiveRecommendService(
|
|||||||
limit = (20 - onAirChannelList.size).toLong(),
|
limit = (20 - onAirChannelList.size).toLong(),
|
||||||
isBlocked = {
|
isBlocked = {
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it)
|
isBlockedBetweenMembers(memberId = member.id!!, creatorId = it)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ class LiveRecommendService(
|
|||||||
fun getFollowingChannelList(member: Member): List<GetRecommendChannelResponse> {
|
fun getFollowingChannelList(member: Member): List<GetRecommendChannelResponse> {
|
||||||
val onAirFollowingChannelList = repository.getOnAirFollowingChannelList(
|
val onAirFollowingChannelList = repository.getOnAirFollowingChannelList(
|
||||||
memberId = member.id!!,
|
memberId = member.id!!,
|
||||||
isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) },
|
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) },
|
||||||
isCreator = member.role == MemberRole.CREATOR,
|
isCreator = member.role == MemberRole.CREATOR,
|
||||||
isAdult = member.auth != null
|
isAdult = member.auth != null
|
||||||
)
|
)
|
||||||
@@ -83,7 +83,7 @@ class LiveRecommendService(
|
|||||||
memberId = member.id!!,
|
memberId = member.id!!,
|
||||||
withOutCreatorList = onAirCreatorIdList,
|
withOutCreatorList = onAirCreatorIdList,
|
||||||
limit = (20 - onAirCreatorIdList.size).toLong(),
|
limit = (20 - onAirCreatorIdList.size).toLong(),
|
||||||
isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) }
|
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
return onAirFollowingChannelList + notOnAirFollowingChannelList
|
return onAirFollowingChannelList + notOnAirFollowingChannelList
|
||||||
@@ -92,14 +92,14 @@ class LiveRecommendService(
|
|||||||
fun getFollowingAllChannelList(member: Member, pageable: Pageable): GetCreatorFollowingAllListResponse {
|
fun getFollowingAllChannelList(member: Member, pageable: Pageable): GetCreatorFollowingAllListResponse {
|
||||||
val totalCount = repository.getCreatorFollowingAllListTotalCount(
|
val totalCount = repository.getCreatorFollowingAllListTotalCount(
|
||||||
memberId = member.id!!,
|
memberId = member.id!!,
|
||||||
isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) }
|
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
val items = repository.getCreatorFollowingAllList(
|
val items = repository.getCreatorFollowingAllList(
|
||||||
memberId = member.id!!,
|
memberId = member.id!!,
|
||||||
offset = pageable.offset,
|
offset = pageable.offset,
|
||||||
limit = pageable.pageSize.toLong(),
|
limit = pageable.pageSize.toLong(),
|
||||||
isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) }
|
isBlocked = { isBlockedBetweenMembers(memberId = member.id!!, creatorId = it) }
|
||||||
)
|
)
|
||||||
|
|
||||||
return GetCreatorFollowingAllListResponse(
|
return GetCreatorFollowingAllListResponse(
|
||||||
@@ -107,4 +107,9 @@ class LiveRecommendService(
|
|||||||
items = items
|
items = items
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isBlockedBetweenMembers(memberId: Long, creatorId: Long): Boolean {
|
||||||
|
return blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) ||
|
||||||
|
blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = memberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ data class GetLiveRoomUserProfileResponse(
|
|||||||
val profileUrl: String,
|
val profileUrl: String,
|
||||||
val gender: String,
|
val gender: String,
|
||||||
val instagramUrl: String,
|
val instagramUrl: String,
|
||||||
|
val fancimmUrl: String,
|
||||||
|
val xUrl: String,
|
||||||
val youtubeUrl: String,
|
val youtubeUrl: String,
|
||||||
val websiteUrl: String,
|
val websiteUrl: String,
|
||||||
val blogUrl: String,
|
val blogUrl: String,
|
||||||
|
val kakaoOpenChatUrl: String,
|
||||||
val introduce: String,
|
val introduce: String,
|
||||||
val tags: String,
|
val tags: String,
|
||||||
val isSpeaker: Boolean?,
|
val isSpeaker: Boolean?,
|
||||||
|
|||||||
@@ -121,9 +121,15 @@ class LiveRoomQueryRepositoryImpl(
|
|||||||
.leftJoin(quarterLiveRankings).on(liveRoom.id.eq(quarterLiveRankings.roomId))
|
.leftJoin(quarterLiveRankings).on(liveRoom.id.eq(quarterLiveRankings.roomId))
|
||||||
|
|
||||||
if (hasMemberId) {
|
if (hasMemberId) {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
.and(
|
||||||
.and(blockMember.isActive.isTrue)
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
where = where.and(blockMember.id.isNull)
|
where = where.and(blockMember.id.isNull)
|
||||||
@@ -190,9 +196,15 @@ class LiveRoomQueryRepositoryImpl(
|
|||||||
.innerJoin(liveRoom.member, member)
|
.innerJoin(liveRoom.member, member)
|
||||||
|
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
.and(
|
||||||
.and(blockMember.isActive.isTrue)
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
where = where.and(blockMember.id.isNull)
|
where = where.and(blockMember.id.isNull)
|
||||||
@@ -269,9 +281,15 @@ class LiveRoomQueryRepositoryImpl(
|
|||||||
.limit(10)
|
.limit(10)
|
||||||
|
|
||||||
if (memberId != null) {
|
if (memberId != null) {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
.and(
|
||||||
.and(blockMember.isActive.isTrue)
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
select = select.leftJoin(blockMember).on(blockMemberCondition)
|
||||||
where = where.and(blockMember.id.isNull)
|
where = where.and(blockMember.id.isNull)
|
||||||
|
|||||||
@@ -500,6 +500,13 @@ class LiveRoomService(
|
|||||||
val room = repository.getLiveRoom(id = roomId)
|
val room = repository.getLiveRoom(id = roomId)
|
||||||
?: throw SodaException(messageKey = "live.room.already_ended")
|
?: throw SodaException(messageKey = "live.room.already_ended")
|
||||||
|
|
||||||
|
val creatorId = room.member!!.id!!
|
||||||
|
val isBlockedByCreator = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
||||||
|
val isBlockedByMember = blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = member.id!!)
|
||||||
|
if (isBlockedByCreator || isBlockedByMember) {
|
||||||
|
throw SodaException(messageKey = "live.room.already_ended")
|
||||||
|
}
|
||||||
|
|
||||||
if (room.isAdult && member.auth == null) {
|
if (room.isAdult && member.auth == null) {
|
||||||
throw SodaException(messageKey = "live.room.adult_verification_required")
|
throw SodaException(messageKey = "live.room.adult_verification_required")
|
||||||
}
|
}
|
||||||
@@ -747,6 +754,14 @@ class LiveRoomService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isBlockedByMember = blockMemberRepository.isBlocked(
|
||||||
|
blockedMemberId = room.member!!.id!!,
|
||||||
|
memberId = member.id!!
|
||||||
|
)
|
||||||
|
if (isBlockedByMember) {
|
||||||
|
throw SodaException(messageKey = "live.room.not_found")
|
||||||
|
}
|
||||||
|
|
||||||
val kickOutCount = kickOutService.getKickOutCount(roomId = room.id!!, userId = member.id!!)
|
val kickOutCount = kickOutService.getKickOutCount(roomId = room.id!!, userId = member.id!!)
|
||||||
if (kickOutCount >= 2) {
|
if (kickOutCount >= 2) {
|
||||||
throw SodaException(
|
throw SodaException(
|
||||||
@@ -910,6 +925,13 @@ class LiveRoomService(
|
|||||||
val room = repository.findByIdOrNull(roomId)
|
val room = repository.findByIdOrNull(roomId)
|
||||||
?: throw SodaException(messageKey = "live.room.info_not_found")
|
?: throw SodaException(messageKey = "live.room.info_not_found")
|
||||||
|
|
||||||
|
val creatorId = room.member!!.id!!
|
||||||
|
val isBlockedByCreator = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
||||||
|
val isBlockedByMember = blockMemberRepository.isBlocked(blockedMemberId = creatorId, memberId = member.id!!)
|
||||||
|
if (isBlockedByCreator || isBlockedByMember) {
|
||||||
|
throw SodaException(messageKey = "live.room.info_not_found")
|
||||||
|
}
|
||||||
|
|
||||||
val currentTimeStamp = Date().time
|
val currentTimeStamp = Date().time
|
||||||
val expireTimestamp = (currentTimeStamp + (60 * 60 * 24 * 1000)) / 1000
|
val expireTimestamp = (currentTimeStamp + (60 * 60 * 24 * 1000)) / 1000
|
||||||
|
|
||||||
@@ -1098,9 +1120,12 @@ class LiveRoomService(
|
|||||||
else -> messageSource.getMessage("member.gender.unknown", langContext.lang)
|
else -> messageSource.getMessage("member.gender.unknown", langContext.lang)
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
instagramUrl = user.instagramUrl,
|
instagramUrl = user.instagramUrl,
|
||||||
|
fancimmUrl = user.fancimmUrl,
|
||||||
|
xUrl = user.xUrl,
|
||||||
youtubeUrl = user.youtubeUrl,
|
youtubeUrl = user.youtubeUrl,
|
||||||
websiteUrl = user.websiteUrl,
|
websiteUrl = user.websiteUrl,
|
||||||
blogUrl = user.blogUrl,
|
blogUrl = user.blogUrl,
|
||||||
|
kakaoOpenChatUrl = user.websiteUrl,
|
||||||
introduce = user.introduce,
|
introduce = user.introduce,
|
||||||
tags = user.tags
|
tags = user.tags
|
||||||
.asSequence()
|
.asSequence()
|
||||||
@@ -1425,7 +1450,8 @@ class LiveRoomService(
|
|||||||
return repository.getLatestFinishedLive()
|
return repository.getLatestFinishedLive()
|
||||||
.filter {
|
.filter {
|
||||||
if (member?.id != null) {
|
if (member?.id != null) {
|
||||||
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.memberId)
|
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.memberId) &&
|
||||||
|
!blockMemberRepository.isBlocked(blockedMemberId = it.memberId, memberId = member.id!!)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ data class GetRoomDetailManager(
|
|||||||
val introduce: String,
|
val introduce: String,
|
||||||
val youtubeUrl: String?,
|
val youtubeUrl: String?,
|
||||||
val instagramUrl: String?,
|
val instagramUrl: String?,
|
||||||
val websiteUrl: String?,
|
val fancimmUrl: String?,
|
||||||
val blogUrl: String?,
|
val xUrl: String?,
|
||||||
|
val kakaoOpenChatUrl: String?,
|
||||||
val profileImageUrl: String,
|
val profileImageUrl: String,
|
||||||
val isCreator: Boolean
|
val isCreator: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -42,8 +43,9 @@ data class GetRoomDetailManager(
|
|||||||
introduce = member.introduce,
|
introduce = member.introduce,
|
||||||
youtubeUrl = member.youtubeUrl,
|
youtubeUrl = member.youtubeUrl,
|
||||||
instagramUrl = member.instagramUrl,
|
instagramUrl = member.instagramUrl,
|
||||||
websiteUrl = member.websiteUrl,
|
fancimmUrl = member.fancimmUrl,
|
||||||
blogUrl = member.blogUrl,
|
xUrl = member.xUrl,
|
||||||
|
kakaoOpenChatUrl = member.websiteUrl,
|
||||||
profileImageUrl = if (member.profileImage != null) {
|
profileImageUrl = if (member.profileImage != null) {
|
||||||
"$cloudFrontHost/${member.profileImage}"
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ data class Member(
|
|||||||
|
|
||||||
// SNS
|
// SNS
|
||||||
var instagramUrl = ""
|
var instagramUrl = ""
|
||||||
|
var fancimmUrl = ""
|
||||||
|
var xUrl = ""
|
||||||
var youtubeUrl = ""
|
var youtubeUrl = ""
|
||||||
var websiteUrl = ""
|
var websiteUrl = ""
|
||||||
var blogUrl = ""
|
var blogUrl = ""
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
|||||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
||||||
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
|
||||||
|
import kr.co.vividnext.sodalive.member.auth.AuthRepository
|
||||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||||
import kr.co.vividnext.sodalive.member.block.GetBlockedMemberListResponse
|
import kr.co.vividnext.sodalive.member.block.GetBlockedMemberListResponse
|
||||||
@@ -80,6 +81,7 @@ class MemberService(
|
|||||||
private val stipulationAgreeRepository: StipulationAgreeRepository,
|
private val stipulationAgreeRepository: StipulationAgreeRepository,
|
||||||
private val creatorFollowingRepository: CreatorFollowingRepository,
|
private val creatorFollowingRepository: CreatorFollowingRepository,
|
||||||
private val blockMemberRepository: BlockMemberRepository,
|
private val blockMemberRepository: BlockMemberRepository,
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
private val signOutRepository: SignOutRepository,
|
private val signOutRepository: SignOutRepository,
|
||||||
private val nicknameChangeLogRepository: NicknameChangeLogRepository,
|
private val nicknameChangeLogRepository: NicknameChangeLogRepository,
|
||||||
private val memberTagRepository: MemberTagRepository,
|
private val memberTagRepository: MemberTagRepository,
|
||||||
@@ -301,8 +303,9 @@ class MemberService(
|
|||||||
point = totalPoint,
|
point = totalPoint,
|
||||||
youtubeUrl = member.youtubeUrl,
|
youtubeUrl = member.youtubeUrl,
|
||||||
instagramUrl = member.instagramUrl,
|
instagramUrl = member.instagramUrl,
|
||||||
websiteUrl = member.websiteUrl,
|
fancimmUrl = member.fancimmUrl,
|
||||||
blogUrl = member.blogUrl,
|
xUrl = member.xUrl,
|
||||||
|
kakaoOpenChatUrl = member.websiteUrl,
|
||||||
liveReservationCount = liveReservationCount,
|
liveReservationCount = liveReservationCount,
|
||||||
isAuth = member.auth != null,
|
isAuth = member.auth != null,
|
||||||
orderList = orderList
|
orderList = orderList
|
||||||
@@ -520,25 +523,40 @@ class MemberService(
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun memberBlock(request: MemberBlockRequest, memberId: Long) {
|
fun memberBlock(request: MemberBlockRequest, memberId: Long) {
|
||||||
var blockMember = blockMemberRepository.getBlockAccount(
|
val member = repository.findByIdOrNull(id = memberId)
|
||||||
blockedMemberId = request.blockMemberId,
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
memberId = memberId
|
val blockedMember = repository.findByIdOrNull(id = request.blockMemberId)
|
||||||
)
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
|
||||||
if (blockMember == null) {
|
val blockTargetMemberIds = mutableSetOf(request.blockMemberId)
|
||||||
val blockedMember = repository.findByIdOrNull(id = request.blockMemberId)
|
blockedMember.auth?.let { auth ->
|
||||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
val verifiedMemberIds = authRepository.getMemberIdsByNameAndBirthAndDiAndGender(
|
||||||
|
name = auth.name,
|
||||||
|
birth = auth.birth,
|
||||||
|
di = auth.di,
|
||||||
|
gender = auth.gender
|
||||||
|
)
|
||||||
|
blockTargetMemberIds.addAll(verifiedMemberIds)
|
||||||
|
}
|
||||||
|
blockTargetMemberIds.remove(memberId)
|
||||||
|
|
||||||
val member = repository.findByIdOrNull(id = memberId)
|
blockTargetMemberIds.forEach { targetMemberId ->
|
||||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
val targetMember = repository.findByIdOrNull(id = targetMemberId) ?: return@forEach
|
||||||
|
|
||||||
blockMember = BlockMember()
|
var blockMember = blockMemberRepository.getBlockAccount(
|
||||||
blockMember.member = member
|
blockedMemberId = targetMemberId,
|
||||||
blockMember.blockedMember = blockedMember
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
blockMemberRepository.save(blockMember)
|
if (blockMember == null) {
|
||||||
} else {
|
blockMember = BlockMember()
|
||||||
blockMember.isActive = true
|
blockMember.member = member
|
||||||
|
blockMember.blockedMember = targetMember
|
||||||
|
|
||||||
|
blockMemberRepository.save(blockMember)
|
||||||
|
} else {
|
||||||
|
blockMember.isActive = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,12 +580,13 @@ class MemberService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return repository.findByNicknameAndOtherCondition(nickname, member)
|
return repository.findByNicknameAndOtherCondition(nickname, member)
|
||||||
.asSequence()
|
.filter {
|
||||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) }
|
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) &&
|
||||||
|
!blockMemberRepository.isBlocked(blockedMemberId = it.id!!, memberId = member.id!!)
|
||||||
|
}
|
||||||
.map {
|
.map {
|
||||||
GetRoomDetailUser(it, cloudFrontHost)
|
GetRoomDetailUser(it, cloudFrontHost)
|
||||||
}
|
}
|
||||||
.toList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -714,12 +733,16 @@ class MemberService(
|
|||||||
member.instagramUrl = profileUpdateRequest.instagramUrl
|
member.instagramUrl = profileUpdateRequest.instagramUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileUpdateRequest.websiteUrl != null) {
|
if (profileUpdateRequest.fancimmUrl != null) {
|
||||||
member.websiteUrl = profileUpdateRequest.websiteUrl
|
member.fancimmUrl = profileUpdateRequest.fancimmUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileUpdateRequest.blogUrl != null) {
|
if (profileUpdateRequest.xUrl != null) {
|
||||||
member.blogUrl = profileUpdateRequest.blogUrl
|
member.xUrl = profileUpdateRequest.xUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileUpdateRequest.kakaoOpenChatUrl != null) {
|
||||||
|
member.websiteUrl = profileUpdateRequest.kakaoOpenChatUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileUpdateRequest.isVisibleDonationRank != null) {
|
if (profileUpdateRequest.isVisibleDonationRank != null) {
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ data class ProfileResponse(
|
|||||||
val rewardCan: Int,
|
val rewardCan: Int,
|
||||||
val youtubeUrl: String?,
|
val youtubeUrl: String?,
|
||||||
val instagramUrl: String?,
|
val instagramUrl: String?,
|
||||||
val blogUrl: String?,
|
val fancimmUrl: String?,
|
||||||
val websiteUrl: String?,
|
val xUrl: String?,
|
||||||
|
val kakaoOpenChatUrl: String?,
|
||||||
val introduce: String,
|
val introduce: String,
|
||||||
val tags: List<String>
|
val tags: List<String>
|
||||||
) {
|
) {
|
||||||
@@ -29,8 +30,9 @@ data class ProfileResponse(
|
|||||||
rewardCan = member.getRewardCan(container),
|
rewardCan = member.getRewardCan(container),
|
||||||
youtubeUrl = member.youtubeUrl,
|
youtubeUrl = member.youtubeUrl,
|
||||||
instagramUrl = member.instagramUrl,
|
instagramUrl = member.instagramUrl,
|
||||||
websiteUrl = member.websiteUrl,
|
fancimmUrl = member.fancimmUrl,
|
||||||
blogUrl = member.blogUrl,
|
xUrl = member.xUrl,
|
||||||
|
kakaoOpenChatUrl = member.websiteUrl,
|
||||||
introduce = member.introduce,
|
introduce = member.introduce,
|
||||||
tags = member.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList()
|
tags = member.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ data class ProfileUpdateRequest(
|
|||||||
val introduce: String? = null,
|
val introduce: String? = null,
|
||||||
val youtubeUrl: String? = null,
|
val youtubeUrl: String? = null,
|
||||||
val instagramUrl: String? = null,
|
val instagramUrl: String? = null,
|
||||||
val websiteUrl: String? = null,
|
val fancimmUrl: String? = null,
|
||||||
val blogUrl: String? = null,
|
val xUrl: String? = null,
|
||||||
|
val kakaoOpenChatUrl: String? = null,
|
||||||
val isVisibleDonationRank: Boolean? = null,
|
val isVisibleDonationRank: Boolean? = null,
|
||||||
val donationRankingPeriod: DonationRankingPeriod? = null,
|
val donationRankingPeriod: DonationRankingPeriod? = null,
|
||||||
val container: String
|
val container: String
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface AuthRepository : JpaRepository<Auth, Long>, AuthQueryRepository
|
|||||||
interface AuthQueryRepository {
|
interface AuthQueryRepository {
|
||||||
fun getOldestCreatedAtByDi(di: String): LocalDateTime
|
fun getOldestCreatedAtByDi(di: String): LocalDateTime
|
||||||
fun getMemberIdsByDi(di: String): List<Long>
|
fun getMemberIdsByDi(di: String): List<Long>
|
||||||
|
fun getMemberIdsByNameAndBirthAndDiAndGender(name: String, birth: String, di: String, gender: Int): List<Long>
|
||||||
fun getAuthIdByMemberId(memberId: Long): Long?
|
fun getAuthIdByMemberId(memberId: Long): Long?
|
||||||
fun getActiveMemberIdsByDi(di: String): List<Long>
|
fun getActiveMemberIdsByDi(di: String): List<Long>
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,20 @@ class AuthQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AuthQ
|
|||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMemberIdsByNameAndBirthAndDiAndGender(name: String, birth: String, di: String, gender: Int): List<Long> {
|
||||||
|
return queryFactory
|
||||||
|
.select(member.id)
|
||||||
|
.from(member)
|
||||||
|
.leftJoin(member.auth, auth)
|
||||||
|
.where(
|
||||||
|
auth.name.eq(name)
|
||||||
|
.and(auth.birth.eq(birth))
|
||||||
|
.and(auth.di.eq(di))
|
||||||
|
.and(auth.gender.eq(gender))
|
||||||
|
)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAuthIdByMemberId(memberId: Long): Long? {
|
override fun getAuthIdByMemberId(memberId: Long): Long? {
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.select(auth.id)
|
.select(auth.id)
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ data class MyPageResponse(
|
|||||||
val point: Int,
|
val point: Int,
|
||||||
val youtubeUrl: String?,
|
val youtubeUrl: String?,
|
||||||
val instagramUrl: String?,
|
val instagramUrl: String?,
|
||||||
val websiteUrl: String? = null,
|
val fancimmUrl: String? = null,
|
||||||
val blogUrl: String? = null,
|
val xUrl: String? = null,
|
||||||
|
val kakaoOpenChatUrl: String? = null,
|
||||||
val liveReservationCount: Int,
|
val liveReservationCount: Int,
|
||||||
val isAuth: Boolean,
|
val isAuth: Boolean,
|
||||||
val orderList: GetAudioContentOrderListResponse
|
val orderList: GetAudioContentOrderListResponse
|
||||||
|
|||||||
@@ -28,9 +28,15 @@ class RecommendChannelQueryRepository(
|
|||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): List<RecommendChannelResponse> {
|
): List<RecommendChannelResponse> {
|
||||||
val blockMemberCondition = if (memberId != null) {
|
val blockMemberCondition = if (memberId != null) {
|
||||||
blockMember.member.id.eq(member.id)
|
blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -89,9 +95,15 @@ class RecommendChannelQueryRepository(
|
|||||||
locale: String? = null
|
locale: String? = null
|
||||||
): List<RecommendChannelContentItem> {
|
): List<RecommendChannelContentItem> {
|
||||||
val blockMemberCondition = if (memberId != null) {
|
val blockMemberCondition = if (memberId != null) {
|
||||||
blockMember.member.id.eq(audioContent.member.id)
|
blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(audioContent.member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(audioContent.member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,15 @@ class RankingRepository(
|
|||||||
) {
|
) {
|
||||||
fun getCreatorRankings(memberId: Long? = null): List<Member> {
|
fun getCreatorRankings(memberId: Long? = null): List<Member> {
|
||||||
val blockMemberCondition = if (memberId != null) {
|
val blockMemberCondition = if (memberId != null) {
|
||||||
blockMember.member.id.eq(member.id)
|
blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ class SearchRepository(
|
|||||||
keyword: String,
|
keyword: String,
|
||||||
memberId: Long
|
memberId: Long
|
||||||
): Int {
|
): Int {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.select(member.id)
|
.select(member.id)
|
||||||
@@ -61,9 +67,15 @@ class SearchRepository(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): List<SearchResponseItem> {
|
): List<SearchResponseItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return queryFactory
|
return queryFactory
|
||||||
.select(
|
.select(
|
||||||
@@ -102,9 +114,15 @@ class SearchRepository(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): Int {
|
): Int {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = audioContent.member.isActive.isTrue
|
var where = audioContent.member.isActive.isTrue
|
||||||
.and(audioContent.member.role.eq(MemberRole.CREATOR))
|
.and(audioContent.member.role.eq(MemberRole.CREATOR))
|
||||||
@@ -161,9 +179,15 @@ class SearchRepository(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): List<SearchResponseItem> {
|
): List<SearchResponseItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = audioContent.member.isActive.isTrue
|
var where = audioContent.member.isActive.isTrue
|
||||||
.and(audioContent.member.role.eq(MemberRole.CREATOR))
|
.and(audioContent.member.role.eq(MemberRole.CREATOR))
|
||||||
@@ -227,9 +251,15 @@ class SearchRepository(
|
|||||||
isAdult: Boolean,
|
isAdult: Boolean,
|
||||||
contentType: ContentType
|
contentType: ContentType
|
||||||
): Int {
|
): Int {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = series.isActive.isTrue
|
var where = series.isActive.isTrue
|
||||||
.and(audioContent.isActive.isTrue)
|
.and(audioContent.isActive.isTrue)
|
||||||
@@ -291,9 +321,15 @@ class SearchRepository(
|
|||||||
offset: Long,
|
offset: Long,
|
||||||
limit: Long
|
limit: Long
|
||||||
): List<SearchResponseItem> {
|
): List<SearchResponseItem> {
|
||||||
val blockMemberCondition = blockMember.member.id.eq(member.id)
|
val blockMemberCondition = blockMember.isActive.isTrue
|
||||||
.and(blockMember.isActive.isTrue)
|
.and(
|
||||||
.and(blockMember.blockedMember.id.eq(memberId))
|
blockMember.member.id.eq(member.id)
|
||||||
|
.and(blockMember.blockedMember.id.eq(memberId))
|
||||||
|
.or(
|
||||||
|
blockMember.member.id.eq(memberId)
|
||||||
|
.and(blockMember.blockedMember.id.eq(member.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var where = series.isActive.isTrue
|
var where = series.isActive.isTrue
|
||||||
.and(audioContent.isActive.isTrue)
|
.and(audioContent.isActive.isTrue)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaExceptionHandler
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.data.web.PageableHandlerMethodArgumentResolver
|
||||||
|
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||||
|
|
||||||
|
class ChannelDonationControllerTest {
|
||||||
|
private lateinit var channelDonationService: ChannelDonationService
|
||||||
|
private lateinit var controller: ChannelDonationController
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
channelDonationService = Mockito.mock(ChannelDonationService::class.java)
|
||||||
|
controller = ChannelDonationController(channelDonationService)
|
||||||
|
|
||||||
|
mockMvc = MockMvcBuilders
|
||||||
|
.standaloneSetup(controller)
|
||||||
|
.setControllerAdvice(SodaExceptionHandler(LangContext(), SodaMessageSource()))
|
||||||
|
.setCustomArgumentResolvers(
|
||||||
|
AuthenticationPrincipalArgumentResolver(),
|
||||||
|
PageableHandlerMethodArgumentResolver()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldReturnErrorResponseWhenRequesterIsAnonymous() {
|
||||||
|
mockMvc.perform(
|
||||||
|
get("/explorer/profile/channel-donation")
|
||||||
|
.param("creatorId", "1")
|
||||||
|
.param("page", "0")
|
||||||
|
.param("size", "5")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
.andExpect(jsonPath("$.message").value("로그인 정보를 확인해주세요."))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldForwardPageableAndMemberToServiceWhenControllerMethodIsCalled() {
|
||||||
|
val member = createMember(id = 7L, role = MemberRole.USER, nickname = "viewer")
|
||||||
|
val item = GetChannelDonationListItem(
|
||||||
|
id = 1001L,
|
||||||
|
memberId = member.id!!,
|
||||||
|
nickname = member.nickname,
|
||||||
|
profileUrl = "https://cdn.test/profile/default-profile.png",
|
||||||
|
can = 1000,
|
||||||
|
isSecret = false,
|
||||||
|
message = "1,000캔을 후원하셨습니다.",
|
||||||
|
createdAt = "2026-02-23T09:30:00"
|
||||||
|
)
|
||||||
|
val response = GetChannelDonationListResponse(totalCount = 1, items = listOf(item))
|
||||||
|
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationService.getChannelDonationList(
|
||||||
|
creatorId = 1L,
|
||||||
|
member = member,
|
||||||
|
offset = 10L,
|
||||||
|
limit = 5L
|
||||||
|
)
|
||||||
|
).thenReturn(response)
|
||||||
|
|
||||||
|
val apiResponse = controller.getChannelDonationList(
|
||||||
|
creatorId = 1L,
|
||||||
|
member = member,
|
||||||
|
pageable = PageRequest.of(2, 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(true, apiResponse.success)
|
||||||
|
assertEquals(1, apiResponse.data!!.totalCount)
|
||||||
|
assertEquals(1001L, apiResponse.data!!.items[0].id)
|
||||||
|
|
||||||
|
Mockito.verify(channelDonationService).getChannelDonationList(
|
||||||
|
creatorId = 1L,
|
||||||
|
member = member,
|
||||||
|
offset = 10L,
|
||||||
|
limit = 5L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(id: Long, role: MemberRole, nickname: String): Member {
|
||||||
|
val member = Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
member.id = id
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
class ChannelDonationMessageRepositoryTest @Autowired constructor(
|
||||||
|
private val channelDonationMessageRepository: ChannelDonationMessageRepository,
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) {
|
||||||
|
@Test
|
||||||
|
fun shouldFilterByDateAndSortByCreatedAtAndIdDescForViewer() {
|
||||||
|
val creator = saveMember(nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
val viewer = saveMember(nickname = "viewer", role = MemberRole.USER)
|
||||||
|
val otherUser = saveMember(nickname = "other", role = MemberRole.USER)
|
||||||
|
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val tieTime = now.minusDays(2)
|
||||||
|
|
||||||
|
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
|
||||||
|
val publicTieFirst = saveMessage(member = otherUser, creator = creator, can = 2, isSecret = false)
|
||||||
|
val publicTieSecond = saveMessage(member = viewer, creator = creator, can = 3, isSecret = false)
|
||||||
|
val secretMine = saveMessage(member = viewer, creator = creator, can = 4, isSecret = true)
|
||||||
|
val secretOther = saveMessage(member = otherUser, creator = creator, can = 5, isSecret = true)
|
||||||
|
|
||||||
|
updateCreatedAt(oldPublic.id!!, now.minusMonths(2))
|
||||||
|
updateCreatedAt(publicTieFirst.id!!, tieTime)
|
||||||
|
updateCreatedAt(publicTieSecond.id!!, tieTime)
|
||||||
|
updateCreatedAt(secretMine.id!!, now.minusDays(1))
|
||||||
|
updateCreatedAt(secretOther.id!!, now.minusHours(12))
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val list = channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
memberId = viewer.id!!,
|
||||||
|
isCreator = false,
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
startDateTime = now.minusMonths(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
memberId = viewer.id!!,
|
||||||
|
isCreator = false,
|
||||||
|
startDateTime = now.minusMonths(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(3, list.size)
|
||||||
|
assertEquals(secretMine.id, list[0].id)
|
||||||
|
assertEquals(publicTieSecond.id, list[1].id)
|
||||||
|
assertEquals(publicTieFirst.id, list[2].id)
|
||||||
|
assertEquals(3, totalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldIncludeAllRecentSecretMessagesForCreator() {
|
||||||
|
val creator = saveMember(nickname = "creator2", role = MemberRole.CREATOR)
|
||||||
|
val viewer = saveMember(nickname = "viewer2", role = MemberRole.USER)
|
||||||
|
val otherUser = saveMember(nickname = "other2", role = MemberRole.USER)
|
||||||
|
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val oldPublic = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
|
||||||
|
val recentPublic = saveMessage(member = viewer, creator = creator, can = 2, isSecret = false)
|
||||||
|
val recentSecretMine = saveMessage(member = viewer, creator = creator, can = 3, isSecret = true)
|
||||||
|
val recentSecretOther = saveMessage(member = otherUser, creator = creator, can = 4, isSecret = true)
|
||||||
|
|
||||||
|
updateCreatedAt(oldPublic.id!!, now.minusMonths(2))
|
||||||
|
updateCreatedAt(recentPublic.id!!, now.minusDays(3))
|
||||||
|
updateCreatedAt(recentSecretMine.id!!, now.minusDays(2))
|
||||||
|
updateCreatedAt(recentSecretOther.id!!, now.minusDays(1))
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
|
||||||
|
val list = channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
memberId = creator.id!!,
|
||||||
|
isCreator = true,
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
startDateTime = now.minusMonths(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
val totalCount = channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
memberId = creator.id!!,
|
||||||
|
isCreator = true,
|
||||||
|
startDateTime = now.minusMonths(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(3, list.size)
|
||||||
|
assertEquals(recentSecretOther.id, list[0].id)
|
||||||
|
assertEquals(recentSecretMine.id, list[1].id)
|
||||||
|
assertEquals(recentPublic.id, list[2].id)
|
||||||
|
assertEquals(3, totalCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||||
|
return memberRepository.saveAndFlush(
|
||||||
|
Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMessage(member: Member, creator: Member, can: Int, isSecret: Boolean): ChannelDonationMessage {
|
||||||
|
val message = ChannelDonationMessage(can = can, isSecret = isSecret)
|
||||||
|
message.member = member
|
||||||
|
message.creator = creator
|
||||||
|
return channelDonationMessageRepository.saveAndFlush(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCreatedAt(id: Long, createdAt: LocalDateTime) {
|
||||||
|
entityManager.createQuery(
|
||||||
|
"update ChannelDonationMessage m set m.createdAt = :createdAt where m.id = :id"
|
||||||
|
)
|
||||||
|
.setParameter("createdAt", createdAt)
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class ChannelDonationServiceTest {
|
||||||
|
private lateinit var canPaymentService: CanPaymentService
|
||||||
|
private lateinit var memberRepository: MemberRepository
|
||||||
|
private lateinit var channelDonationMessageRepository: ChannelDonationMessageRepository
|
||||||
|
private lateinit var service: ChannelDonationService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
canPaymentService = Mockito.mock(CanPaymentService::class.java)
|
||||||
|
memberRepository = Mockito.mock(MemberRepository::class.java)
|
||||||
|
channelDonationMessageRepository = Mockito.mock(ChannelDonationMessageRepository::class.java)
|
||||||
|
service = ChannelDonationService(
|
||||||
|
canPaymentService = canPaymentService,
|
||||||
|
memberRepository = memberRepository,
|
||||||
|
channelDonationMessageRepository = channelDonationMessageRepository,
|
||||||
|
messageSource = SodaMessageSource(),
|
||||||
|
langContext = LangContext(),
|
||||||
|
cloudFrontHost = "https://cdn.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldThrowWhenDonateCanIsLessThanOne() {
|
||||||
|
val member = createMember(id = 10L, role = MemberRole.USER, nickname = "viewer")
|
||||||
|
val request = PostChannelDonationRequest(
|
||||||
|
creatorId = 1L,
|
||||||
|
can = 0,
|
||||||
|
isSecret = false,
|
||||||
|
message = "",
|
||||||
|
container = "aos"
|
||||||
|
)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.donate(request, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("content.donation.error.minimum_can", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldPassUserVisibilityFlagToRepositoryWhenRequesterIsNotCreator() {
|
||||||
|
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer")
|
||||||
|
val message = ChannelDonationMessage(can = 3, isSecret = true, additionalMessage = "응원합니다")
|
||||||
|
message.id = 1001L
|
||||||
|
message.member = viewer
|
||||||
|
message.creator = creator
|
||||||
|
message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(1)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
Mockito.eq(0L),
|
||||||
|
Mockito.eq(5L),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(listOf(message))
|
||||||
|
|
||||||
|
val result = service.getChannelDonationList(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
member = viewer,
|
||||||
|
offset = 0,
|
||||||
|
limit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1, result.totalCount)
|
||||||
|
assertEquals(1, result.items.size)
|
||||||
|
assertEquals("3캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message)
|
||||||
|
|
||||||
|
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
Mockito.eq(0L),
|
||||||
|
Mockito.eq(5L),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldFormatCanWithCommaInDonationMessage() {
|
||||||
|
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
val viewer = createMember(id = 2L, role = MemberRole.USER, nickname = "viewer")
|
||||||
|
val message = ChannelDonationMessage(can = 1000, isSecret = true, additionalMessage = "응원합니다")
|
||||||
|
message.id = 1001L
|
||||||
|
message.member = viewer
|
||||||
|
message.creator = creator
|
||||||
|
message.createdAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(1)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(viewer.id!!),
|
||||||
|
Mockito.eq(false),
|
||||||
|
Mockito.eq(0L),
|
||||||
|
Mockito.eq(5L),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(listOf(message))
|
||||||
|
|
||||||
|
val result = service.getChannelDonationList(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
member = viewer,
|
||||||
|
offset = 0,
|
||||||
|
limit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("1,000캔을 비밀후원하셨습니다.\n\"응원합니다\"", result.items[0].message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldPassCreatorVisibilityFlagToRepositoryWhenRequesterIsCreatorSelf() {
|
||||||
|
val creator = createMember(id = 1L, role = MemberRole.CREATOR, nickname = "creator")
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findCreatorByIdOrNull(creator.id!!)).thenReturn(creator)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageTotalCount(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(true),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(0)
|
||||||
|
Mockito.`when`(
|
||||||
|
channelDonationMessageRepository.getChannelDonationMessageList(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(true),
|
||||||
|
Mockito.eq(0L),
|
||||||
|
Mockito.eq(5L),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
).thenReturn(emptyList())
|
||||||
|
|
||||||
|
service.getChannelDonationList(
|
||||||
|
creatorId = creator.id!!,
|
||||||
|
member = creator,
|
||||||
|
offset = 0,
|
||||||
|
limit = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageTotalCount(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(true),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
Mockito.verify(channelDonationMessageRepository).getChannelDonationMessageList(
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(creator.id!!),
|
||||||
|
Mockito.eq(true),
|
||||||
|
Mockito.eq(0L),
|
||||||
|
Mockito.eq(5L),
|
||||||
|
anyLocalDateTime()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(id: Long, role: MemberRole, nickname: String): Member {
|
||||||
|
val member = Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
member.id = id
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anyLocalDateTime(): LocalDateTime {
|
||||||
|
Mockito.any(LocalDateTime::class.java)
|
||||||
|
return LocalDateTime.MIN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package kr.co.vividnext.sodalive.extensions
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class StringExtensionsTest {
|
||||||
|
@Test
|
||||||
|
fun shouldRemoveDeletedPrefixWhenNicknameStartsWithDeletedPrefix() {
|
||||||
|
val nickname = "deleted_testUser"
|
||||||
|
|
||||||
|
val sanitizedNickname = nickname.removeDeletedNicknamePrefix()
|
||||||
|
|
||||||
|
assertEquals("testUser", sanitizedNickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldKeepNicknameWhenDeletedPrefixDoesNotExist() {
|
||||||
|
val nickname = "testUser"
|
||||||
|
|
||||||
|
val sanitizedNickname = nickname.removeDeletedNicknamePrefix()
|
||||||
|
|
||||||
|
assertEquals("testUser", sanitizedNickname)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldReturnEmptyStringWhenNicknameContainsOnlyDeletedPrefix() {
|
||||||
|
val nickname = "deleted_"
|
||||||
|
|
||||||
|
val sanitizedNickname = nickname.removeDeletedNicknamePrefix()
|
||||||
|
|
||||||
|
assertEquals("", sanitizedNickname)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,121 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Check if a commit message follows project rules
|
print_usage() {
|
||||||
# Rules: 50/72 formatting, no advertisements/branding
|
echo "Usage:"
|
||||||
# Usage: ./check-commit-message-rules.sh [commit-hash]
|
echo " $0"
|
||||||
# If no commit-hash is provided, checks the latest commit
|
echo " $0 <commit-hash>"
|
||||||
|
echo " $0 --message \"<commit-message>\""
|
||||||
|
echo " $0 --message-file <file-path>"
|
||||||
|
}
|
||||||
|
|
||||||
# Determine which commit to check
|
load_commit_message() {
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
commit_ref="HEAD"
|
local commit_ref="HEAD"
|
||||||
echo "Checking latest commit..."
|
echo "Checking latest commit..." >&2
|
||||||
else
|
git log -1 --pretty=format:"%s%n%b" "$commit_ref"
|
||||||
commit_ref="$1"
|
return
|
||||||
echo "Checking commit: $commit_ref"
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Get the commit message
|
case "$1" in
|
||||||
commit_message=$(git log -1 --pretty=format:"%s%n%b" "$commit_ref")
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--message)
|
||||||
|
shift
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "[FAIL] --message option requires a commit message"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Checking provided commit message..." >&2
|
||||||
|
printf '%s' "$*"
|
||||||
|
;;
|
||||||
|
--message-file)
|
||||||
|
shift
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "[FAIL] --message-file option requires a file path"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$1" ]; then
|
||||||
|
echo "[FAIL] Commit message file not found: $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Checking commit message file: $1" >&2
|
||||||
|
cat "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "[FAIL] Invalid arguments"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Split into subject and body
|
local commit_ref="$1"
|
||||||
subject=$(echo "$commit_message" | head -n1)
|
if ! git rev-parse --verify "$commit_ref^{commit}" >/dev/null 2>&1; then
|
||||||
body=$(echo "$commit_message" | tail -n +2 | sed '/^$/d')
|
echo "[FAIL] Invalid commit reference: $commit_ref"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking commit: $commit_ref" >&2
|
||||||
|
git log -1 --pretty=format:"%s%n%b" "$commit_ref"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
commit_message=$(load_commit_message "$@")
|
||||||
|
subject=$(printf '%s\n' "$commit_message" | head -n1)
|
||||||
|
body=$(printf '%s\n' "$commit_message" | tail -n +2)
|
||||||
|
|
||||||
echo "Checking commit message format..."
|
echo "Checking commit message format..."
|
||||||
echo "Subject: $subject"
|
echo "Subject: $subject"
|
||||||
|
|
||||||
# Check subject line length
|
exit_code=0
|
||||||
subject_length=${#subject}
|
|
||||||
if [ $subject_length -gt 50 ]; then
|
if [ -z "$subject" ]; then
|
||||||
echo "[FAIL] Subject line too long: $subject_length characters (max 50)"
|
echo "[FAIL] Subject must not be empty"
|
||||||
exit_code=1
|
exit 1
|
||||||
else
|
|
||||||
echo "[PASS] Subject line length OK: $subject_length characters"
|
|
||||||
exit_code=0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check body line lengths if body exists
|
subject_pattern='^([a-z]+)(\([a-z0-9._/-]+\))?(!)?: (.+)$'
|
||||||
if [ -n "$body" ]; then
|
if [[ "$subject" =~ $subject_pattern ]]; then
|
||||||
echo "Checking body line lengths..."
|
type="${BASH_REMATCH[1]}"
|
||||||
while IFS= read -r line; do
|
description="${BASH_REMATCH[4]}"
|
||||||
line_length=${#line}
|
|
||||||
if [ $line_length -gt 72 ]; then
|
|
||||||
echo "[FAIL] Body line too long: $line_length characters (max 72)"
|
|
||||||
echo "Line: $line"
|
|
||||||
exit_code=1
|
|
||||||
fi
|
|
||||||
done <<< "$body"
|
|
||||||
|
|
||||||
if [ $exit_code -eq 0 ]; then
|
echo "[PASS] Subject follows Conventional Commit format"
|
||||||
echo "[PASS] All body lines within 72 characters"
|
echo "[PASS] Type is lowercase: $type"
|
||||||
|
|
||||||
|
if printf '%s\n' "$description" | grep -Eq '[가-힣]'; then
|
||||||
|
echo "[PASS] Description contains Korean text"
|
||||||
|
else
|
||||||
|
echo "[FAIL] Description must contain Korean text"
|
||||||
|
exit_code=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "[INFO] No body content to check"
|
echo "[FAIL] Subject must match: <type>(scope): <description>"
|
||||||
|
echo " scope is optional, example: feat: 기능을 추가한다"
|
||||||
|
exit_code=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for advertisements, branding, or promotional content
|
if [ -n "$body" ] && printf '%s\n' "$body" | grep -Eq '^Refs:'; then
|
||||||
echo "Checking for advertisements and branding..."
|
while IFS= read -r refs_line; do
|
||||||
if echo "$commit_message" | grep -qi "generated with\|claude code\|anthropic\|co-authored-by.*claude\|🤖"; then
|
if ! printf '%s\n' "$refs_line" | grep -Eq '^Refs: #[0-9]+(, #[0-9]+)*$'; then
|
||||||
echo "[FAIL] Commit message contains advertisements, branding, or promotional content"
|
echo "[FAIL] Refs footer format is invalid: $refs_line"
|
||||||
exit_code=1
|
echo " expected format: Refs: #123 or Refs: #123, #456"
|
||||||
else
|
exit_code=1
|
||||||
echo "[PASS] No advertisements or branding detected"
|
fi
|
||||||
|
done < <(printf '%s\n' "$body" | grep -E '^Refs:')
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo "[PASS] Refs footer format is valid"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $exit_code -eq 0 ]; then
|
if [ $exit_code -eq 0 ]; then
|
||||||
echo "[PASS] Commit message follows all rules"
|
echo "[PASS] Commit message follows AGENTS.md rules"
|
||||||
else
|
else
|
||||||
echo "[FAIL] Commit message violates project rules"
|
echo "[FAIL] Commit message violates AGENTS.md rules"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
|
|||||||
Reference in New Issue
Block a user