Compare commits

...

1707 Commits

Author SHA1 Message Date
b69756ef81 Merge pull request 'test' (#343) from test into main
Reviewed-on: #343
2025-09-18 19:25:50 +00:00
dad517a953 feat(admin-character-list): 캐릭터 검색결과
- 캐릭터 목록과 동일한 내용으로 변경
2025-09-18 19:58:12 +09:00
eb2d093b02 feat(admin-character-list): 캐릭터 검색에 페이징 추가 2025-09-18 19:29:34 +09:00
67186bba55 feat(original): 원작
- 원천 원작, 원천 원작 링크, 글/그림 작가, 제작사, 태그 추가
2025-09-18 18:04:59 +09:00
1a3a9149a2 Merge pull request 'test' (#342) from test into main
Reviewed-on: #342
2025-09-16 06:11:32 +00:00
edeecad2ce feat(original-app): 원작 리스트
- 페이징 추가
2025-09-15 16:00:09 +09:00
387f5388d9 feat(original-app): 원작 상세, 캐릭터 리스트
- 원작 상세에 캐릭터 20개 조회
- 지정 원작에 속한 활성 캐릭터 목록 조회 API 추가
2025-09-15 15:32:20 +09:00
adcaa0a5fd fix(original): 캐릭터 수정
- 원작 ID가 0이 들어오면 캐릭터의 원작을 null로 처리한다.
2025-09-15 06:43:40 +09:00
47b2c1cb93 fix(original): 캐릭터 수정
- 원작 ID가 0이 들어오면 캐릭터의 원작을 null로 처리한다.
2025-09-15 06:17:55 +09:00
ce120a6d5d Merge pull request 'test' (#341) from test into main
Reviewed-on: #341
2025-09-14 20:33:50 +00:00
7f3589dcfb fix(original): 인기 캐릭터 조회
- 캐시 키 변경
2025-09-15 05:20:46 +09:00
b134c28c10 feat(original): 관리자 캐릭터 상세 조회
- 원작 데이터 추가
2025-09-15 05:18:01 +09:00
41c8d0367d feat(original): 원작별 캐릭터 조회 API 추가 2025-09-15 00:31:14 +09:00
3b148d549e feat(original-app): 앱용 원작 목록/상세 API 및 조회 로직 추가
- 공개 목록 API: 미인증 사용자는 19금 비노출, 활성 캐릭터가 1개 이상 연결된 원작만 반환, 총개수+리스트 제공
- 상세 API: 로그인/본인인증 필수, 원작 상세+소속 활성 캐릭터 리스트 반환
2025-09-14 23:27:58 +09:00
b6c96af8a2 feat(original): 원작 도메인과 캐릭터를 연결하기 위해 각각 검색 API 추가 2025-09-14 23:00:33 +09:00
4904625488 feat(original): 원작 도메인 추가, 관리자 CRUD/연결 API, 소프트 삭제, 서비스 계층 정비
- 원작 엔티티/레포지토리/관리자 API 구축(이미지 S3 업로드 포함)
- 캐릭터-원작 연관관계 및 관리자에서 배정/해제 API 제공
- 소프트 삭제(`isDeleted`) 도입 및 조회/수정/배정 로직에서 삭제 항목 필터링
- 컨트롤러-레포지토리 직접 접근 제거, `AdminOriginalWorkService`로 DB 접근 캡슐화
- 캐릭터 등록/수정에서 `originalWorkId` 지원 및 외부 API 업데이트 조건 분리
2025-09-14 22:33:30 +09:00
08b5fd23ab Merge pull request 'test' (#340) from test into main
Reviewed-on: #340
2025-09-14 08:51:11 +00:00
0574f4f629 feat(cache): 인기 캐릭터 조회에 윈도우 기반 동적 캐시 키 적용
- ChatCharacterService.getPopularCharacters()에 @Cacheable 추가
- 키: popular-chat-character:<windowStartEpoch>:<limit>
- 윈도우(매일 20:00 UTC) 전환 시 자동으로 신규 키 사용 → 전일 순위 캐시와 분리 보장

Why: 동일 윈도우 내 반복 요청의 DB 부하를 줄이고, 경계 전환 시 자연스러운 캐시 갱신을 보장.
2025-09-14 17:43:53 +09:00
4adc3e127c fix(popular): 일일 인기 캐릭터 집계 윈도우를 전날 완료 구간으로 고정
- UTC 20:00 경계 직후에도 [전날 20:00, 당일 20:00) 범위 사용으로 일일 순위 정확화
- RankingWindowCalculator.now(): lastBoundary 기반 [start, endExclusive) 계산
2025-09-14 17:28:33 +09:00
dd0a1c2293 fix(chat-character): 인기 캐릭터
- 캐시 제거
2025-09-14 16:46:56 +09:00
a07407417c fix(admin-chat-calculate): 캐릭터 정산 API
- ONLY_FULL_GROUP_BY 대응
- c2.image_path 집계식 적용
2025-09-13 05:01:52 +09:00
e33e3b43b7 fix(admin-chat-calculate): 캐릭터 정산 API
- ONLY_FULL_GROUP_BY 대응
2025-09-13 04:33:01 +09:00
634bf759ca feat(admin-chat-calculate): 캐릭터 정산 API에 채팅 횟수 구매(CHAT_QUOTA_PURCHASE) 추가 2025-09-13 03:54:24 +09:00
0ed29c6097 feat(admin-chat-calculate): 캐릭터 정산 API에 imagePath를 imageHost를 포함한 url로 변경 추가 2025-09-13 03:20:26 +09:00
b752434fbb feat(admin-chat-calculate): 캐릭터 정산 API에 totalCount 추가 2025-09-13 03:06:55 +09:00
eec63cc7b2 feat(admin-chat-calculate): 캐릭터별 정산 조회 API 추가 2025-09-13 02:00:30 +09:00
3dc9dd1f35 feat(character): 최근 등록된 캐릭터 전체보기 API
- 반환 값에 전체 개수 추가
2025-09-12 19:00:45 +09:00
88e287067b feat(character): 최근 등록된 캐릭터 전체보기 API 추가 2025-09-12 18:37:25 +09:00
eb18e2d009 Merge pull request 'test' (#339) from test into main
Reviewed-on: #339
2025-09-11 17:05:45 +00:00
27a3f450ef fix(character): 인기 캐릭터 응답을 DTO로 변경하여 jackson 직렬화 오류 해결
- ChatCharacterService.getPopularCharacters 반환을 List<ChatCharacter> → List<Character> DTO로 변경
- 캐시 대상도 DTO로 전환(@Cacheable 유지, 동적 키/고정 TTL 그대로 사용)
- 컨트롤러에서 불필요한 매핑 제거(서비스가 DTO로 반환)
- Character DTO 직렬화 안정성 확보(@JsonProperty 추가)
- 이미지 URL 생성 로직을 서비스로 이동하고 imageHost(@Value) 주입해 구성
2025-09-11 18:53:27 +09:00
58a46a09c3 fix(character): SpEL 정적 호출 오류로 @JvmStatic 추가 2025-09-11 18:21:13 +09:00
83a1316a64 feat(character): UTC 20시 경계 기반 인기 캐릭터 집계 구현 및 캐시 적용
- 집계 기준을 "채팅방 전체 메시지 수"로 변경하여 캐릭터별 인기 순위 산정
- Querydsl `PopularCharacterQuery` 추가: chat_message → chat_participant(CHARACTER) → chat_character 조인
- 시간 경계: UTC 20:00 기준 [windowStart, nextBoundary) 구간 사용(배타적 종료 `<`)
- `ChatCharacterService.getPopularCharacters`에 @Cacheable 적용
  - cacheNames: `popularCharacters_24h`
  - key: `RankingWindowCalculator.now('popular-chat-character').cacheKey`
  - 상위 20개 기본, `loadCharactersInOrder`로 랭킹 순서 보존
- `RankingWindowCalculator`: 경계별 동적 키 생성(`popular-chat-character:{windowStartEpoch}`) 및 윈도우 계산
- `RedisConfig`: 24시간 TTL 캐시 `popularCharacters_24h` 추가(문자열/JSON 직렬화 지정)
- `ChatCharacterController`: 메인 API에 인기 캐릭터 섹션 연동

WHY
- 20시(UTC) 경계 변경 시 키가 달라져 첫 조회에서 자동 재집계/재캐싱
- 방 전체 참여도를 반영해 보다 직관적인 인기 지표 제공
- 캐시(24h TTL)로 DB 부하 최소화, 경계 전환 후 자연 무효화
2025-09-11 18:06:40 +09:00
f05f146c89 fix(chat-quota): 유료 차감 후 무료·유료 동시 0일 때 next_recharge_at 설정 누락 수정
- 문제: 유료 잔여가 있을 때 유료 우선 차감 경로에서 `next_recharge_at` 설정 분기가 없어,
  무료/유료가 동시에 0이 되는 경우 다음 무료 충전 시점이 노출되지 않음
- 수정: `ChatRoomQuotaService.consumeOneForSend`의 유료 차감 분기에
  `remainingPaid==0 && remainingFree==0 && nextRechargeAt==null` 조건에서
  `now + 6h`로 `next_recharge_at`을 설정하도록 로직 추가
- 참고: 무료 차감 경로의 `next_recharge_at` 설정 및 입장 시 lazy refill 동작은 기존과 동일
2025-09-11 12:35:16 +09:00
a27852ed44 Merge pull request '캐릭터 챗봇' (#338) from test into main
Reviewed-on: #338
2025-09-10 06:08:47 +00:00
3782062f4a fix(chat-room): 입장/전송 next 계산 보완 및 채팅 가능 시 next=null 처리
enter:
roomPaid==0 && roomFree>0 && global<=0 → 글로벌 next
roomPaid==0 && roomFree==0 → (global<=0) ? max(roomNext, globalNext) : roomNext
채팅 가능(totalRemaining>0)인 경우 next=null 반환(유료>0 포함)
send:
totalRemaining==0 && global<=0 → max(roomNext, globalNext)
채팅 가능(totalRemaining>0)인 경우 next=null 반환
2025-09-10 13:31:27 +09:00
fd83abb46c feat(chat): 글로벌/방 쿼터 정책 개편, 결제/조회/차단/이관 로직 반영
글로벌: 무료 40, UTC 20:00 lazy refill(유료 제거)
방: 무료 10, 무료 0 순간 now+6h, 경과 시 lazy refill(무료=10, next=null)
전송: 유료 우선, 무료 사용 시 글로벌/룸 동시 차감, 조건 불충족 예외
API: 방 쿼터 조회/구매 추가(구매 시 30캔, UseCan에 roomId:characterId 기록)
next 계산: enter/send에서 경계 케이스 처리(max(room, global))
대화 초기화: 유료 쿼터 새 방으로 이관
2025-09-09 22:42:14 +09:00
a9d1b9f4a6 fix(character): 캐릭터 상세 조회 응답에 MBTI·성별·나이 필드 추가
- CharacterDetailResponse에 gender, age 필드 추가
- ChatCharacterController에서 gender, age 매핑
- 기존 엔티티(ChatCharacter)의 gender/age 활용
2025-09-05 16:55:50 +09:00
ad69dad725 fix(character-image): 리스트 응답 ownedCount에 프로필(+1) 반영
프로필 이미지는 무료로 항상 열람 가능하므로 보유 개수(ownedCount)에도
프로필 1장을 포함하도록 수정했습니다. 이를 통해 전체 개수(totalCount)와
보유 개수 산정 기준이 일관되게 맞춰집니다.
2025-09-01 16:33:53 +09:00
2f55303d16 feat(admin-curation): 리스트 정합성 개선 및 활성 캐릭터 수 DB 집계 적용
- 비활성(삭제) 큐레이션을 목록에서 제외: findByIsActiveTrueOrderBySortOrderAsc 사용
- 리스트 항목에 characterCount 추가 및 DB GROUP BY + COUNT로 직접 집계
- CharacterCurationMappingRepository: 집계용 프로젝션(CharacterCountPerCuration)과 countActiveCharactersByCurations 쿼리 추가
- CharacterCurationAdminService: listAll에서 집계 결과를 활용해 characterCount 매핑 (대량 엔티티 로딩 제거)
- CharacterCurationRepository: findMaxSortOrder 쿼리로 신규 등록 정렬 순서 계산에 활용
- 컨트롤러: 캐릭터 리스트 응답 DTO(CharacterCurationCharacterItemResponse) 사용, 이미지 URL은 CloudFront host + imagePath로 조립
2025-09-01 14:06:01 +09:00
3a9128a894 fix(character): 추가 정보 증분 업데이트 적용 및 값 필드 가변화
- 왜: 기존에는 추가 정보(memories, personalities, backgrounds, relationships) 수정 시 전체 삭제 후 재생성되어 변경 누락/DB 오버헤드가 발생함
- 무엇:
  - Memory/Personality/Background 값 필드(content/description/emotion)를 var로 전환해 in-place 업데이트 허용
  - 서비스 레이어에 증분 업데이트 로직 적용
    - 요청에 없는 항목만 제거, 기존 항목은 값만 갱신, 신규 키만 추가
    - relationships는 personName+relationshipName 복합 키 매칭(keyOf)으로 필드만 갱신
  - ChatCharacter 컬렉션에 orphanRemoval=true 설정하여 iterator.remove 시 고아 삭제 보장
  - updateChatCharacterWithDetails에서 clear/add 제거 → 증분 업데이트 메서드 호출로 변경
- 효과: DELETE+INSERT 제거로 성능 개선, ID/createdAt 유지로 감사 추적 용이, 데이터 정합성 향상
2025-09-01 12:29:26 +09:00
def6296d4d fix(chat-character): 캐릭터 등록/수정 API
- 재시도 규칙 제거
2025-09-01 11:03:46 +09:00
034472defa fix(chat-character): DB에서 speechStyle type을 varchar에서 text로의 변경에 따라 @Column(columnDefinition = "TEXT") 추가 2025-08-29 01:38:49 +09:00
550e4ac9ce fix(character-main): 최근 대화 캐릭터 조회에서 roomId 대신 characterId 반환 2025-08-28 19:50:20 +09:00
d26e0a89f6 feat(admin-curation): 큐레이션 캐릭터 다중 등록 및 검증 로직 개선
- 중복 ID 제거 및 0 이하 ID 필터링
- 조회 단계에서 활성 캐릭터만 조회하여 검증 포함
- 존재하지 않거나 비활성인 캐릭터는 건너뛰고 나머지만 등록
- 기존 매핑 있는 캐릭터는 무시, 다음 정렬 순서(nextOrder)로 일괄 추가
2025-08-28 19:22:31 +09:00
6767afdd35 feat(character-curation): 캐릭터 큐레이션 도메인/관리 API 추가 및 메인 화면 통합
- CharacterCuration/CharacterCurationMapping 엔티티 추가
- 리포지토리/서비스(조회·관리) 구현
- 관리자 컨트롤러에 등록/수정/삭제/정렬/캐릭터 추가·삭제·정렬 API 추가
- 앱 메인 API에 큐레이션 섹션 노출
- 정렬/소프트 삭제/활성 캐릭터 필터링 규칙 적용
2025-08-28 17:39:53 +09:00
a58de0cf92 feat(chat-room-list): 이미지 메시지면 최근 메시지를 [이미지]로 표시 2025-08-28 02:33:04 +09:00
df93f0e0ce feat(chat-quota): 30캔으로 충전시 유료 채팅 횟수
- 50 -> 40으로 변경
2025-08-28 00:22:15 +09:00
0b54b126db temp(chat-character): 최신 캐릭터 50개 조회 2025-08-28 00:21:07 +09:00
a94cf8dad9 feat(chat): 입장 라우팅 도입 및 라우팅 시 배경 이미지 URL 무시(null)
- baseRoom이 비활성/미참여면 동일 캐릭터의 내 활성 방으로 라우팅해 응답 구성
- 라우팅된 경우 bgImageUrl은 항상 null 처리; 대체 방 없으면 기존 예외 유지
2025-08-28 00:18:21 +09:00
2c3e12a42c fix(chat-room): 세션 종료 외부 API
- ContentType 설정 제거
2025-08-27 19:18:46 +09:00
c4dbdc1b8e fix(chat-room): 비활성 채팅방 접근 방지를 위해 조회 로직 일원화
- 이 변경으로 비활성화된 채팅방에 대한 메시지 전송/조회/입장/리셋 등 모든 경로에서 안전하게 접근이 차단됩니다.
2025-08-27 17:43:32 +09:00
42ed4692af feat(chat): 채팅방 초기화 API 추가 및 세션 종료 실패 시 롤백 처리
- /api/chat/room/{chatRoomId}/reset POST 엔드포인트 추가
- 초기화 절차: 30캔 결제 → 기존 방 나가기 → 동일 캐릭터로 새 방 생성 → 응답 반환
- 결제 시 CanUsage.CHAT_ROOM_RESET 신규 항목 사용(본인 귀속)
- ChatQuotaService.resetFreeToDefault 추가 및 초기화 성공 시 무료 10회로 리셋(nextRechargeAt 초기화)
- 사용내역 타이틀에 "캐릭터 톡 초기화" 노출(CanService)
- ChatRoomResetRequest DTO(container 포함) 추가
- leaveChatRoom에 throwOnSessionEndFailure 옵션 추가(기본 false 유지)
- endExternalSession에 throwOnFailure 옵션 추가: 최대 3회 재시도 후 실패 시 예외 전파 가능
- 채팅방 초기화 흐름에서는 외부 세션 종료 실패 시 예외를 던져 트랜잭션 롤백되도록 처리
2025-08-27 17:16:18 +09:00
258943535c feat(chat-room): 채팅방 입장 시 선택적 캐릭터 이미지 서명 URL 반환 및 파라미터 추가
enter API에 characterImageId 선택 파라미터 추가
동일 캐릭터/활성 여부/보유 여부 검증 후 5분 만료의 CloudFront 서명 URL 생성
ChatRoomEnterResponse에 bgImageUrl 필드 추가해 응답 포함
서명 URL 생성 실패 시 warn 로그만 남기고 null 반환하여 사용자 흐름 유지
기존 호출은 그대로 동작하며, 파라미터와 응답 필드 추가는 하위 호환됨
2025-08-27 15:18:24 +09:00
0347d767f0 feat(character-image): 캐릭터 이미지 리스트 첫 칸에 프로필 이미지 포함 및 페이징 보정
사용자 경험 향상을 위해 캐릭터 프로필 이미지를 이미지 리스트의 맨 앞에 노출하도록 변경.
2025-08-27 14:22:07 +09:00
48b0190242 feat(character-image): 보유 이미지 전용 목록 API 추가 및 DB 페이징 적용
- /api/chat/character/image/my-list 엔드포인트 추가
  - 로그인/본인인증 체크
  - 캐릭터 프로필 이미지를 리스트 맨 앞에 포함
  - 보유 이미지(무료 또는 구매 이력 존재)만 노출
  - CloudFront 서명 URL 발급로 접근 제어
- 페이징 로직 개선
  - 기존: 전체 조회 후 메모리에서 필터링/슬라이싱
  - 변경: QueryDSL로 DB 레벨에서 보유 이미지만 오프셋/리밋 조회
  - 프로필 아이템(인덱스 0) 포함을 고려하여 owned offset/limit 계산
  - 빈 페이지 요청 시 즉시 빈 결과 반환
- Repository
  - CharacterImageQueryRepository + Impl 추가
  - findOwnedActiveImagesByCharacterPaged(...) 구현
    - 구매 이력: CHAT_MESSAGE_PURCHASE, CHARACTER_IMAGE_PURCHASE만 인정, 환불 제외
    - 활성 이미지, sortOrder asc, id asc 정렬 + offset/limit
- Service
  - getCharacterImagePath(characterId) 추가
  - pageOwnedActiveByCharacterForMember(...) 추가
- Controller
  - my-list 응답 스키마는 list와 동일하게 totalCount/ownedCount/items 유지
  - 페이지 사이즈 상한 20 적용, 5분 만료 서명 URL
2025-08-26 23:52:30 +09:00
15d0952de8 fix(quota): 캐릭터 톡 채팅 쿼터 조회
- applyRefillOnEnterAndGetStatus를 적용하여 채팅 쿼터 조회 시 Lazy Refill 적용
2025-08-26 17:32:00 +09:00
84ebc1762b fix(quota): 채팅 쿼터 구매 시 사용 내역 문구
- 캐릭터 톡 이용권 구매
2025-08-26 17:28:06 +09:00
a096b16945 fix(quota): 채팅 쿼터 구매
- nextRechargeAt = null 설정
2025-08-26 17:06:35 +09:00
37ac52116a temp(quota): 기다리면 무료 쿼터 시간
- 테스트를 위해 임시로 1분으로 수정
2025-08-26 17:00:19 +09:00
fcb68be006 fix(chat-room): 채팅방 입장
- AI 채팅 쿼터 Lazy refill 적용을 위해 read/write 모두 가능하도록 Transaction 수정
2025-08-26 14:57:57 +09:00
048c48d754 fix(quota)!: AI 채팅 쿼터(무료/유료) 구매 Response를 ChatQuotaStatusResponse으로 변경 2025-08-26 13:57:02 +09:00
6ecac8d331 feat(quota)!: AI 채팅 쿼터(무료/유료) 도입 및 입장/전송 응답에 상태 포함
- ChatQuota 엔티티/레포/서비스/컨트롤러 추가
- 입장 시 Lazy refill 적용, 전송 시 무료 우선 차감 및 잔여/리필 시간 응답 포함
- ChatRoomEnterResponse에 totalRemaining/nextRechargeAtEpoch 추가
- SendChatMessageResponse 신설 및 send API 응답 스키마 변경
- CanUsage에 CHAT_QUOTA_PURCHASE 추가, CanPaymentService/CanService에 결제 흐름 반영
2025-08-26 13:22:49 +09:00
8b1dd7cb95 temp: 임시로 최신 캐릭터 30개 보여주는 것으로 수정 2025-08-25 17:37:51 +09:00
5a58fe9077 feat(chat): 이미지 메시지 조회 시 CloudFront 서명 URL 적용 및 DTO 변환 로직 공통화
- 조회 가능한(보유/무료/결제완료) 이미지 메시지의 이미지 URL을 ImageContentCloudFront.generateSignedURL(만료 5분)로 생성
- 접근 불가(미보유, 유료 미구매) 이미지 메시지는 기존 공개 호스트 URL(블러/스냅샷 경로) 유지
- ChatRoomService에 ImageContentCloudFront를 주입하고, toChatMessageItemDto에서 이미지 URL/hasAccess 결정 로직 단일화
- enterChatRoom, getChatMessages, sendMessage 경로의 중복된 DTO 매핑 로직 제거
- purchaseMessage 결제 완료 시 forceHasAccess=true로 접근 가능 DTO 반환
2025-08-25 14:28:11 +09:00
12574dbe46 feat(chat-room, payment): 유료 메시지 구매 플로우 구현 및 결제 연동(이미지 보유 처리 포함)
- 채팅 유료 메시지 구매 API 추가: POST /api/chat/room/{chatRoomId}/messages/{messageId}/purchase
- ChatRoomService.purchaseMessage 구현: 참여/유효성/가격 검증, 이미지 메시지 보유 시 결제 생략, 결제 완료 시 ChatMessageItemDto 반환
- CanPaymentService.spendCanForChatMessage 추가: UseCan에 chatMessage(+이미지 메시지면 characterImage) 연동 저장 및 게이트웨이 별 정산 기록(setUseCanCalculate)
- Character Image 결제 경로에 정산 기록 호출 누락분 보강
- ChatMessageItemDto 변환 헬퍼(toChatMessageItemDto) 추가 및 접근권한(hasAccess) 계산 일원화
2025-08-25 14:01:10 +09:00
b3e7c00232 feat(chat): 이미지/유료(PPV) 메시지 도입 — 엔티티·서비스·DTO 확장 및 트리거 전송
- ChatMessageType(TEXT/IMAGE) 도입
- ChatMessage에 messageType/characterImage/imagePath/price 추가
- ChatMessageItemDto에 messageType/imageUrl/price/hasAccess 추가
- 캐릭터 답변 로직
  - 텍스트 메시지 항상 저장/전송
  - 트리거 일치 시 이미지 메시지 추가 저장/전송
  - 미보유 시 blur + price 스냅샷, 보유 시 원본 + price=null
- enterChatRoom/getChatMessages 응답에 확장된 필드 매핑 및 hasAccess 계산 반영
2025-08-23 05:34:02 +09:00
692e060f6d feat(character-image): 이미지 단독 구매 API 및 결제 연동 추가
- 구매 요청/응답 DTO 추가
- 미보유 시 캔 차감 및 구매 이력 저장
- 서명 URL(5분) 반환
2025-08-22 21:37:18 +09:00
2ac0a5f896 feat(character-image): 캐릭터 이미지 리스트
- isAdult 값 추가
2025-08-22 01:21:04 +09:00
f8be99547a fix: ImageBlurUtil.kt
- 블러 radius 200 -> 240
2025-08-21 21:18:29 +09:00
7dd585c3dd fix: ImageBlurUtil.kt
- 블러 radius 160 -> 200
2025-08-21 21:10:35 +09:00
7355949c1e fix: ImageBlurUtil.kt
- 블러 radius 100 -> 160
2025-08-21 20:54:44 +09:00
539b9fb2b2 fix: 유저/관리자 캐릭터 이미지 리스트
- 불필요한 Response 제거
2025-08-21 20:52:39 +09:00
99386c6d53 fix: ImageBlurUtil.kt
- 블러 처리 방식 변경
2025-08-21 20:14:06 +09:00
abbd73ac00 fix: ImageBlurUtil.kt
- 블러 처리 방식 변경
2025-08-21 19:51:10 +09:00
4bee95c8a6 fix: ImageBlurUtil.kt
- 블러 적용 범위 radius 10 -> 50
2025-08-21 19:34:23 +09:00
090fc81829 fix: ImageBlurUtil.kt
- 블러 적용 범위 radius 10 -> 50
2025-08-21 19:10:37 +09:00
75100cacec fix: ImageContentCloudFront.kt
- host, key-pair-id, key-file-path 참조 변경
2025-08-21 18:09:17 +09:00
13fd262c94 feat(chat-character-image): 캐릭터 이미지 리스트 API 추가 및 보유 판단 로직 적용 2025-08-21 17:39:19 +09:00
8451cdfb80 fix(chat-character-image): 캐릭터 이미지 가격
- 이미지 단독 구매 가격과 메시지를 통한 구매 가겨으로 분리
2025-08-21 04:07:25 +09:00
c8841856c0 fix(chat-character-image): 캐릭터 이미지 트리거 수정
- triggers가 null이거나 빈 리스트이면 수정없이 실행종료
2025-08-21 04:01:47 +09:00
2a30b28e43 feat(chat-character-image): 캐릭터 이미지
- 등록시 블러 이미지를 생성하여 저장하는 기능 추가
2025-08-21 04:00:02 +09:00
dd6849b840 feat(chat-character-image): 캐릭터 이미지
- 등록, 리스트, 상세, 트리거 단어 업데이트, 삭제 기능 추가
2025-08-21 03:33:42 +09:00
ca27903e45 fix(character-comment): 캐릭터 상세 댓글 집계 및 최신 댓글 조회 기준 수정
- 최신 댓글 조회 시 원댓글(Parent=null)만 대상으로 조회하도록 Repository 메서드 및 Service 로직 변경

- 총 댓글 수를 "활성 원댓글 + 활성 부모를 가진 활성 답글"로 계산하여, 삭제된 원댓글의 답글은 집계에서 제외되도록 수정
2025-08-20 15:39:52 +09:00
aeab6eddc2 feat(chat-character-comment): 캐릭터 댓글의 답글에 댓글 내용 추가 2025-08-20 00:37:39 +09:00
1c0d40aed9 feat(chat-character-comment): 캐릭터 댓글에 글쓴이 ID 추가 2025-08-20 00:26:11 +09:00
1444afaae2 feat(chat-character-comment): 캐릭터 댓글 삭제 및 신고 API 추가
- 삭제 API: 본인 댓글에 대해 soft delete 처리
- 신고 API: 신고 내용을 그대로 저장하는 CharacterCommentReport 엔티티/리포지토리 도입
- Controller: 삭제, 신고 엔드포인트 추가 및 인증/본인인증 체크
- Service: 비즈니스 로직 구현 및 예외 처리 강화

왜: 캐릭터 댓글 관리 기능 요구사항(삭제/신고)을 충족하기 위함
무엇: 엔드포인트, 서비스 로직, DTO 및 JPA 엔티티/리포지토리 추가
2025-08-20 00:13:13 +09:00
a05bc369b7 feat(character-comment): 댓글/대댓글 API
- 커서를 추가하여 페이징 처리
2025-08-19 23:57:46 +09:00
6c7f411869 feat(character-comment): 캐릭터 댓글/답글 API 및 응답 확장
- 댓글 리스트에 댓글 개수 추가
2025-08-19 23:37:24 +09:00
f61c45e89a feat(character-comment): 캐릭터 댓글/답글 API 추가 및 상세 응답 확장
- 캐릭터 댓글 엔티티/레포지토리/서비스/컨트롤러 추가
  - 댓글 작성 POST /api/chat/character/{characterId}/comments
  - 답글 작성 POST /api/chat/character/{characterId}/comments/{commentId}/replies
  - 댓글 목록 GET /api/chat/character/{characterId}/comments?limit=20
  - 답글 목록 GET /api/chat/character/{characterId}/comments/{commentId}/replies?limit=20
- DTO 추가/확장
  - CharacterCommentResponse, CharacterReplyResponse, CharacterCommentRepliesResponse, CreateCharacterCommentRequest
- 캐릭터 상세 응답(CharacterDetailResponse) 확장
  - latestComment(최신 댓글 1건) 추가
  - totalComments(전체 활성 댓글 수) 추가
- 성능 최적화: getReplies에서 원본 댓글 replyCount 계산 시 DB 카운트 호출 제거
  - toCommentResponse(replyCountOverride) 도입으로 원본 댓글 replyCount=0 고정
- 공통 검증: 로그인/본인인증/빈 내용 체크, 비활성 캐릭터/댓글 차단

WHY
- 캐릭터 상세 화면에 댓글 경험 제공 및 전체 댓글 수 노출 요구사항 반영
- 답글 조회 시 불필요한 카운트 쿼리 제거로 DB 호출 최소화
2025-08-19 18:47:59 +09:00
27ed9f61d0 fix(chat): 채팅방 메시지 전송 API 반환값 수정
- 기존: SendChatMessageResponse으로 메시지 리스트를 한 번 더 Wrapping해서 보냄

- 수정: 메시지 리스트 반환
2025-08-14 22:00:42 +09:00
df77e31043 feat(chat): 채팅방 입장 API와 메시지 페이징 초기 로드 구현
- GET /api/chat/room/{chatRoomId}/enter 엔드포인트 추가
- 참여 검증 후 roomId, character(아이디/이름/프로필/타입) 제공
- 최신 20개 메시지 조회(내림차순 조회 후 createdAt 오름차순으로 정렬)
- hasMoreMessages 플래그 계산(가장 오래된 메시지 이전 존재 여부 판단)
2025-08-14 21:56:27 +09:00
2d65bdb8ee feat(chat): 채팅방 메시지 조회 API에 커서 기반 페이징 도입 및 createdAt 추가
cursor(< messageId) 기준의 커서 페이징 도입, 경계 exclusive 처리
limit 파라미터로 페이지 사이즈 가변화 (기본 20)
응답 스키마를 ChatMessagesPageResponse(messages, hasMore, nextCursor)로 변경
메시지 정렬을 createdAt 오름차순(표시 시간 순)으로 반환
ChatMessageItemDto에 createdAt(epoch millis) 필드 추가
레포지토리에 Pageable 기반 조회 및 이전 데이터 존재 여부 검사 메서드 추가
컨트롤러/서비스 시그니처 및 내부 로직 업데이트
2025-08-14 21:43:42 +09:00
4966aaeda9 fix(chat-room): 메시지 있는 방만 목록 조회되도록 쿼리 수정 2025-08-14 14:35:07 +09:00
28bd700b03 fix(chat-room): ONLY_FULL_GROUP_BY로 인한 그룹화 오류 수정 2025-08-14 14:09:31 +09:00
f2ca013b96 feat(chat-room): 채팅방 리스트 응답 개선(타입/미리보기/상대 이미지/시간)
- ChatRoomListQueryDto: characterType, lastActivityAt 필드 추가
- ChatRoomListItemDto: opponentType, lastMessagePreview, lastMessageTimeLabel 제공
- 레포지토리 정렬 기준을 최근 메시지 또는 생성일로 일원화(COALESCE)
2025-08-14 13:39:59 +09:00
6cf7dabaef feat(character): 홈의 최근 목록을 채팅방 기반으로 노출
- ChatRoomService.listMyChatRooms 사용, 최근 순 최대 10개 노출
- 방 title/imageUrl을 그대로 사용해 UI/데이터 일관성 유지
- 비로그인 사용자는 빈 배열 반환

refactor(dto): RecentCharacter.characterId → roomId로 변경
2025-08-14 00:53:35 +09:00
e6d63592ec fix(chat-character): 관계 스키마 변경에 따라 엔티티/CRUD/응답 DTO 수정
- ChatCharacterRelationship 엔티티를 personName, relationshipName, description(TEXT), importance, relationshipType, currentStatus로 변경

- ChatCharacter.addRelationship 및 Service 메서드 시그니처를 새 스키마에 맞게 수정

- 등록/수정 플로우에서 relationships 매핑 로직 업데이트

- Admin 상세 응답 DTO(RelationshipResponse) 및 매핑 업데이트

- 전체 빌드 성공
2025-08-13 19:49:46 +09:00
3ac4ebded3 feat(chat): 외부 캐릭터 챗 세션 API
- 응답값에 Response 모델에 JsonIgnoreProperties 추가하여 필요한 데이터만 파싱할 수 있도록 수정
2025-08-13 16:49:45 +09:00
6f9fc659f3 feat(chat): 외부 캐릭터 챗 세션 API URL 수정
- /api/session/... -> /api/sessions/...
2025-08-13 14:39:20 +09:00
005bb0ea2e feat(chat): 멤버가 최근에 대화한 캐릭터 목록
- ChatCharacterRepository.kt의 JPQL 정렬 절을 `ORDER BY MAX(COALESCE(m.createdAt, r.createdAt)) DESC`로 변경
2025-08-13 00:08:10 +09:00
80a0543e10 feat(admin-character): 캐릭터 배너 리스트 API
- 배너 이미지 URL - hostimagePath => host/imagePath로 수정
2025-08-12 23:38:18 +09:00
5d42805514 feat(admin-character): 캐릭터 배너 등록/수정 API
- 배너 이미지 저장 경로 수정
2025-08-12 23:26:13 +09:00
1b7ae8a2c5 feat(admin-character): 캐릭터 배너 등록/수정 API
- 배너 이미지 저장 경로 수정
2025-08-12 23:15:34 +09:00
168b0b13fb feat(admin-character): 캐릭터 배너 등록/수정 API
- request dto에 JsonProperty 추가
2025-08-12 23:02:18 +09:00
d99fcba468 feat(admin-character): 캐릭터 배너 등록/수정 API
- request를 JSON String으로 받도록 수정
2025-08-12 22:47:56 +09:00
147b8b0a42 feat(admin-character): 캐릭터 수정 API
- 가치관, 취미, 목표가 중복 매핑이 되지 않도록 수정
2025-08-12 21:51:52 +09:00
eed755fd11 feat(admin-character): 캐릭터 수정 API
- 태그 중복 매핑이 되지 않도록 수정
2025-08-12 21:03:06 +09:00
74a612704e feat(admin-character): 캐릭터 수정 API
- 태그 중복 매핑이 되지 않도록 수정
2025-08-12 20:40:25 +09:00
8defc56d1e ExternalApiData에 @JsonIgnoreProperties(ignoreUnknown = true)를 추가하여 없는 필드는 무시하도록 수정 2025-08-12 18:22:37 +09:00
1db20d118d ExternalApiResponse
- 각 필드에 JsonProperty 추가
2025-08-12 18:08:45 +09:00
7a70a770bb 캐릭터 Controller
- exception print
2025-08-12 17:54:39 +09:00
cc9e4f974f 캐릭터 Controller
- exception print
2025-08-12 17:38:45 +09:00
2965b8fea0 feat(admin-character): 캐릭터 리스트, 캐릭터 상세
- CharacterType: 첫 글자만 대문자, 나머지 소문자로 변경
- 이미지가 null 이면 ""으로 변경
2025-08-12 17:01:51 +09:00
00c617ec2e feat(admin-character): 캐릭터 상세 결과에 characterType 추가 2025-08-12 16:45:25 +09:00
01ef738d31 feat(chat-character): 캐릭터 상세 조회 응답 확장 및 ‘다른 캐릭터’ 추천 추가
- 상세 페이지 정보 강화 및 탐색성 향상을 위해 응답 필드를 확장
- CharacterDetailResponse에 originalTitle, originalLink, characterType, others 추가
- OtherCharacter DTO 추가 (characterId, name, imageUrl, tags)
- 공유 태그 기반으로 현재 캐릭터를 제외한 랜덤 10개 캐릭터 조회 JPA 쿼리 추가
  - ChatCharacterRepository.findRandomBySharedTags(@Query, RAND 정렬, 페이징)
- 서비스 계층에 getOtherCharactersBySharedTags 추가 및 태그 지연 로딩 초기화
- 컨트롤러에서:
  - others 리스트를 조회/매핑하여 응답에 포함
  - originalTitle, originalLink, characterType을 응답에 포함
2025-08-12 03:47:48 +09:00
423cbe7315 feat(chat-character): 캐릭터 상세 조회 응답 스키마 간소화 및 태그 포맷 규칙 적용
- CharacterDetailResponse에서 불필요 필드 제거
  - 제거: age, gender, speechPattern, speechStyle, appearance, memories, relationships, values, hobbies, goals
- 성격(personalities), 배경(backgrounds)을 각각 첫 번째 항목 1개만 반환하도록 변경
  - 단일 객체(Optional)로 응답: CharacterPersonalityResponse?, CharacterBackgroundResponse?
- 태그 포맷 규칙 적용
  - 태그에 # 프리픽스가 없으면 붙이고, 공백으로 연결하여 단일 문자열로 반환
- Controller 로직 정리
  - 불필요 매핑 제거 및 DTO 스키마 변경에 맞춘 변환 로직 반영
2025-08-12 03:16:29 +09:00
afb003c397 feat(chat-character): 원작/원작 링크/캐릭터 유형 추가 및 외부 API 호출 분리
- ChatCharacter 엔티티에 originalTitle, originalLink(Nullable), characterType(Enum) 필드 추가
  - characterType: CLONE | CHARACTER (기본값 CHARACTER)
  - 원작/원작 링크는 빈 문자열 대신 null 허용으로 저장
- Admin DTO(Register/Update)에 originalTitle, originalLink, characterType 필드 추가
- 등록 API에서 외부 API 요청 바디에 3개 필드(originalTitle, originalLink, characterType) 제외 처리
- 수정 API에서 3개 필드만 변경된 경우 외부 API 호출 생략하고 DB만 업데이트
  - hasChanges: 외부 API 대상 필드 변경 여부 판단(3개 필드 제외)
  - hasDbOnlyChanges: 3개 필드만 변경된 경우 처리 분기
- Service 계층에 필드 매핑 및 Enum 파싱 추가
  - createChatCharacter / createChatCharacterWithDetails에 originalTitle/originalLink/characterType 반영
- 이름 중복 검증 로직 유지, isActive=false 비활성화 이름 처리 로직 유지
2025-08-12 02:58:26 +09:00
2dc5a29220 feat(chat-character): 관계 name 필드 추가에 따른 등록/수정/조회 로직 및 DTO 반영
- 관계 스키마를 name, relationShip 구조로 일원화
- Admin/사용자 컨트롤러 조회 응답에서 관계를 객체로 반환하도록 수정
- 등록/수정 요청 DTO에 ChatCharacterRelationshipRequest(name, relationShip) 추가
- 서비스 계층 create/update/add 메소드 시그니처 및 매핑 로직 업데이트
- description 한 줄 소개 사용 전제 하의 관련 사용부 점검(엔티티 컬럼 구성은 기존 유지)
2025-08-12 02:13:46 +09:00
c525ec0330 feat(chat): 내 채팅방 목록 페이징 적용 및 page 파라미터 추가
- Repository에 Pageable 인자로 전달하여 DB 레벨 limit/offset 적용
- Service에서 PageRequest.of(page, 20)로 20개 페이지 처리 고정
- Controller /api/chat/room/list에 page 요청 파라미터 추가 및 전달

왜: 참여 중인 채팅방 목록이 페이징되지 않아 20개 단위로 최신 메시지 기준 내림차순 페이징 처리 필요
2025-08-11 14:26:00 +09:00
735f1e26df feat(chat-character): 최근 대화한 캐릭터 조회 구현 및 메인 API 연동
왜: 기존에는 채팅방 미구현으로 최근 대화 리스트를 빈 배열로 응답했음. 채팅방/메시지 기능이 준비됨에 따라 실제 최근 대화 캐릭터를 노출해야 함.
무엇:
- repository: findRecentCharactersByMember JPA 쿼리 추가 (채팅방/참여자/메시지 조인, 최신 메시지 기준 정렬)
- service: getRecentCharacters(member, limit) 구현 (member null 처리 및 페이징 적용)
- controller: /api/chat/character/main에서 인증 사용자 기준 최근 캐릭터 최대 10개 반환
2025-08-11 11:33:35 +09:00
5129400a29 fix(banner): 캐릭터 검색 결과
- Paging 관련 데이터 중 totalCount만 반환
2025-08-08 21:46:47 +09:00
a6a01aaa37 fix(banner): 캐릭터 검색
- 검색 결과에 imageHost와 imagePath 사이에 / 추가
2025-08-08 21:19:37 +09:00
b819df9656 feat(securityConfig): 아래 API는 로그인 하지 않아도 조회할 수 있도록 수정
- /api/chat/list
2025-08-08 17:31:21 +09:00
5d1c5fcc44 fix(chat): 채팅방 메시지
- 메시지 DB 타입을 TEXT로 변경
2025-08-08 17:11:38 +09:00
ebad3b31b7 fix(chat): 채팅방 메시지 전송 API
- 빈 메시지이면 전송하지 않고 반환
2025-08-08 16:52:30 +09:00
3e9f7f9e29 fix(chat): 채팅방, 채팅방 메시지, 채팅방 참여자 엔티티 이름 변경
- CharacterChatRoom -> ChatRoom
- CharacterChatMessage -> ChatMessage
- CharacterChatParticipant -> ChatParticipant
2025-08-08 16:47:47 +09:00
4b3463e97c feat(chat): 채팅방 메시지 전송 API 구현 2025-08-08 16:41:53 +09:00
002f2c2834 feat(chat): 채팅방 메시지 조회 API 구현 2025-08-08 16:00:30 +09:00
1509ee0729 feat(chat): 채팅방 나가기 API 구현 2025-08-08 15:48:20 +09:00
830e41dfa3 feat(chat): 채팅방 세션 조회 API 구현 2025-08-08 15:15:29 +09:00
4d1f84cc5c feat(chat-room): 채팅방 목록 API 응답 구조 개편 및 최근 메시지/프로필 이미지 제공\n\n- 페이징 객체 제거: ApiResponse<List<ChatRoomListItemDto>> 형태로 반환\n- 메시지 보낸 시간 필드 제거\n- 상대방(캐릭터) 프로필 이미지 URL 제공 (imageHost/imagePath 조합 -> imageUrl)\n- 가장 최근 메시지 1개 미리보기 제공 (최대 25자, 초과 시 ... 처리)\n- 목록 조회 쿼리 투영 DTO 및 정렬 로직 개선 (최근 메시지 없으면 방 생성 시간 사용)\n- 비인증/미본인인증 사용자: 빈 리스트 반환 2025-08-08 14:27:25 +09:00
1bafbed17c feat(chat): 채팅방 생성 API 구현
- 채팅방 생성 및 조회 기능 구현
- 외부 API 연동을 통한 세션 생성 로직 추가
- 채팅방 참여자(유저, 캐릭터) 추가 기능 구현
- UUID 기반 유저 ID 생성 로직 추가
2025-08-08 00:27:25 +09:00
694d9cd05a feat(character chat room): 채팅방, 채팅메시지, 채팅방 참여자 엔티티 구성 2025-08-07 23:35:57 +09:00
60172ae84d feat(character): 캐릭터 상세 조회 API 추가
- 캐릭터 ID로 상세 정보를 조회하는 API 엔드포인트 추가
- 캐릭터 상세 정보 조회 서비스 메서드 구현
- 캐릭터 상세 정보 응답 DTO 클래스 추가
2025-08-07 23:10:36 +09:00
7e7a1122fa refactor(character): 최근 등록된 캐릭터 조회 로직 개선
조회할 때부터 isActive = true, limit 10개를 불러오도록 리팩토링
- ChatCharacterRepository에 findByIsActiveTrueOrderByCreatedAtDesc 메소드 추가
- ChatCharacterService의 getNewCharacters 메소드 수정
2025-08-07 22:40:06 +09:00
a1533c8e98 feat(character): 캐릭터 메인 API 추가 2025-08-07 22:33:29 +09:00
b0a6fc6498 feat: weraser api 연동 부분
- exception 발생시 exception message도 같이 출력
2025-08-07 21:18:29 +09:00
74ed7b20ba feat: 캐릭터 생성/수정 Request
- JsonProperty 추가
2025-08-07 20:48:27 +09:00
206c25985a fix: 캐릭터 리포지토리
- active -> isActive로 변경
2025-08-07 16:52:41 +09:00
0001697274 fix: 환경변수 값 변수명 수정 2025-08-07 16:15:56 +09:00
add21c45c5 fix(캐릭터 성격특성): description SQL 컬럼 타입 TEXT로 변경 2025-08-07 16:01:53 +09:00
ef8458c7a3 feat(banner): 정렬 순서 추가 2025-08-07 15:31:03 +09:00
81f972edc1 fix(banner): ChatCharacterBanner 엔티티의 isActive 속성 참조 오류 수정
- 사용하지 않는 메서드 제거
2025-08-07 14:45:28 +09:00
c729a402aa feat(banner): 배너 등록/수정/삭제 API 2025-08-07 14:38:09 +09:00
2335050834 feat(admin): 관리자 페이지 캐릭터 상세 API 구현 2025-08-07 12:30:19 +09:00
6340ed27cf fix(chat): ChatCharacter 엔티티의 isActive 속성 참조 오류 수정 2025-08-07 12:01:34 +09:00
618f80fddc feat(admin): 관리자 페이지 캐릭터 리스트 API 구현
1. isActive가 true인 캐릭터만 조회하는 기능 구현
2. 페이징 처리 구현 (기본 20개 조회)
3. 필요한 데이터 포함 (id, 캐릭터명, 프로필 이미지, 설명, 성별, 나이, MBTI, 태그, 성격, 말투, 등록일, 수정일)
2025-08-07 11:59:21 +09:00
45b6c8db96 git commit -m "fix(chat): 캐릭터 등록/수정 API
- 이름 중복 검사 로직 추가
2025-08-06 22:19:52 +09:00
5132a6b9fa feat(character): 캐릭터 수정 API 구현
- ChatCharacterUpdateRequest 클래스 추가 (모든 필드 nullable)
- ChatCharacter 엔티티의 필드를 var로 변경하여 수정 가능하게 함
- 이미지 포함/제외 수정 API를 하나로 통합
- 변경된 데이터만 업데이트하도록 구현
- isActive가 false인 경우 특별 처리 추가
2025-08-06 21:59:16 +09:00
de6642b675 git commit -m "feat(chat): 캐릭터 등록 API 구현
- 외부 API 호출 및 응답 처리 구현
- 이미지 파일 S3 업로드 기능 추가
- Multipart 요청 처리 지원"
2025-08-06 20:51:01 +09:00
3b42399726 feat: 255자 넘어가야 하는 필드 columnDefinition = "TEXT" 추가 2025-08-06 18:44:56 +09:00
689f9fe48f feat(chat): ChatCharacter와 다른 엔티티 간 관계 구현
ChatCharacter와 Memory, Personality, Background, Relationship 간 1:N 관계 설정
Tag, Value, Hobby, Goal 엔티티의 중복 방지 및 관계 매핑 구현
관계 설정을 위한 서비스 및 리포지토리 클래스 추가
2025-08-06 17:42:48 +09:00
73038222cc feat: .junie/, .kiro/ 폴더 이하 파일들 git에 포함되지 않도록 코드 추가 2025-08-05 16:41:53 +09:00
c7925c1706 Merge pull request 'feat: 최근 공지사항 API 추가' (#337) from test into main
Reviewed-on: #337
2025-07-28 02:16:19 +00:00
2659adb7a9 feat: 최근 공지사항 API 추가 2025-07-25 21:44:32 +09:00
be59bd7e89 Merge pull request 'fix: 크리에이터 팔로우 API' (#336) from test into main
Reviewed-on: #336
2025-07-21 13:52:34 +00:00
fcb2ca1917 fix: 크리에이터 팔로우 API
- 본인은 팔로우 되지 않도록 수정
2025-07-21 22:30:19 +09:00
51ce143fc2 Merge pull request 'test' (#335) from test into main
Reviewed-on: #335
2025-07-21 11:46:56 +00:00
804e139385 fix: 라이브 메인 API - 최근 종료된 라이브
- 쿼리 최적화
2025-07-21 20:39:54 +09:00
f0fc996426 fix: 라이브 메인 API - 최근 종료된 라이브
- 날짜 제한 1주
2025-07-21 20:28:21 +09:00
89eb11f808 Merge pull request 'fix: 라이브 메인 API - 최근 종료된 라이브' (#334) from test into main
Reviewed-on: #334
2025-07-21 10:59:38 +00:00
efdb485a3b fix: 라이브 메인 API - 최근 종료된 라이브
- 날짜 제한 2주
2025-07-21 19:44:38 +09:00
30d89987a4 Merge pull request 'test' (#333) from test into main
Reviewed-on: #333
2025-07-21 09:54:56 +00:00
3d695069a2 fix: 홈 메인 API - 인기 크리에이터
- 팔로잉 여부 추가
2025-07-21 18:21:53 +09:00
e068b57062 fix: 라이브 메인 API - 최근 종료한 라이브
- 팔로잉 여부 제거
2025-07-21 18:05:33 +09:00
811810cd36 fix: GetCommunityPostListResponse
- json property 제거
2025-07-21 16:45:58 +09:00
c90df4b02b fix: 라이브 메인 API
- 테마별 최신콘텐츠 캐시 제거
2025-07-21 16:44:10 +09:00
7c1082f833 fix: 라이브 메인 API
- @JsonProperty 애노테이션 추가
2025-07-21 16:31:05 +09:00
800b8d3216 fix: 라이브 메인 API
- @JsonProperty 애노테이션 추가
2025-07-21 16:18:33 +09:00
ab877beae1 fix: 라이브 메인 API
- redis caching이 적용된 data class에 @JsonProperty 애노테이션 추가
2025-07-21 15:48:40 +09:00
046c163e6f feat: 라이브 메인 API
- 기존에 섹션별로 따로따로 호출하던 것을 하나로 합쳐서 호출할 수 있도록 API 추가
2025-07-21 15:14:47 +09:00
7959d3e5ed Merge pull request 'test' (#332) from test into main
Reviewed-on: #332
2025-07-18 12:33:22 +00:00
8e877a6366 fix: 라이브 다시듣기 콘텐츠 API 추가 2025-07-18 20:27:02 +09:00
d18c19dd35 fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 18:09:00 +09:00
a99260209b fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 18:00:36 +09:00
2192ddc8fa fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 17:50:18 +09:00
741a1282a3 fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 17:30:48 +09:00
1a6a331ad8 fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 17:22:05 +09:00
1ba63e2cab fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 17:13:37 +09:00
5696240e03 fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 16:49:18 +09:00
885243a5b0 fix: 최근 종료한 라이브 API 오류 수정 2025-07-18 16:35:15 +09:00
a849d00c7f fix: 최근 종료한 라이브 API 오류 수정
- SQLSyntaxErrorException 오류수정
- select 값에 집계쿼리를 넣어서 해결
2025-07-18 15:46:20 +09:00
d04b44c931 fix: 최근 종료한 라이브 API
- 차단 당한 크리에이터는 안보이도록 수정
- 20개 미만이면 재시도 처리
- 재시도 최대 횟수 3회
2025-07-18 14:40:16 +09:00
a3aad9d2c9 feat: 최근 종료한 라이브 20개 가져오는 API 추가 2025-07-18 14:15:03 +09:00
d98268f809 refactor: timeAgo 함수
- LocalDateTime 확장함수 처리
2025-07-18 13:33:19 +09:00
34440e9ba3 fix: 라이브 후원 합계 API
- 안쓰는 파라미터 제거
2025-07-17 19:36:10 +09:00
d1c889e5f2 fix: 라이브 리스트 API
- 라이브 시작 시간 UTC 추가
2025-07-17 18:58:48 +09:00
1e29573ef7 Merge pull request 'fix: 검색 API' (#331) from test into main
Reviewed-on: #331
2025-07-16 10:58:56 +00:00
55da259510 fix: 검색 API
- 콘텐츠, 시리즈 검색에서 크리에이터의 닉네임으로도 검색 되도록 수정
2025-07-16 19:00:39 +09:00
cc2f533dc6 Merge pull request 'fix: 메인 홈 API - 요일별 시리즈' (#330) from test into main
Reviewed-on: #330
2025-07-14 19:14:06 +00:00
4436e6f20a fix: 메인 홈 API - 요일별 시리즈
- 시리즈 생성 날짜 내림차순 정렬
2025-07-15 04:03:38 +09:00
32b0c19f9d Merge pull request 'test' (#329) from test into main
Reviewed-on: #329
2025-07-14 17:57:26 +00:00
3cedd36e15 fix: 메인 홈 API
- 기존 홈 탭 상단에 있는 배너 임시 추가
2025-07-15 02:46:14 +09:00
ecbe9b2e93 . 2025-07-15 02:38:29 +09:00
9ad6b6ea48 fix: 메인 홈 API - 최신 콘텐츠
- 무료/유료 콘텐츠 모두 조회 되도록 수정
2025-07-15 01:32:45 +09:00
0d2daf4d2c fix: 메인 홈 API - 추천 채널
- 미인증 계정에서 19금 콘텐츠가 조회되지 않도록 수정
2025-07-15 01:29:57 +09:00
edf16a6021 fix: 메인 홈 API
- 기존 홈 탭 상단에 있는 배너 임시 추가
2025-07-15 01:10:00 +09:00
9af2d768e8 Merge pull request 'test' (#327) from test into main
Reviewed-on: #327
2025-07-14 11:07:57 +00:00
7551a19b34 fix: 메인 홈 API
- 로그인 하지 않고 조회가 가능하도록 수정
2025-07-14 18:48:57 +09:00
f59f45d9a4 fix: 메인 홈 - 추천 채널
- 콘텐츠가 빈 리스트로 반환되는 버그 수정
2025-07-12 03:18:37 +09:00
81e82ad731 fix: 메인 홈 - 추천 채널
- 콘텐츠가 빈 리스트로 반환되는 버그 수정
2025-07-12 02:53:31 +09:00
ca870392e2 fix: 메인 홈 - 요일별 시리즈
- groupBy 이후 없는 컬럼으로 정렬한 오류 수정
2025-07-12 00:43:45 +09:00
a7e167a95f fix: 메인 홈 - 요일별 시리즈
- groupBy 추가하여 동일한 시리즈가 여러개 추가되어 있는 버그 수정
2025-07-11 23:39:55 +09:00
a49b82a7c2 fix: 메인 홈 - 인기 크리에이터
- 팔로워 수 추가
2025-07-11 20:00:39 +09:00
704ad12ccf fix: 메인 홈 - getContentCurationList
- 캐시 제거
2025-07-11 19:36:29 +09:00
ab9fd2bc16 fix: 메인 홈 - GetAudioContentMainItem
- JsonProperty를 isPointAvailable 수정
- @JsonProperty를 -> @get:JsonProperty, @param:JsonProperty로 수정
2025-07-11 18:43:07 +09:00
69a63a77d3 fix: 메인 홈 - GetAudioContentMainItem
- JsonProperty를 pointAvailable 수정하여 Redis Cache에서 데이터 가져올 떄 파싱이 이뤄질 수 있도록 수정
2025-07-11 18:02:52 +09:00
da7e4c2156 fix: 메인 홈 - GetContentCurationResponse
- JsonProperty를 추가하여 Redis Cache에서 데이터 가져올 떄 파싱이 이뤄질 수 있도록 수정
2025-07-11 17:46:22 +09:00
a4b5185f6b fix: 메인 홈 - 최근 콘텐츠 조회
- join 하지 않은 blockMember 제거
- 정렬 조건 추가 - id 내림차순
2025-07-11 14:04:08 +09:00
22fc8b22b8 feat: 메인 홈
- API 추가
2025-07-10 15:31:41 +09:00
a8da17162a feat: 커뮤니티 글 등록/수정
- 유료 글에서만 gif를 등록할 수 있도록 수정
2025-07-03 15:26:35 +09:00
5677824cde Merge pull request 'test' (#326) from test into main
Reviewed-on: #326
2025-06-13 11:37:26 +00:00
f13c221fd6 fix: 커뮤니티 댓글 조회
- 결과값에 isSecret(비밀 댓글 여부) 추가
2025-06-13 16:51:10 +09:00
4ffa9363a8 fix: 커뮤니티 댓글 조회
- 프로필 이미지 imageHost에 /가 포함되도록 수정
2025-06-13 16:06:44 +09:00
6d2f48f86d fix: 커뮤니티 댓글 조회
- 크리에이터가 아닌 경우 내가 쓴 비밀댓글 + 일반댓글만 조회되도록 수정
2025-06-12 19:08:47 +09:00
8e01ced1f5 feat: 커뮤니티 댓글
- 유료 커뮤니티 글을 구매한 경우 비밀 댓글 쓰기 기능 추가
2025-06-12 16:10:32 +09:00
e8f1bc09f9 Merge pull request 'test' (#325) from test into main
Reviewed-on: #325
2025-06-12 05:00:31 +00:00
640f5ce6f5 fix: 팔로워 리스트
- 차단한 멤버는 팔로워 리스트에 보이지 않도록 수정
2025-06-12 13:51:03 +09:00
c0be30027c fix: 팔로워 리스트
- 차단한 멤버는 팔로워 리스트에 보이지 않도록 수정
2025-06-12 13:44:09 +09:00
832586bd41 fix: 팔로워 리스트
- 차단한 멤버는 팔로워 리스트에 보이지 않도록 수정
2025-06-12 13:25:51 +09:00
1a774937b3 fix: 커뮤니티 게시물 조회
- isAdult를 무조건 false로 조회되던 문제를 게시물의 isAdult에 따라 다르게 조회되도록 수정
2025-06-12 12:00:21 +09:00
d1a936d55b Merge pull request 'test' (#324) from test into main
Reviewed-on: #324
2025-06-10 11:01:31 +00:00
e508dafb34 feat: 시리즈 상세 콘텐츠 리스트 - 포인트 사용 가능 여부 추가 2025-06-10 18:03:52 +09:00
8335717741 feat: 크리에이터 채널 콘텐츠 리스트 - 포인트 사용 가능 여부 추가 2025-06-10 14:44:54 +09:00
16a2b82ffd feat: 콘텐츠 메인, 콘텐츠 랭킹 - 포인트 사용 가능 여부 추가 2025-06-10 11:14:48 +09:00
8db5c6443d fix: 쿠폰 사용 - 쿠폰 사용 완료 안내 문구 수정 2025-06-09 17:17:52 +09:00
9ed717fb95 feat: 쿠폰 사용 - 쿠폰 사용 완료 안내 문구 적용 2025-06-09 16:52:19 +09:00
dcd4497315 feat: 포인트 내역 - 쿠폰으로 충전한 포인트 내역도 조회할 수 있도록 포인트 정책과의 조인을 leftJoin으로 변경 2025-06-09 16:36:46 +09:00
54c0322398 feat: 쿠폰 사용 - 포인트 쿠폰이면 포인트 충전 되도록 로직 수정 2025-06-09 15:16:11 +09:00
e3c33c71a0 feat: 쿠폰 생성, 쿠폰 리스트
- 쿠폰 타입(캔, 포인트) 추가
2025-06-09 14:47:33 +09:00
dc97eaa835 Merge pull request 'fix: 앱 콘텐츠 수정' (#323) from test into main
Reviewed-on: #323
2025-06-05 02:36:25 +00:00
7055bb9872 fix: 앱 콘텐츠 수정
- 태그 수정, 포인트 사용여부 수정 기능
2025-06-04 17:21:08 +09:00
dcbe57806c Merge pull request 'test' (#322) from test into main
Reviewed-on: #322
2025-06-02 12:41:46 +00:00
fd1b17e356 fix: 크리에이터 관리자 콘텐츠 수정 - 태그 수정 기능
- 이미 있는 태그는 다시 추가되지 않도록 추가
2025-06-02 21:33:00 +09:00
28427a873a fix: 크리에이터 관리자 콘텐츠 수정 - 태그 수정 기능
- 이미 있는 태그는 다시 추가되지 않도록 추가
2025-06-02 21:20:47 +09:00
5bdb101b52 fix: 크리에이터 관리자 콘텐츠 수정 - 태그 수정 기능
- 빈 칸인 경우 #으로 추가되는 버그 수정
2025-06-02 20:53:06 +09:00
97b2b38f8e fix: 크리에이터 관리자, 관리자 콘텐츠 리스트
- isActive = True 태그만 조회되도록 수정
2025-06-02 20:25:54 +09:00
2268f4a3fc fix: 크리에이터 관리자 콘텐츠 수정 - 태그 수정 기능
- 빈 칸인 경우 #으로 추가되는 버그 수정
2025-06-02 20:21:50 +09:00
9eff828249 feat: 크리에이터 관리자 콘텐츠 수정
- 태그 수정 기능 추가
2025-06-02 20:10:13 +09:00
b14438cc15 Merge pull request 'fix: 유저 행동 기록, 포인트 지급' (#321) from test into main
Reviewed-on: #321
2025-05-28 07:19:27 +00:00
3275ac5036 fix: 유저 행동 기록, 포인트 지급
- 행동 횟수 체크 순서를 조정하여 포인트 지급 누락 보완
2025-05-28 15:41:06 +09:00
b27d3bd5c6 Merge pull request 'fix: 유저 행동 기록, 포인트 지급' (#320) from test into main
Reviewed-on: #320
2025-05-26 10:33:16 +00:00
e049e0fa3c fix: 유저 행동 기록, 포인트 지급
- 포인트 지급 완료시 푸시 보내지 않도록 수정
2025-05-26 19:22:42 +09:00
03ebc9cfe9 Merge pull request 'fix: 큐레이션 아이템 조회' (#319) from test into main
Reviewed-on: #319
2025-05-23 05:43:37 +00:00
caee89cf53 fix: 큐레이션 아이템 조회
- 관리자에서 지정한 순서대로 보이도록 수정
2025-05-23 14:37:42 +09:00
24841b9850 Merge pull request 'fix: 코루틴 내 트랜잭션 간 조회 안 되는 문제 해결' (#318) from test into main
Reviewed-on: #318
2025-05-22 04:31:42 +00:00
e67b798714 fix: actionCount 를 조회할 때 endDate가 마지막 action 저장 이전의 시간이 측정될 수도 있어서 LocalDateTime.now()로 수정 2025-05-22 13:19:52 +09:00
dc13053825 fix: 구매하지 않은 콘텐츠에 댓글을 써도 ORDER_CONTENT_COMMENT 이벤트가 있으면 유저 행동 데이터에 기록되는 버그 수정 2025-05-22 13:01:39 +09:00
af352256e9 fix: 코루틴 내 트랜잭션 간 조회 안 되는 문제 해결
- 각 트랜잭션을 TransactionTemplate 블록으로 분리하여 커밋 시점 명확화
- 두 번째 트랜잭션에서 entityManager.clear() 호출로 1차 캐시 무시
- CoroutineExceptionHandler 추가로 비동기 예외 로깅 처리
- @PreDestroy 추가로 서비스 종료 시 CoroutineScope 정리
2025-05-22 12:25:17 +09:00
d35a3d1a8c Merge pull request 'test' (#317) from test into main
Reviewed-on: #317
2025-05-20 10:26:16 +00:00
b92810efd2 fix: 앱 실행시 처음 실행하는 유저 정보 조회 API
- point 추가
2025-05-20 17:56:51 +09:00
fcbd809691 fix: 유저 포인트 조회시 유효기간을 기준으로 오름차순 정렬 2025-05-20 16:56:34 +09:00
60c4e0b528 Merge pull request 'test' (#316) from test into main
Reviewed-on: #316
2025-05-20 06:03:10 +00:00
d3ec13e6c0 fix: 유저 행동 데이터에 따른 포인트 지급
- 본인인증을 한 유저만 포인트 정책에 따라 포인트를 지급하도록 수정
2025-05-20 00:51:04 +09:00
a36d9f02d8 fix: 포인트 내역 리스트
- 유저의 포인트 보상내역, 사용내역 id 내림차순 정렬
2025-05-20 00:14:57 +09:00
d6db862c9d fix: 포인트 내역 리스트
- 유저의 포인트 보상내역, 사용내역 API 추가
2025-05-19 21:38:24 +09:00
56542a7bf1 fix: 포인트 사용내역
- 포인트를 어디에 사용했는지 알기 위해 포인트 사용내역 저장시 orderId 추가
2025-05-19 20:49:16 +09:00
36b8e8169e fix: 유저 행동 데이터에 따른 포인트 지급
- 유저가 지급 받을 포인트가 0 이상인 경우에만 포인트 지급 로그를 남기고 푸시 발송
2025-05-19 16:27:58 +09:00
b102241efd fix: 유저 행동 데이터
- commentId -> contentCommentId 로 변경
2025-05-19 15:25:17 +09:00
f36010fefa fix: 유저 행동 데이터
- commentId -> contentCommentId 로 변경
2025-05-19 15:17:44 +09:00
aa23d6d50f fix: 주문한 콘텐츠에 댓글 작성 이벤트
- 포인트 받은 현황을 조회할 때 주문 ID를 같이 조회하도록 만들어서 주문한 콘텐츠에 댓글 작성 이벤트의 경우 주문별로 참여할 수 있도록 수정
2025-05-19 15:08:21 +09:00
6df043dfac fix: 콘텐츠 댓글 작성시 유저 행동 데이터에 댓글 ID를 같이 기록하도록 수정 2025-05-19 15:05:31 +09:00
fe84292483 fix: 포인트 지급 요소 계산시 정책 시작 날짜 이후의 유저 행동들만 반영하도록 수정 2025-05-19 14:43:50 +09:00
0f48c71837 fix: transactionTemplate 을 적용하여 횟수가 잘못 판단되는 경우 최소화 2025-05-19 11:43:24 +09:00
107e8fce55 fix: 유저의 행동 데이터 기록시 주문한 콘텐츠에 댓글을 쓰는 것을 판단하기 위해 주문 정보 조회시 id 내림차순으로 하여 가장 최근 주문정보를 가져오도록 수정 2025-05-19 10:49:16 +09:00
3079998a5d fix: 구매한 콘텐츠 댓글 이벤트 추가
- 구매한 콘텐츠 댓글 쓰기시 구매한 캔을 포인트로 지급 해야 되는데 설정한 포인트로 지급되는 버그 수정
2025-05-17 18:44:04 +09:00
e2d0ae558a feat: 구매한 콘텐츠 댓글 이벤트 추가
- 구매한 콘텐츠 댓글 쓰기시 구매한 캔을 포인트로 지급
2025-05-17 18:13:11 +09:00
1bca1b27ed feat: 구매한 콘텐츠 댓글 이벤트 추가 2025-05-17 18:07:02 +09:00
6fc372c898 feat: 유저 행동 데이터 기록 Controller 추가 2025-05-16 21:24:12 +09:00
ddcd54d3b9 feat: 유저 행동 데이터 기록 추가 - 콘텐츠에 댓글 쓰기 2025-05-16 20:32:48 +09:00
eb8c8c14e8 fix: 유저 행동 데이터 기록시 포인트 지급과 로그 기록 순서 변경
- 기존: 포인트 지급 후 로그 기록
- 변경: 로그 기록 후 포인트 지급
2025-05-16 17:57:37 +09:00
affc0cc235 fix: 관리자 - 포인트 정책 리스트 값 추가
- 지급유형(매일, 전체) 추가
- 참여가능 횟수 추가
2025-05-16 17:31:28 +09:00
f23251f5bb fix: 유저 행동 데이터 기록시 포인트 지급 조건 수정
- 지급유형(매일, 전체) 추가
- 참여가능 횟수 추가
- 주문한 콘텐츠에 댓글을 쓰면 포인트 지급을 위해 포인트 지급 이력에 orderId 추가
2025-05-16 15:01:33 +09:00
84f33d1bc2 Merge pull request 'fix: 소셜로그인시 유저 행동데이터 SIGN_UP 중복 기록 버그' (#315) from test into main
Reviewed-on: #315
2025-05-12 08:24:53 +00:00
73c9a90ae3 fix: 소셜로그인시 유저 행동데이터 SIGN_UP 중복 기록 버그
- 소셜로그인 시 isNew 플래그를 통해 회원가입/로그인을 구분하여 SIGN_UP 중복 기록 버그 수정
2025-05-12 17:19:34 +09:00
c4e1709b99 Merge pull request 'test' (#314) from test into main
Reviewed-on: #314
2025-05-12 02:12:47 +00:00
ced35af66d fix: 예약 취소 푸시 발송
- push 토큰 가져올 때 push token 테이블을 참조하지 않아 발생하는 버그 수정
2025-05-09 11:28:01 +09:00
b915ace6ff fix: 푸시메시지 발송 방식 변경
- iOS일 때는 notification, android 일 때는 data-only 방식으로 발송하던 현재 방식에서 모두 notification을 사용하는 방식으로 수정
2025-05-08 19:47:59 +09:00
e7a5fd5819 Merge pull request 'fix: 구글/카카오 로그인 회원가입 오류 수정' (#313) from test into main
Reviewed-on: #313
2025-05-02 10:58:04 +00:00
2fd7419bdd fix: 구글/카카오 로그인 회원가입 오류 수정
- 회원가입 전에 푸시 토큰 등록을 시도하여 에러나는 오류 수정
2025-05-02 19:38:46 +09:00
4bde03643c Merge pull request 'test' (#312) from test into main
Reviewed-on: #312
2025-04-29 02:56:16 +00:00
fd510710d9 feat: 푸시 토큰(카카오, 구글 로그인) - 한 사람이 여러개의 디바이스로 로그인 해도 모든 푸시 토큰이 기록되어 있어서 모든 디바이스에 푸시가 가도록 수정 2025-04-28 21:58:50 +09:00
8a924bd5be feat: 푸시 토큰 - 한 사람이 여러개의 디바이스로 로그인 해도 모든 푸시 토큰이 기록되어 있어서 모든 디바이스에 푸시가 가도록 수정 2025-04-28 21:40:20 +09:00
1bc52b56af Merge pull request 'fix: 콘텐츠 업로드 - 제목과 내용에서 trim 함수를 적용하여 앞/뒤 빈칸 제거' (#311) from test into main
Reviewed-on: #311
2025-04-25 09:43:31 +00:00
73edc0515f fix: 콘텐츠 업로드 - 제목과 내용에서 trim 함수를 적용하여 앞/뒤 빈칸 제거 2025-04-25 18:37:45 +09:00
9c33fd93f7 Merge pull request 'refactor: 본인인증 - 본인인증이 완료된 후 유저 행동 데이터를 기록하도록 수정' (#310) from test into main
Reviewed-on: #310
2025-04-24 11:10:17 +00:00
7870f8ea78 refactor: 본인인증 - 본인인증이 완료된 후 유저 행동 데이터를 기록하도록 수정 2025-04-24 20:04:49 +09:00
3c087bc275 Merge pull request '유저 행동 데이터, 포인트 추가' (#309) from test into main
Reviewed-on: #309
2025-04-24 02:44:57 +00:00
27c5b991cf fix: 오디션 지원 내역 - 탈퇴한 사람은 보이지 않도록 수정 2025-04-24 11:37:11 +09:00
8a937f01a4 feat: 콘텐츠 상세 - 포인트 사용 가능 여부 추가 2025-04-24 10:50:14 +09:00
3940282ed8 feat: 마이페이지 - 포인트 추가 2025-04-23 18:26:47 +09:00
ca704f38b9 fix: 포인트 정책 수정 - @Transactional 추가 2025-04-23 17:29:08 +09:00
6ff044e4ab fix: 포인트 정책 조회 - date가 null인 경우 빈칸으로 표시 2025-04-23 17:09:57 +09:00
fa98138541 fix: 포인트 정책 생성 - endDate가 빈칸이면 null 처리 2025-04-23 16:55:58 +09:00
cb7917dc26 fix: 포인트 정책 등록 - request에 활성화 여부 제거 2025-04-23 14:57:33 +09:00
58d066af0a feat: 유저 행동 데이터 - 본인인증 추가 2025-04-23 14:45:13 +09:00
e2daff6463 feat: 콘텐츠 정산 - 포인트를 사용한 주문과 사용하지 않은 주문 분리 2025-04-23 00:55:24 +09:00
7c3b7cffc2 fix: 콘텐츠 주문 - 포인트 결제 후 추가 결제를 해야하는 캔이 남아 있는 경우에만 캔을 결제하도록 수정 (남아 있는 캔이 없는데 결제 처리가 되서 0캔으로 데이터가 쌓이는 것 방지) 2025-04-22 23:39:48 +09:00
775391f590 fix: 포인트 정책 조회 Query 로직 수정 - where 조건에 불완전한 조건문이 들어있던 버그 수정 2025-04-22 22:42:07 +09:00
57adfec490 fix: 포인트 정책 조회 Query 로직 수정 - where 조건에 불완전한 조건문이 들어있던 버그 수정 2025-04-22 22:30:12 +09:00
24e62c1885 fix: 포인트 정책 조회 Query 로직 수정 - where 조건에 불완전한 조건문이 들어있던 버그 수정 2025-04-22 22:10:38 +09:00
a70b5d89ec fix: 관리자 포인트 정책 리스트 - 전체 개수 추가 2025-04-22 21:54:42 +09:00
761d56f4bd fix: 크리에이터 관리자 콘텐츠 수정 - 포인트 사용 가능 여부 추가 2025-04-22 21:19:47 +09:00
e759f62b5f fix: 크리에이터 관리자 콘텐츠 리스트 - 포인트 사용 가능 여부 추가 2025-04-22 21:07:16 +09:00
9e2d031b5d fix: 콘텐츠 업로드 - 포인트 사용 가능 여부 추가 2025-04-22 19:39:07 +09:00
b9cb8ad4a8 fix: 포인트 결제 조건 - 포인트 결제가 가능한 콘텐츠만 포인트 결제를 하도록 수정 2025-04-22 18:49:52 +09:00
c1d4c1ff1d feat: 기존 푸시 메시지 전송 로직에 최대 3회 재시도 처리 로직 추가 2025-04-22 17:44:19 +09:00
971683a81e feat: 포인트 지급 시 FCM data-only 푸시 메시지 전송 및 실패 시 재시도 처리 2025-04-22 17:35:47 +09:00
51dae0f02c feat: 포인트 사용 로직 구현 (만료일 순 + 10포인트 단위 차감) 2025-04-22 15:39:45 +09:00
e2c70de2e0 feat: 유저 행동 기록 및 포인트 지급 로직 구현 + 회원가입 연동 2025-04-21 22:03:58 +09:00
d94418067f 관리자 포인트 지급 정책 리스트, 생성, 수정 API 2025-04-21 19:08:31 +09:00
1cb2ee77b5 포인트 지급 정책
- Title 추가
2025-04-21 14:35:05 +09:00
336d3c9434 유저 행동데이터, 포인트
- Entity 생성
2025-04-21 14:22:10 +09:00
8ad13c289e Merge pull request '회원탈퇴' (#308) from test into main
Reviewed-on: #308
2025-04-15 10:42:37 +00:00
7649ce6e52 회원탈퇴
- 이메일 가입자만 비밀번호 체크
2025-04-15 19:28:28 +09:00
7577f48a09 Merge pull request '한정판 콘텐츠' (#307) from test into main
Reviewed-on: #307
2025-04-15 09:44:12 +00:00
5759a51017 한정판 콘텐츠
- 해당 콘텐츠 크리에이터인 경우 콘텐츠 구매자 리스트 추가
2025-04-11 21:39:39 +09:00
0251906964 Merge pull request '비밀번호 찾기' (#306) from test into main
Reviewed-on: #306
2025-04-10 06:28:57 +00:00
dd5c121f1f 비밀번호 찾기
- 이메일 로그인이 아닌 계정의 비밀번호를 찾으려고 하면 예외 발생
- 에러 메시지 : 해당 계정은 OO계정으로 가입되어 있습니다. 해당 소셜 로그인을 사용해 주세요.
2025-04-10 15:16:56 +09:00
2723a5f134 Merge pull request '일별 전체 회원 수' (#305) from test into main
Reviewed-on: #305
2025-04-10 02:30:00 +00:00
cae3a92a66 일별 전체 회원 수
- 이메일, 구글, 카카오 회원 수 추가
2025-04-10 11:12:43 +09:00
c3c60605fd Merge pull request '관리자 - 회원리스트, 크리에이터 리스트' (#304) from test into main
Reviewed-on: #304
2025-04-09 10:35:01 +00:00
562550880c 관리자 - 회원리스트, 크리에이터 리스트
- 로그인 타입 추가 (소셜로그인, 이메일 로그인)
2025-04-09 19:07:04 +09:00
238f704b22 Merge pull request '소셜 로그인, 회원가입 - 이메일 체크 로직 수정' (#303) from test into main
Reviewed-on: #303
2025-04-08 07:04:11 +00:00
a9c68f9971 소셜 로그인, 회원가입 - 이메일 체크 로직 수정
- 이미 가입된 계정인 경우 안내 문구 자세히 안내
2025-04-08 15:32:45 +09:00
5639d8ac8e Merge pull request 'test' (#302) from test into main
Reviewed-on: #302
2025-04-07 10:23:13 +00:00
d822a4a8ac 카카오 로그인 추가 2025-04-07 15:58:08 +09:00
e52c914000 관리자
- 새로운 시리즈(추천 시리즈) 보이는 순서를 orders 순서대로 보이도록 수정
2025-04-07 12:16:39 +09:00
a301f854ba 구글 로그인 - provider가 구글로 기록되도록 수정 2025-04-04 14:54:20 +09:00
602d9625e2 구글 로그인 - 인증없이 실행되도록 수정 2025-04-04 14:05:31 +09:00
5598bca8d3 구글 로그인 추가 2025-04-04 13:21:49 +09:00
1bbaf8f7b7 이벤트
- link를 빈칸으로 기록할 수 있도록 수정
2025-04-03 15:30:25 +09:00
3bb2753607 이벤트
- link를 빈칸으로 기록할 수 있도록 수정
2025-04-03 15:23:28 +09:00
08848c783d 이벤트
- link를 빈칸으로 기록할 수 있도록 수정
2025-04-03 12:29:46 +09:00
9aac591591 Merge pull request 'test' (#301) from test into main
Reviewed-on: #301
2025-04-01 13:31:24 +00:00
6e229af790 콘텐츠 상세
- 이전화/다음화 추가
2025-04-01 18:27:59 +09:00
ce8cc3eb29 콘텐츠 상세
- 이전화/다음화 추가
2025-04-01 17:36:32 +09:00
198ecddc89 콘텐츠 상세
- 이전화/다음화 추가
2025-04-01 16:21:32 +09:00
ffa8e5aebb Merge pull request '일별 전체 회원 수 통계' (#300) from test into main
Reviewed-on: #300
2025-03-31 03:50:18 +00:00
ae439b7e64 일별 전체 회원 수 통계
- 본인인증 수 추가
2025-03-31 12:37:32 +09:00
cbbfe014cc Merge pull request '광고 통계' (#299) from test into main
Reviewed-on: #299
2025-03-28 05:29:40 +00:00
3f1101ff73 광고 통계
- 광고를 터치하여 앱을 실행한 수 추가
2025-03-28 11:21:03 +09:00
83028f7817 Merge pull request 'test' (#298) from test into main
Reviewed-on: #298
2025-03-26 21:08:29 +00:00
5777d9700f 크리에이터, 콘텐츠, 시리즈 검색
- 콘텐츠, 시리즈 검색 결과에 크리에이터 닉네임 추가
2025-03-27 05:40:07 +09:00
e1e9f4588a 크리에이터, 콘텐츠, 시리즈 검색 2025-03-27 00:49:00 +09:00
be2f013b9a 마케팅 트래킹
- AppLaunch 트래킹에 빈 본문 추가
2025-03-26 16:51:00 +09:00
70d1795557 Merge pull request 'test' (#297) from test into main
Reviewed-on: #297
2025-03-26 04:23:28 +00:00
0b03ebeb70 마케팅 트래킹
- type에 @Enumerated(value = EnumType.STRING) 추가
2025-03-26 13:15:43 +09:00
c466ecb77c 마케팅 트래킹
- 복합키를 AUTO_INCREMENT의 단일키로 변경
- AppLaunch 트래킹 추가
2025-03-26 13:09:09 +09:00
8c6c681424 Merge pull request 'marketing 정보 업데이트 시 pid 값이 있으면 항상 로그인 기록 남기기' (#296) from test into main
Reviewed-on: #296
2025-03-25 11:25:46 +00:00
ba9c71a4ec marketing 정보 업데이트 시 pid 값이 있으면 항상 로그인 기록 남기기 2025-03-25 18:57:24 +09:00
50bc9f4ff3 Merge pull request '라이브 방 - 예약 중 조회' (#295) from test into main
Reviewed-on: #295
2025-03-24 10:04:08 +00:00
e33050a6d6 라이브 방 - 예약 중 조회
- 로그인 없이 조회시 예약완료로 표시되는 버그 수정
2025-03-24 18:43:33 +09:00
f00ea03fad Merge pull request 'test' (#294) from test into main
Reviewed-on: #294
2025-03-24 09:09:16 +00:00
3595c02e74 라이브 방
- 로그인 없이 조회 가능하도록 수정
2025-03-22 06:37:20 +09:00
3ff84074bd 라이브 방
- 로그인 없이 조회 가능하도록 수정
2025-03-22 06:26:17 +09:00
6dd6be183b 라이브 메인
- 로그인 없이 조회 가능하도록 수정
2025-03-22 06:10:28 +09:00
0764247447 오디션 메인
- 로그인 없이 조회 가능하도록 수정
2025-03-22 05:09:08 +09:00
f9f9b9aab9 FAQ
- 로그인 없이 조회가 가능하도록 수정
2025-03-22 04:39:54 +09:00
ec0252bae0 콘텐츠 메인 홈
- 로그인 없이 인기 단편 조회가 가능하도록 수정
2025-03-22 03:16:54 +09:00
dc74d203bd 콘텐츠 메인 홈
- 로그인 없이 인기 단편 조회가 가능하도록 수정
2025-03-22 02:42:44 +09:00
387d364861 콘텐츠 메인 홈
- 로그인 없이 조회가 가능하도록 수정
2025-03-22 01:50:00 +09:00
82afdecf6c 콘텐츠 메인 홈
- 로그인 없이 조회가 가능하도록 수정
2025-03-22 01:38:32 +09:00
519c63a023 콘텐츠 메인 홈
- 로그인 없이 조회가 가능하도록 수정
2025-03-22 00:52:34 +09:00
f22e7b9ad1 Merge pull request '자동생성 닉네임에 사용될 형용사, 명사 값 추가' (#293) from test into main
Reviewed-on: #293
2025-03-21 10:27:30 +00:00
d45a25258e 자동생성 닉네임에 사용될 형용사, 명사 값 추가 2025-03-21 18:43:49 +09:00
c7ec95f4bb Merge pull request 'test' (#292) from test into main
Reviewed-on: #292
2025-03-20 19:24:03 +00:00
bc822355df 회원탈퇴 시 닉네임 앞에 "deleted_"를 추가 2025-03-21 04:15:53 +09:00
9535ff18de 닉네임 자동생성
- 닉네임을 더 유니크하게 생성할 수 있도록 형용사와 명사 추가
2025-03-21 04:11:35 +09:00
da0a83bb6d 닉네임 자동생성
- '의'가 들어간 단어 제거
2025-03-21 02:50:27 +09:00
4977ee99df 회원가입 로직 개선
- 기본 프로필 이미지와 닉네임 자동생성을 통해 회원가입 단계 축소
2025-03-21 00:24:15 +09:00
229e7a8ccc Merge pull request '시리즈 상세, 채널 상세' (#291) from test into main
Reviewed-on: #291
2025-03-19 09:43:06 +00:00
9ed031e574 시리즈 상세, 채널 상세
- 19금 콘텐츠 보기 설정 적용
2025-03-19 18:34:20 +09:00
3c616474ff Merge pull request 'test' (#290) from test into main
Reviewed-on: #290
2025-03-19 07:51:25 +00:00
b1fb62dd65 콘텐츠 메인 홈 - 인기 시리즈, 인기 단편
콘텐츠 메인 단편 - 랭킹
- 기존 조건에 계산은 최대 5번까지만 하도록 수정
2025-03-19 16:45:31 +09:00
b7b166c362 콘텐츠 메인 홈 - 인기 시리즈
- 데이터가 5개 미만이면 5개 이상이 될 때까지 랭킹 계산 시작 날짜를 1주일 씩 이전으로 설정
2025-03-19 16:27:55 +09:00
46321dd3c1 콘텐츠 메인 홈 - 인기 단편
- 데이터가 5개 미만이면 5개 이상이 될 때까지 랭킹 계산 시작 날짜를 1주일 씩 이전으로 설정
2025-03-19 16:23:44 +09:00
1998a95c35 콘텐츠 메인 단편 - 일간랭킹
- 데이터가 5개 미만이면 5개 이상이 될 때까지 랭킹 계산 시작 날짜를 5일씩 이전으로 설정
2025-03-19 16:15:27 +09:00
13a1fa674b 콘텐츠 메인 홈 - 인기 단편
- 19금 콘텐츠 보기 설정 적용
2025-03-19 14:26:03 +09:00
e488f3419e 콘텐츠 메인 홈 - 채널별 인기 콘텐츠 채널
- 19금 콘텐츠 안보기 설정시 일반 콘텐츠 판매량으로 채널 조회
2025-03-19 12:23:54 +09:00
56eb6b3ce3 Merge pull request '19금 콘텐츠 보기 설정 적용' (#289) from test into main
Reviewed-on: #289
2025-03-19 02:05:17 +00:00
dc1c29b69d 콘텐츠 메인 홈, 모닝콜, asmr, 단편, 무료, 다시듣기, 시리즈
- 남성향, 여성향 선택한 유저의 경우 해당 성향의 콘텐츠 + 미인증 크리에이터의 콘텐츠를 보여주도록 수정
2025-03-18 17:27:11 +09:00
c7eae53b22 콘텐츠 메인 홈, 모닝콜, asmr, 단편, 무료, 다시듣기, 시리즈
- 남성향, 여성향 선택한 유저의 경우 해당 성향의 콘텐츠 + 미인증 크리에이터의 콘텐츠를 보여주도록 수정
2025-03-18 17:10:19 +09:00
b3b3d46696 콘텐츠 메인 홈, 모닝콜, asmr, 단편, 무료, 다시듣기, 시리즈
- 19금 콘텐츠 (안)보기 설정
- 남성향, 여성향 콘텐츠만 보기 설정 적용
2025-03-18 16:07:10 +09:00
545836d43c Merge pull request '관리자 광고통계, 일별 전체 회원 수' (#288) from test into main
Reviewed-on: #288
2025-03-17 08:50:59 +00:00
537ec88d05 관리자 광고통계, 일별 전체 회원 수
- 1페이지 이외에 데이터가 보이지 않는 버그 수정
2025-03-17 17:44:14 +09:00
219f83dec0 Merge pull request 'test' (#287) from test into main
Reviewed-on: #287
2025-03-17 05:54:05 +00:00
d54f05fa00 관리자 광고통계, 일별 전체 회원 수
- 날짜 내림차순으로 정렬
2025-03-17 14:47:05 +09:00
5708f4f059 관리자 광고통계, 일별 전체 회원 수
- 날짜 내림차순으로 정렬
2025-03-17 14:39:01 +09:00
353807404a 관리자 광고통계
- 날짜 내림차순으로 정렬
2025-03-17 14:31:42 +09:00
a76a841238 Merge pull request 'test' (#286) from test into main
Reviewed-on: #286
2025-03-14 16:11:17 +00:00
81fa445964 관리자 - 일별 전체 회원수 API
- 결제자 수 중복을 제거하고 카운팅하도록 수정
2025-03-15 01:03:16 +09:00
f65ddbc5b8 관리자 - 일별 전체 회원수 API
- 결제자 수 중복을 제거하고 카운팅하도록 수정
2025-03-15 00:54:17 +09:00
b817a230fd 관리자 - 일별 전체 회원수 API
- 결제자 수 중복을 제거하고 카운팅하도록 수정
2025-03-15 00:46:26 +09:00
3a180d478c 관리자 - 일별 전체 회원수 API
- 합계 날짜 범위를 전체 날짜 범위로 수정
2025-03-15 00:09:35 +09:00
74fecddf95 관리자 - 일별 전체 회원수 API
- 페이지 계산 수정
2025-03-14 23:55:04 +09:00
1dec8913c5 관리자 - 일별 전체 회원수 API
- 일별 회원가입, 회원탈퇴, 결제자 수를 반환하는 API 생성
2025-03-14 21:48:52 +09:00
c26680de84 Merge pull request '이벤트 배너, 충전 이벤트 - 기간 설정에 시간 추가' (#285) from test into main
Reviewed-on: #285
2025-03-14 03:40:07 +00:00
b9063fb22f 관리자 - 충전이벤트
- 시간 계산을 Querydsl 코드에서 수행
- 등록/수정 시 이벤트 진행기간에 시간도 포함하도록 수정
2025-03-14 02:44:34 +09:00
287d133080 관리자 - 이벤트 배너
- 이벤트 기간 설정을 시간:분 까지 설정하도록 수정
2025-03-14 02:13:42 +09:00
3ef1a732e5 관리자 - 이벤트 배너 서비스
- 시작 전인 이벤트도 보이도록 수정
2025-03-14 01:55:08 +09:00
7cd95da83c 관리자 - 광고 통계
- 패키지 이동 (marketing/statistics -> statistics/ad)
2025-03-14 01:49:10 +09:00
dd138bff86 관리자 - 이벤트 배너 서비스
- 이미지 host를 Querydsl 코드에서 추가
- 시작 전인 이벤트도 보이도록 수정
2025-03-14 01:43:43 +09:00
8fffad9d3a Merge pull request 'test' (#284) from test into main
Reviewed-on: #284
2025-03-13 12:25:35 +00:00
327b0149d9 콘텐츠 홈 단편 탭
- 유료 콘텐츠만 나오도록 수정
2025-03-13 21:16:42 +09:00
b822cf47bb 광고 통계
- 전체 개수 계산시 NonUniqueResultException 버그 수정
2025-03-13 19:51:58 +09:00
f4f0f203a2 Merge pull request '유저 정보 조회' (#283) from test into main
Reviewed-on: #283
2025-03-12 08:00:13 +00:00
30e1e461e3 유저 정보 조회
- 성별, 가입일, 충전횟수 추가
2025-03-12 02:51:42 +09:00
b7196f5a0c Merge pull request 'test' (#282) from test into main
Reviewed-on: #282
2025-03-11 08:01:05 +00:00
3e25accaa3 관리자 마케팅 - 광고 통계
- 날짜별 검색 추가
2025-03-11 16:38:58 +09:00
5b3c5731ee 관리자 마케팅 - 광고 통계
- LOGIN 기록 추가
2025-03-11 16:31:31 +09:00
84de4e0c5a 마케팅 - 매체 파트너 코드 기록
- 마케팅 PID 가 변경될 때 LOGIN 기록
2025-03-11 15:27:26 +09:00
5d33a18890 Merge pull request 'test' (#281) from test into main
Reviewed-on: #281
2025-03-10 05:35:30 +00:00
48677a5a24 마케팅 - 매체 파트너 코드 정렬 수정
- id 오름차순에서 내림차순으로 변경
2025-03-10 13:50:16 +09:00
b0349ac133 마케팅 - 광고 통계
- 전체 개수를 size로 구하지 않고 count 함수를 이용하도록 수정
2025-03-09 17:38:04 +09:00
96186a1a50 Merge pull request '마케팅 - 매체 파트너 코드 조회 API - link 값 수정' (#280) from test into main
Reviewed-on: #280
2025-03-07 06:27:08 +00:00
925c5203be 마케팅 - 매체 파트너 코드 조회 API - link 값 수정
- 쿼리파라미터에 af_dp 추가
2025-03-07 15:17:52 +09:00
bc8bc479d1 Merge pull request 'test' (#279) from test into main
Reviewed-on: #279
2025-03-06 17:58:32 +00:00
89a8a145df 마케팅 - 매체 파트너 코드 조회 API - link 값 수정
- 쿼리파라미터의 키와 값을 각각 인코딩 적용
2025-03-07 02:47:34 +09:00
83a938dc53 마케팅 - 매체 파트너 코드 조회 API - link 값 수정
- 쿼리파라미터의 키와 값을 각각 인코딩 적용
2025-03-07 02:31:40 +09:00
47595b1291 Merge pull request 'test' (#278) from test into main
Reviewed-on: #278
2025-03-05 14:05:47 +00:00
75940bbb23 마케팅 - 매체 파트너 코드 조회 API - link 값 수정
- utm_campaign 값을 pidName에서 pid 로 변경
2025-03-05 22:47:29 +09:00
37516a0072 마케팅
- 광고 통계 조회 API 추가
2025-03-05 21:49:33 +09:00
01a88964df Merge pull request 'test' (#277) from test into main
Reviewed-on: #277
2025-03-05 09:44:59 +00:00
0f68b297a0 마케팅 매체 파트너 코드 조회
- 링크 인코딩 제거
2025-03-05 17:16:05 +09:00
d454acdcbe 마케팅 매체 파트너 코드 조회
- 전체 개수 추가
2025-03-05 16:10:11 +09:00
2ce13afc0a 마케팅 매체 파트너 코드 조회 - link 인코딩 수정
- host 는 인코딩하지 않고 쿼리 파라미터 부분만 인코딩 하도록 수정
2025-03-05 15:53:25 +09:00
2dd75ae7e8 marketing info 업데이트 API
- pid가 빈칸이면 등록하지 않도록 수정
2025-03-05 00:49:50 +09:00
a17a6a41da 관리자
- 매체 파트너 코드(pid) 조회 API
2025-03-05 00:39:49 +09:00
5db181aa74 관리자
- 매체 파트너 코드(pid) 수정 API
2025-03-04 23:53:22 +09:00
d74f1ddb81 관리자
- 매체 파트너 코드(pid) 등록 API
2025-03-04 23:36:31 +09:00
be12148d04 트래킹
- mediaGroup과 pidName 추가
2025-03-04 17:33:01 +09:00
72d10f9443 캔 충전
- 트래킹 로직 적용
2025-03-04 17:05:41 +09:00
81b11976a7 DateTime -> Datetime 2025-03-04 13:40:23 +09:00
f918e89307 DateTime -> Datetime 2025-03-04 13:34:56 +09:00
83ed4b6961 marketing info 업데이트 API 생성 2025-03-04 12:28:30 +09:00
3216c73ee8 회원가입 로직에 광고 트래킹 적용
- 광고 트래킹 관련 Entity 추가
- pid가 현재 광고 중인 pid인 경우 트래킹 로그 생성
2025-03-04 10:52:35 +09:00
3a2b77379f Merge pull request '콘텐츠 업로드' (#276) from test into main
Reviewed-on: #276
2025-02-28 04:45:04 +00:00
801b9934d6 콘텐츠 업로드
- 알람, 모닝콜, 슬립콜은 소장만 가능하도록 수정
2025-02-28 13:30:24 +09:00
dc4e5f75cd Merge pull request '콘텐츠 메인 콘텐츠 탭 - 채널별 추천 단편' (#275) from test into main
Reviewed-on: #275
2025-02-26 03:14:33 +00:00
7a745c2f4b 콘텐츠 메인 콘텐츠 탭 - 채널별 추천 단편
- 좋아요 개수를 기준으로 내림차순 정렬하도록 수정
- 유료 콘텐으만 나오도록 수정
2025-02-26 12:01:39 +09:00
d0178d551c Merge pull request '콘텐츠 메인 콘텐츠 탭 - 채널별 추천 단편' (#274) from test into main
Reviewed-on: #274
2025-02-25 14:54:53 +00:00
4b1c2e36ed 콘텐츠 메인 콘텐츠 탭 - 채널별 추천 단편
- 좋아요 개수를 기준으로 내림차순 정렬하도록 수정
2025-02-25 23:42:23 +09:00
827333108d Merge pull request '콘텐츠 대여기간' (#273) from test into main
Reviewed-on: #273
2025-02-25 14:02:18 +00:00
2e05b25c41 콘텐츠 메인
- 테마에 오디오북 추가
2025-02-25 22:52:33 +09:00
d4318cc48c 콘텐츠 대여기간
- 15 -> 5일로 변경
2025-02-24 12:17:07 +09:00
587b90bd27 Merge pull request '콘텐츠 메인 무료 탭 - 새로운 콘텐츠' (#272) from test into main
Reviewed-on: #272
2025-02-22 01:56:49 +00:00
780088eb0c 콘텐츠 메인 무료 탭 - 새로운 콘텐츠
- 상단 탭에 있는 테마 제거
2025-02-22 10:52:30 +09:00
4dc20c5e90 Merge pull request '콘텐츠 메인 무료 탭' (#271) from test into main
Reviewed-on: #271
2025-02-22 00:39:09 +00:00
2a3d7c9291 콘텐츠 메인 무료 탭
- 큐레이션에서 '크리에이터 소개'가 제목에 들어가면 제외하도록 수정
2025-02-22 09:33:19 +09:00
ac25782f2b Merge pull request '관리자 태그 큐레이션 - 콘텐츠 검색' (#270) from test into main
Reviewed-on: #270
2025-02-21 21:46:15 +00:00
8ac9695535 관리자 태그 큐레이션 - 콘텐츠 검색
- 이전에 추가했던 기록이 있으면 검색되지 않던 버그 수정
2025-02-22 06:26:39 +09:00
20437d56e7 Merge pull request '메인 시리즈 탭 - 완결 시리즈' (#269) from test into main
Reviewed-on: #269
2025-02-21 21:15:52 +00:00
5933a74885 메인 시리즈 탭 - 완결 시리즈
- 1~10일, 11~20일, 21~해당 월의 마지막날 로 3등분 하여 순위를 계산하고 10일씩 반영되도록 수정
ex)오늘이 23일 이면 11~20일 사이에 팔린 개수를 기준으로 순위 계산
2025-02-22 05:56:56 +09:00
f0b412828a Merge pull request '메인 시리즈 탭 - 완결 시리즈' (#268) from test into main
Reviewed-on: #268
2025-02-21 19:27:33 +00:00
afb64eb8f2 메인 시리즈 탭 - 완결 시리즈
- 1~10일, 11~20일, 21~해당 월의 마지막날 로 3등분 하여 순위를 계산하고 10일씩 반영되도록 수정
ex)오늘이 23일 이면 11~20일 사이에 팔린 개수를 기준으로 순위 계산
2025-02-22 04:09:03 +09:00
367faac5c3 Merge pull request 'test' (#267) from test into main
Reviewed-on: #267
2025-02-20 18:24:35 +00:00
3c90f065fb 관리자 콘텐츠 메인 시리즈 탭 - 큐레이션
- 시리즈 검색시 큐레이션에 추가되었다가 삭제된 시리즈도 검색 되도록 수정
2025-02-21 03:18:41 +09:00
8b731999a7 콘텐츠 메인 시리즈 탭 - 큐레이션
- 시리즈 정렬 추가
- 내용이 추가되지 않은 큐레이션은 보이지 않도록 수정
2025-02-21 03:05:08 +09:00
5182d03b16 콘텐츠 메인 시리즈 탭 - 큐레이션
- 모든 큐레이션 데이터가 시리즈 큐레이션에 중복되어 나오는 버그 수정
2025-02-21 02:56:54 +09:00
84deaaa970 Merge pull request '콘텐츠 메인 시리즈 탭 - 장르별 시리즈' (#266) from test into main
Reviewed-on: #266
2025-02-19 12:52:17 +00:00
433a9a29c5 콘텐츠 메인 시리즈 탭 - 장르별 시리즈
- 콘텐츠가 있는 장르만 표시하도록 수정
2025-02-19 21:46:02 +09:00
a2b39466c2 Merge pull request '기존 콘텐츠 메인 - 새로운 콘텐츠' (#265) from test into main
Reviewed-on: #265
2025-02-19 11:34:02 +00:00
3388bb4283 기존 콘텐츠 메인 - 새로운 콘텐츠
- 전체 테마를 선택시 데이터가 나오지 않는 버그 수정
2025-02-19 19:12:11 +09:00
03586c4005 Merge pull request '기존 콘텐츠 메인 - 새로운 콘텐츠' (#264) from test into main
Reviewed-on: #264
2025-02-19 09:49:04 +00:00
8ae225f434 기존 콘텐츠 메인 - 새로운 콘텐츠
- 전체 테마를 선택시 데이터가 나오지 않는 버그 수정
2025-02-19 18:43:48 +09:00
6ea69e1510 Merge pull request '콘텐츠 메인 무료 탭 - 새로운 무료 콘텐츠' (#263) from test into main
Reviewed-on: #263
2025-02-19 09:24:24 +00:00
c8c1087b73 콘텐츠 메인 무료 탭 - 새로운 무료 콘텐츠
- 전체를 터치하면 테마가 빈칸으로 들어가는 버그 수정
2025-02-19 18:13:26 +09:00
553c6dc539 Merge pull request '콘텐츠 메인 단편 탭 - 새로운 단편' (#262) from test into main
Reviewed-on: #262
2025-02-19 08:20:14 +00:00
feae2f5f98 콘텐츠 메인 무료 탭 - 새로운 무료 콘텐츠 테마
- 데이터가 없는 탭은 나오지 않도록 수정
2025-02-19 17:05:42 +09:00
2a96467d9c 콘텐츠 메인 단편 탭 - 새로운 단편
- 탭으로 나눠져 있는 테마는 보이지 않도록 수정
2025-02-19 16:53:24 +09:00
6cc22f5b6d Merge pull request '콘텐츠 메인 홈, 무료 탭' (#261) from test into main
Reviewed-on: #261
2025-02-19 06:34:53 +00:00
03bd915fa5 콘텐츠 메인 홈, 무료 탭
- 콘텐츠 랭킹에서 잘못 작성된 sql 수정
2025-02-19 14:47:59 +09:00
9103d67cc1 Merge pull request 'test' (#260) from test into main
Reviewed-on: #260
2025-02-18 18:13:25 +00:00
872ec7f13f 콘텐츠 메인 홈 탭 - 채널별 인기 콘텐츠
- 크리에이터 정렬 - 판매수 내림차순
2025-02-19 03:08:48 +09:00
7041aff350 콘텐츠 메인 시리즈 탭 - 완결 시리즈
- 완결시리즈 전체 보여주기
- 정렬 - 월별 판매량 순
2025-02-19 02:20:10 +09:00
000fb7c941 콘텐츠 메인 다시듣기 탭
- 인기 순위 제거
2025-02-19 01:45:45 +09:00
e0d978621b 콘텐츠 메인 ASMR 탭
- 인기 순위 제거
2025-02-19 01:43:55 +09:00
c29627bb64 콘텐츠 메인 단편 탭 - 일간 랭킹
- sortType 추가
2025-02-19 01:33:56 +09:00
839cbdeaec 콘텐츠 메인 단편 탭 - 테마
- 오디오북, 모닝콜, 알람, 슬립콜, 다시듣기, ASMR, 릴레이, 챌린지, 자기소개 제거
2025-02-19 01:10:00 +09:00
25083fb0e4 Merge pull request 'test' (#259) from test into main
Reviewed-on: #259
2025-02-18 14:48:09 +00:00
44e3eda145 콘텐츠 메인 단편 탭 - 태그별 추천 단편 태그
- 콘텐츠가 하나 이상 등록되어 있는 태그만 조회되도록 수정
2025-02-18 21:33:30 +09:00
00c705085e 콘텐츠 메인 단편 탭
- 태그별 추천 단편 API 추가
2025-02-18 19:19:52 +09:00
7b957c6732 레거시 콘텐츠 메인 - 상단 콘텐츠 배너
- 탭이 지정되지 않은 콘텐츠 배너만 보이도록 수정
2025-02-18 17:11:32 +09:00
03e1ef3271 관리자 태그 큐레이션 등록, 수정
- 태그가 #으로 시작하지 않으면 #추가
2025-02-18 17:05:28 +09:00
4370fef5d5 관리자 태그 큐레이션 등록
- 동일한 태그가 등록되지 않도록 validation 추가
2025-02-18 16:34:02 +09:00
93b0565368 관리자 태그 큐레이션 등록
- 동일한 태그가 등록되지 않도록 validation 추가
2025-02-18 16:19:59 +09:00
e0565f7eed 관리자 태그별 추천 단편
- 등록, 수정, 삭제, 순서변경 API 추가
2025-02-18 15:27:39 +09:00
43ea4191c3 콘텐츠 메인 무료 탭
- 콘텐츠 테마와 유저 테이블 데이터를 사용하므로 해당 테이블 조인 코드 추가
2025-02-18 01:59:18 +09:00
308127d044 콘텐츠 메인 무료 탭
- 채널별 추천 무료 콘텐츠 API 추가
2025-02-18 01:49:52 +09:00
ab2f581c9f 콘텐츠 메인 무료 탭
- 채널별 추천 무료 콘텐츠 API 추가
2025-02-18 01:43:37 +09:00
51c4044e2f 콘텐츠 메인 단편 탭
- 큐레이션 추가
2025-02-17 23:34:58 +09:00
fbfb951825 관리자 콘텐츠 메인 큐레이션 아이템
- 순서 변경 기능 추가
2025-02-17 22:50:22 +09:00
b529d49e78 콘텐츠 메인 알람 탭 - 새로운 알람 전체보기
- 전체 개수 추가
2025-02-17 20:22:57 +09:00
3344757af8 콘텐츠 메인 무료 탭
- 새로운 무료 콘텐츠 전체 조회가 먼저 되도록 수정
2025-02-17 12:30:17 +09:00
5521f39cc5 콘텐츠 메인 - 채널별 ** 콘텐츠
- 유료 콘텐츠만 개수에 포함
2025-02-17 12:22:34 +09:00
f9c34d14c3 콘텐츠 메인 - 채널별 ** 콘텐츠
- 매출 순위 제거
2025-02-17 12:15:02 +09:00
dc0902c555 콘텐츠 메인 - 채널별 인기 콘텐츠
- 판매개수 순위 Top2 -> Top4로 변경
2025-02-17 12:07:21 +09:00
239516b98b 콘텐츠 메인 - 홈, 단편 - 채널별 인기 콘텐츠
- 보이는 채널 조건 아래와 같이 변경
- 유료 콘텐츠 4개 이상 등록한 채널의 주간 콘텐츠 판매 개수 Top 20
2025-02-17 11:54:08 +09:00
d2dc045255 Merge pull request 'test' (#258) from test into main
Reviewed-on: #258
2025-02-14 18:09:11 +00:00
3d1716d847 콘텐츠 메인 다시듣기, ASMR
- 채널별 콘텐츠 조회 API 추가
2025-02-15 02:01:59 +09:00
34452525d4 모닝콜 새로운 콘텐츠 전체보기
- API 추가
2025-02-14 22:00:13 +09:00
713d42a674 새로운 콘텐츠 전체보기
- 무료 콘텐츠 전체보기를 위해 isFree 파라미터 추가
2025-02-14 15:55:21 +09:00
e1bfd944e9 콘텐츠 메인 무료 탭
- 새로운 콘텐츠 테마 선택 액션 API 추가
2025-02-14 15:44:40 +09:00
a6f8f6a4d4 콘텐츠 메인 무료 탭
- 크리에이터 소개 전체보기 API 추가
2025-02-14 15:08:09 +09:00
cf538a2c36 콘텐츠 메인 탭
- 큐레이션 조회 로직 수정
2025-02-14 14:39:22 +09:00
3caaa151f4 콘텐츠 메인 시리즈 탭
- 채널별 추천 시리즈 API 추가
2025-02-14 14:09:21 +09:00
60ce64d3e1 콘텐츠 메인 시리즈 탭 - 오리지널 콘텐츠 API
- id 내림차순 정렬
2025-02-14 04:57:41 +09:00
9c9aa33687 콘텐츠 메인 시리즈 탭
- 장르별 추천 시리즈 API
2025-02-14 04:50:13 +09:00
a8589ef4e7 콘텐츠 메인 시리즈 탭
- 완결 시리즈 전체보기 API
2025-02-14 04:31:05 +09:00
10dd50b332 콘텐츠 메인 시리즈 탭
- 오리지널 드라마 전체보기 API
2025-02-14 03:30:50 +09:00
bebfda0343 콘텐츠 메인 시리즈 탭
- 오리지널 드라마 전체보기 API
2025-02-14 03:20:18 +09:00
babfb27b1f 콘텐츠 메인 무료 탭
- 큐레이션에서 '크리에이터 소개' 제거
2025-02-14 01:32:57 +09:00
258bd0796d 콘텐츠 메인 단편 탭
- 채널별 추천 단편 API 추가
2025-02-13 14:56:51 +09:00
2f0182e06c 콘텐츠 메인 단편 탭
- 콘텐츠 일간 랭킹 API 추가
2025-02-13 14:37:44 +09:00
ecddf9975f 콘텐츠 메인 단편 탭
- 테마별 콘텐츠 API 추가
2025-02-13 14:15:34 +09:00
664677a005 콘텐츠 메인 단편 탭 - 새로운 단편 첫 조회시 테마 없이 전체조회 2025-02-13 14:01:09 +09:00
ca9e7da17e 콘텐츠 메인 시리즈 탭 - 채널별 추천 시리즈 변수명 수정
- salesRankContentList -> recommendSeriesByChannel
2025-02-13 02:08:09 +09:00
39eb3d48a8 콘텐츠 메인 시리즈 탭 - 새로운 시리즈
- 크리에이터 프로필 이미지 추가
2025-02-13 01:20:18 +09:00
a6e949bdd6 콘텐츠 메인 시리즈 탭 - 장르별 추천 시리즈 정렬 수정
- 판매 캔 -> 판매 개수
2025-02-12 23:48:41 +09:00
63f952d390 콘텐츠 메인 시리즈 탭
- 차단 당한 유저는 오리지널 오디오 드라마가 조회되지 않도록 수정
2025-02-12 19:54:11 +09:00
7e9cb556d0 시리즈 리스트
- 콘텐츠가 1개 이상 등록된 시리즈만 조회
2025-02-12 19:27:52 +09:00
dce1abaeff 콘텐츠 메인 홈
- 채널별 인기 콘텐츠 API 추가
2025-02-10 02:39:56 +09:00
c4602369ae 콘텐츠 랭킹 아이템
- 크리에이터 프로필 이미지 추가
2025-02-10 02:30:30 +09:00
b7610641e5 채널별 인기 콘텐츠
- audioTheme join을 추가하여 에러 제거
2025-02-09 22:58:09 +09:00
8fb1247279 공지사항
- QueryProjection 사용
- QueryDSL을 통해 DTO로 바로 조회
2025-02-09 22:54:45 +09:00
b8621dfbb0 Merge pull request 'test' (#257) from test into main
Reviewed-on: #257
2025-02-09 13:36:21 +00:00
04f2ac6815 콘텐츠 메인 무료 탭 API
= url -> /free로 변경
2025-02-08 02:46:50 +09:00
9be78062be 콘텐츠 메인
- 무료 탭 API
2025-02-08 02:40:25 +09:00
3410045f83 콘텐츠 메인
- 다시보기 탭 API
2025-02-08 00:52:39 +09:00
14b0eeec7e 콘텐츠 메인
- ASMR 탭 API
2025-02-08 00:01:14 +09:00
d1579126f3 콘텐츠 메인
- 모닝콜 탭 API
2025-02-07 23:45:44 +09:00
c5539bc7e3 콘텐츠 메인
- 단편 탭 API
2025-02-07 18:32:44 +09:00
0f8fcbcaed 콘텐츠 메인
- 시리즈 탭 API
2025-02-07 03:01:24 +09:00
b1f82f9abe 콘텐츠 메인 - 홈 탭 API
- 주간 랭킹 기간 수정
2025-02-06 21:30:16 +09:00
27deff3ff3 콘텐츠 메인 - 홈 탭 API
- 채널별 인기 콘텐츠의 크리에이터가 없는 경우 콘텐츠를 불러오지 않도록 수정
2025-02-06 21:09:11 +09:00
04eb416a73 콘텐츠 메인
- 홈 탭 API
2025-02-06 19:06:27 +09:00
05e714fff1 관리자
- 새로운 시리즈, 무료 추천 시리즈 등록/수정/순서변경 API 추가
2025-02-04 23:25:52 +09:00
55badb6206 오디션 응원 하루 최대 응원 수 수정
- 10회 -> 100회
2025-02-04 00:39:41 +09:00
1b782f3df8 본인인증 - gender 값 리턴 2025-02-03 20:49:29 +09:00
bbf3fc04b6 본인인증 - gender 값 리턴 2025-02-03 19:08:22 +09:00
7657f779b5 본인인증 - 19세 미만 본인인증 불가 메시지 년도 수정
- 2005 -> 올해년도 - 19로 변경
2025-02-03 18:57:28 +09:00
93633940dd Merge pull request 'test' (#256) from test into main
Reviewed-on: #256
2025-02-03 07:20:32 +00:00
3e4bfef14e 알림설정
- 처음에 데이터 생성시 null이 들어오면 false로 처리
2025-02-03 16:15:27 +09:00
09df1eb896 관리자 큐레이션 아이템 - 삭제
- Post -> Put 으로 변경
2025-02-03 15:23:06 +09:00
23b3e6cdce 관리자 큐레이션 아이템
- 시리즈, 콘텐츠 큐레이션 아이템 불러오기 로직 분리
2025-02-03 11:26:02 +09:00
b6f5325351 Merge pull request 'test' (#255) from test into main
Reviewed-on: #255
2025-01-31 15:22:23 +00:00
32a71664a4 라이브 정산
- 캔을 사용한 날짜를 기준으로 계산 하도록 수정
2025-02-01 00:08:37 +09:00
ce881506f9 관리자 큐레이션 아이템 추가
- 콘텐츠 조회 sql에 from 추가
2025-01-31 23:12:23 +09:00
96b832983a 관리자 큐레이션 아이템 추가/제거
- return 추가
2025-01-31 23:06:03 +09:00
8f2ec7f4dd 관리자 큐레이션 아이템 추가 Request
- contentIdList -> itemIdList 변경
2025-01-31 22:50:27 +09:00
705459ee90 관리자 큐레이션 아이템
- 조회, 추가, 삭제, 콘텐츠 검색, 시리즈 검색 API 추가
2025-01-31 21:58:31 +09:00
155ea5c5e4 관리자 큐레이션 조회
- 시리즈 큐레이션 여부 추가
2025-01-24 12:21:33 +09:00
8904ef2247 앱 큐레이션 조회
- tab이 지정 안된 큐레이션만 조회되도록 수정
2025-01-23 22:46:06 +09:00
de07b3d7de 앱 큐레이션 조회
- tab이 지정 안된 큐레이션만 조회되도록 수정
2025-01-23 22:38:52 +09:00
faf827de71 관리자 큐레이션 조회
- tabId를 추가해서 탭별로 조회할 수 있도록 수정
2025-01-23 22:18:00 +09:00
d95f95899c 시리즈
- 오리지널 여부 추가
2025-01-23 19:46:54 +09:00
e9e538168c 관리자 큐레이션 등록/수정
- 탭과 시리즈 여부 추가
2025-01-23 19:33:17 +09:00
49a5e47f9d 관리자 콘텐츠 배너 API
- 조회, 등록, 수정시 탭 설정
2025-01-21 18:54:06 +09:00
8285589b10 관리자 콘텐츠 배너 API
- 탭별 배너 조회
- 탭별 배너 등록
2025-01-21 17:03:42 +09:00
00ce6d6a7a 관리자 콘텐츠 메인 탭 조회 API 2025-01-21 16:19:36 +09:00
7c32c08f1f Merge pull request 'test' (#254) from test into main
Reviewed-on: #254
2025-01-17 05:46:00 +00:00
d36aada227 관리자 시리즈 검색 API - 내용도 검색 제거 2025-01-17 14:13:20 +09:00
5be86bf7d6 관리자 시리즈 검색 API - 내용도 검색 되도록 추가 2025-01-17 14:02:47 +09:00
ddb49f6215 관리자 시리즈 검색 API - 검색어 추가
관리자/앱 콘텐츠 배너 API - 시리즈 추가
2025-01-17 14:00:09 +09:00
40c0b72450 관리자 시리즈 검색 API 추가 2025-01-17 04:06:19 +09:00
e0c9a2cea7 관리자 콘텐츠 배너 등록/수정 API
- 시리즈 등록/수정 기능 추가
2025-01-17 03:15:31 +09:00
df3f045209 앱 이벤트 배너 조회 API
- 앱에서 불필요한 날짜, 팝업용, 본인인증 데이터 제거
2025-01-16 01:30:19 +09:00
6ccdfab551 관리자용 이벤트 배너 API 2025-01-16 01:24:04 +09:00
24dc521f83 관리자 충전이벤트
- 패키지명 변경 (admin/event -> admin/event/charge)
2025-01-15 12:29:06 +09:00
cdf96f4f6a 콘텐츠 메인 탭 엔티티
- 패키지 이동
2025-01-15 00:57:20 +09:00
807de3db57 콘텐츠 메인 탭 엔티티 추가
오디오 콘텐츠 배너
- 시리즈와의 연결을 위해 AudioContentBannerType 에 SERIES 추가
- tab, series 테이블과의 관계 추가
2025-01-14 19:09:26 +09:00
e3e4151187 불필요한 Transaction 애노테이션 제거 2025-01-13 17:19:38 +09:00
1d268da08d Merge pull request '오디션 등록 푸시알림 메시지 수정' (#253) from test into main
Reviewed-on: #253
2025-01-10 10:23:19 +00:00
b34459e6c6 오디션 등록 푸시알림 메시지 수정
- [오디션 제목]이 등록되었습니다. -> '오디션 제목'이 등록되었습니다.
2025-01-09 01:27:36 +09:00
797666ae0d Merge pull request 'test' (#252) from test into main
Reviewed-on: #252
2025-01-08 14:11:08 +00:00
dd5e6c399b 오디션 등록 푸시알림
- data key 변경
2025-01-08 22:51:48 +09:00
456372c7fb 오디션 등록 푸시알림 문구 오타수정
- 오리지얼 -> 오리지널
2025-01-08 18:00:40 +09:00
b4cd489ee9 푸시정보
- 오디션 알림 추가
2025-01-08 17:45:50 +09:00
b04f35c2da 오디션 수정
- 오디션 상태를 모집중으로 변경시 오디션 알림 받기가 되어 있는 유저에게 푸시 발송
2025-01-08 17:38:11 +09:00
dcf470997e Merge pull request 'test' (#251) from test into main
Reviewed-on: #251
2025-01-08 06:29:33 +00:00
a26bb19b0f 관리자 오디션 지원 리스트 삭제 API
- 문구변경
2025-01-08 15:15:18 +09:00
6182a7a77e 관리자 오디션 지원 리스트
- 삭제 기능 추가
2025-01-08 15:04:22 +09:00
0974d1dbf8 Merge pull request '관리자 오디션 지원 리스트' (#250) from test into main
Reviewed-on: #250
2025-01-07 19:44:39 +00:00
d090631d1c 관리자 오디션 지원 리스트
- 지원자 연락처 추가
- 유효한 지원만 조회되도록 수정
2025-01-08 04:27:21 +09:00
12a35db6cd Merge pull request '오디션' (#249) from test into main
Reviewed-on: #249
2025-01-07 17:24:40 +00:00
c4d9d503ac 탐색에 있는 크리에이터 랭킹과 동일한 별도의 API 생성 2025-01-05 15:35:26 +09:00
824cd2f3ea 오디션
- endDate 제거
2025-01-05 15:27:43 +09:00
47dfaec226 크리에이터 프로필 API
- isCreator를 isCreatorRole이라는 이름으로 변경
- 나타내는 값은 동일하다
2025-01-04 01:07:37 +09:00
eb36313c9b 크리에이터 프로필 API
- 랭킹, 추천 크리, 라이브, 콘텐츠, 시리즈, 커뮤니티, 활동요약을 크리에이터인 경우에만 조회하도록 수정
2025-01-04 00:43:10 +09:00
354fbf7e29 오디션 지원 리스트
- 지원자 memberId 추가
2025-01-03 23:40:26 +09:00
1ddd40948e 오디션 투표 - 횟수 계산 방식 수정
- 오디션 지원자별 하루 10개 -> 전체 투표 횟수 하루 10개
2025-01-03 13:00:02 +09:00
64d9f3e362 오디션 지원 리스트
- 비활성화 된 데이터는 조회되지 않도록 수정
2025-01-03 08:33:52 +09:00
460196dc4d 오디션 지원 리스트
- 페이징 적용
2025-01-03 08:04:58 +09:00
80841fe543 오디션 지원
- 오디션 지원 파일 저장 경로 수정
2025-01-03 07:53:16 +09:00
c8f96a10f0 캔 사용내역 - 오디션 투표
- "[오디션 투표] 오디션명"으로 변경
2025-01-03 01:29:42 +09:00
b10c102f94 오디션 투표 API
- 투표시 어떤 오디션 지원에 투표했는지 기록
- 캔 사용내역에 "[오디션 투표] 닉네임" 추가
2025-01-03 01:03:38 +09:00
82b109e3bd 오디션 투표 API
- 투표시 어떤 오디션 지원에 투표했는지 기록
- 캔 사용내역에 "[오디션 투표] 닉네임" 추가
2025-01-03 00:45:49 +09:00
cd0c066978 앱 - 오디션 투표 API 2025-01-03 00:09:53 +09:00
7a395a9906 앱 - 오디션 지원 API
- 기존에 지원한 내역이 있으면 false 처리 후 지원
2025-01-02 22:46:57 +09:00
96f571e0c4 앱 - 오디션 지원 API 2025-01-02 19:32:31 +09:00
8385800e48 앱 - 오디션 상세 캐릭터 리스트
- status 내림차순 정렬
2024-12-31 09:12:26 +09:00
9315447618 앱 - 오디션 리스트 API
- offset, limit 추가
2024-12-31 07:26:25 +09:00
00c306475c 앱 - 오디션 리스트 API
- 상태 정렬 추가
2024-12-31 03:37:00 +09:00
affbb3eba3 앱 - 배역 상세 API, 지원 리스트 API 2024-12-31 03:06:34 +09:00
ddd552deb4 앱 - 오디션 상세 API 2024-12-30 23:42:25 +09:00
b56b2e15af 오디션 캐릭터(배역) 관리자 API
- 관리자만 실행할 수 있도록 수정
2024-12-30 20:49:03 +09:00
f77b5f67d0 앱 API - 오디션 리스트 2024-12-30 20:29:28 +09:00
bb41a81eb1 오디션 배역 수정
- 배역 이름과 배역 정보 유효성 검사 추가
2024-12-28 03:50:43 +09:00
44dfa45ca8 오디션 배역 조회
- 오디션 배역 정보 추가
2024-12-28 03:40:04 +09:00
8cfe9ade9a 오디션 배역 등록/수정
- 오디션 배역 정보 추가
2024-12-28 03:29:11 +09:00
6e6b27bb65 오디션 배역 수정
- 모집 상태를 수정할 수 있는 변수 추가
2024-12-28 02:04:04 +09:00
df3a00f8c0 오디션 배역 수정
- 모집 상태를 수정할 수 있는 변수 추가
2024-12-28 01:51:46 +09:00
2e66b5fa45 오디션 상세 - 오디션 배역 리스트
- 활성화된 배역만 조회되도록 수정
2024-12-28 01:40:16 +09:00
1dba0a3d95 오디션 상세
- 오디션 배역 리스트 데이터에 대본 링크 추가
2024-12-28 01:11:26 +09:00
c9e90974bd 오디션 상세
- 오디션 데이터와 오디션 배역 리스트 데이터 호출을 분리
2024-12-27 23:19:21 +09:00
4f0a882b9e 오디션 상세
- groupBy에 비집계열 모두 추가
2024-12-27 22:05:32 +09:00
a35b602f1a 오디션 상세
- roleList의 조회값이 없는 경우 emptyList로 선언되도록 처리
2024-12-27 21:53:37 +09:00
a3e717f2f7 오디션 배역(캐릭터) 엔티티
- status(모집상태) 추가
2024-12-27 02:07:35 +09:00
8b10e0e770 오디션 배역(캐릭터) 엔티티
- status(모집상태) 추가
2024-12-27 02:00:15 +09:00
22c302efa0 오디션 엔티티
- status(모집상태) 추가
- 리스트 api : 응답값에 status 추가, 활성화 데이터만 조회
- 수정 api : status 수정 기능 추가
2024-12-27 01:56:45 +09:00
86450533cf 오디션 리스트 API
- endDate와 원작링크가 null인 경우 빈 칸으로 처리하는 로직 추가
2024-12-26 22:43:15 +09:00
d940b3092f 오디션 엔티티
- information column type을 TEXT로 수정
2024-12-25 02:46:55 +09:00
99fdf473ae 관리자 오디션 캐릭터
- 등록, 수정, 상세, 지원리스트 API
2024-12-25 02:39:21 +09:00
bb3263dd68 관리자 오디션 상세 API 2024-12-24 16:28:11 +09:00
e29e71b8bd 오디션
- 엔티티 작성
- 관리자 - 오디션 생성, 수정, 리스트 API
2024-12-24 03:26:20 +09:00
9abbb05ad8 Merge pull request 'test' (#248) from test into main
Reviewed-on: #248
2024-12-18 07:10:01 +00:00
0c4dc7e5df 재생목록 리스트, 상세
- 대여가 만료된 콘텐츠를 제외하고 조회되도록 수정
2024-12-18 06:39:10 +09:00
36052f034a 재생목록 등록/수정
- 등록/수정시 만료 날짜 시간 추가
2024-12-18 06:18:36 +09:00
00e4fefc8f 구매목록
- orderType 파라미터 제거
2024-12-18 03:22:13 +09:00
1ecaf69b0b Merge pull request 'test' (#247) from test into main
Reviewed-on: #247
2024-12-17 13:43:45 +00:00
a1f9b676b5 재생목록 상세 날짜 포맷 변경
- yyyy-MM-dd -> yyyy.MM.dd
2024-12-10 03:37:39 +09:00
330e4945e1 재생목록 리스트 정렬순서 변경
- 미정렬 -> 생성날짜 내림차순 정렬
2024-12-09 17:41:49 +09:00
0583a8a56f 재생목록 콘텐츠
- 크리에이터 프로필 이미지 추가
2024-12-08 15:44:42 +09:00
bf62482137 콘텐츠 URL 생성 API 2024-12-06 23:07:59 +09:00
ba17095536 구매목록 조회
- 구매유형별로 조회할 수 있도록 orderType 추가
2024-12-04 18:44:17 +09:00
4ff5e9e163 재생 목록 콘텐츠 조회
- 크리에이터 닉네임 추가
2024-12-04 13:32:35 +09:00
8a03249759 콘텐츠 커버이미지 조회 API
- 빠진 조건문 추가
2024-12-04 11:18:42 +09:00
72cb90357e 플레이 리스트 생성 API - 유효성 검사
- 콘텐츠가 1개 이상 등록되어 있어야 한다.
2024-12-04 10:59:43 +09:00
72563e9bfa 플레이 리스트 콘텐츠 가져오기 API
- 빠진 조건문 추가
2024-12-04 10:55:17 +09:00
e334d1e5d9 Merge pull request '콘텐츠 댓글 푸시 대상자' (#246) from test into main
Reviewed-on: #246
2024-12-03 15:54:34 +00:00
f503492bf9 콘텐츠 댓글 푸시 대상자
- 댓글 : 콘텐츠 크리에이터에게만
- 답글 : 원 댓글 쓴 사람
2024-12-04 00:40:26 +09:00
b735e861d0 Merge pull request '콘텐츠 댓글 푸시 대상자 조회' (#245) from test into main
Reviewed-on: #245
2024-12-02 15:06:49 +00:00
c7513e9045 콘텐츠 댓글 푸시 대상자 조회
- audioContentComment 조회 대상에 추가
2024-12-03 00:01:17 +09:00
4eb433d372 Merge pull request 'test' (#244) from test into main
Reviewed-on: #244
2024-12-02 12:05:30 +00:00
368c647151 Redisson Config
- 최소 유휴 연결 0, DNS 모니터링 간격 30초로 변경
2024-12-02 20:26:46 +09:00
1ca676ce0b Redisson Config
- 최소 유휴 연결 0, DNS 모니터링 간격 30초로 변경
2024-12-02 20:25:07 +09:00
b33945d21c Redisson Config
- ssl 설정
2024-12-02 19:48:51 +09:00
1649c08356 예약 업로드 오픈 스케줄러
- 서버 한 대에서만 실행되도록 Redisson을 이용하여 분산락 적용
2024-12-02 19:18:28 +09:00
2416ae61f3 Merge pull request 'test' (#243) from test into main
Reviewed-on: #243
2024-12-02 04:29:50 +00:00
c54105e65b AudioContentReleaseScheduledTask
- Qualifier로 audioContentReleaseScheduler 선택하도록 추가
2024-12-02 11:12:31 +09:00
9039a7a2d0 taskScheduler에 Primary 설정 2024-12-02 11:02:02 +09:00
a1ef9a4970 콘텐츠 예약 오픈 설정
- 스케줄러 설정 수정
- 외부에서 실행되는 endpoint 제거
2024-12-02 10:46:48 +09:00
c1748001d5 콘텐츠 예약 오픈 설정
- 스케줄러 설정 추가
2024-12-02 08:58:54 +09:00
e470e70612 콘텐츠 예약 오픈 설정
- 분산락 제거, 서버가 여러대라면 여러번 호출될 수 있음
2024-12-02 08:40:38 +09:00
e0d48712ac 콘텐츠 예약 오픈 설정
- 콘텐츠 id뿐 아니라 콘텐츠 전체를 불러와서 중복호출 하지 않도록 수정
2024-12-02 08:25:55 +09:00
05592f94b9 스프링 스케줄러를 이용하여 콘텐츠 예약 오픈 설정 2024-12-02 08:22:16 +09:00
4097e5a133 플레이 리스트에 저장하는 콘텐츠ID에 정렬순서 값 추가
- 이유: 플레이 리스트에 저장하는 콘텐츠ID에는 순서가 있지만 해당 값으로 조회한 콘텐츠 상세내용의 정렬이 콘텐츠ID가 저장된 순서대로 나온다는 보장이 없음, 조회한 콘텐츠 상세의 정렬을 위해 추가
2024-11-27 19:31:59 +09:00
3093d2159d 플레이 리스트 상세 API 추가 2024-11-27 19:09:18 +09:00
d6e5a45be9 플레이 리스트 수정 API 추가 2024-11-27 18:07:17 +09:00
10a65294ce 플레이 리스트 생성 API 수정
- 기존: 플레이 리스트에 콘텐츠 ID와 정렬순서도 같이 저장됨
- 변경: 콘텐츠 ID만 저장됨
- 이유: List 사용하기에 정렬순서를 별도로 저장할 필요가 없다고 판단
2024-11-27 17:38:55 +09:00
22c5c5be25 플레이 리스트 생성 API
- 기존: 플레이 리스트에 콘텐츠 ID만 저장됨
- 변경: 콘텐츠 ID와 정렬순서도 같이 저장됨
2024-11-27 17:15:33 +09:00
7f4de67d67 플레이 리스트 API
- 조회, 생성, 삭제 추가
2024-11-27 01:59:01 +09:00
01fb336985 Merge pull request '콘텐츠 등록' (#242) from test into main
Reviewed-on: #242
2024-11-26 12:46:24 +00:00
559df6c7b8 콘텐츠 등록
- 테마가 모닝콜, 알람, 슬립콜인 경우 5캔 이상의 유료콘텐츠로만 등록이 가능하도록 수정
2024-11-26 21:31:58 +09:00
b6af88a732 Merge pull request 'test' (#241) from test into main
Reviewed-on: #241
2024-11-26 05:33:45 +00:00
b55e08a719 콘텐츠 등록
- 테마가 모닝콜, 알람, 슬립콜인 경우 소장만 가능하도록 수정
2024-11-26 14:27:17 +09:00
cc72e44fca 콘텐츠 상세 - isFullDetailVisible가 false
- 콘텐츠 설명 최대 10 -> 30글자로 수정
2024-11-26 13:39:17 +09:00
58a2a17d6d Merge pull request 'test' (#240) from test into main
Reviewed-on: #240
2024-11-23 17:59:23 +00:00
84804d32ad 콘텐츠 상세
- 50캔 이상의 유료콘텐츠이고 구매하지 않은 콘텐츠 이고 isFullDetailVisible가 false이면 콘텐츠 설명이 최대 10글자까지만 보이도록 수정
2024-11-24 02:02:50 +09:00
fcae1b6770 콘텐츠 등록
- 50캔 이상의 유료콘텐츠는 콘텐츠 설명을 숨길 수 있도록 isFullDetailVisible 추가
2024-11-24 01:48:17 +09:00
b7d7afb8a5 redis를 이전하기 위해 설정했던 모든 커밋 Revert 2024-11-24 01:23:41 +09:00
e38ed331b6 redis repository 자동 스캔 비활성화 2024-11-24 00:06:02 +09:00
2ba798b606 올바르게 Bean이 설정되었는지 출력하는 코드 추가 2024-11-23 23:02:49 +09:00
ee0c99bec9 redis core, redis connection 로깅레벨 DEBUG 2024-11-23 21:34:23 +09:00
e7232db2f3 Redis 패키지 별도로 분리하여 다중 구성이 용이하도록 수정 2024-11-23 21:15:14 +09:00
4dc0a13203 라이브 방 룰렛 처리 및 저장
- Redis -> Valkey로 이전되도록 수정
2024-11-23 01:58:24 +09:00
2f2437e14d 라이브 방 메뉴 처리 및 저장
- Redis -> Valkey로 이전되도록 수정
2024-11-23 01:29:34 +09:00
695ccf975b 라이브 방 강퇴 정보 처리 및 저장
- Redis -> Valkey로 이전되도록 수정
2024-11-23 00:25:45 +09:00
2d0492cafa 라이브 방 정보 처리 및 저장
- Redis -> Valkey로 이전되도록 수정
2024-11-23 00:05:27 +09:00
68472b234e 회원토큰 처리
- Redis -> Valkey로 이전되도록 수정
2024-11-22 21:22:02 +09:00
157e3a39b6 여러대의 Redis와 Valkey에 연결할 수 있도록 환경설정 2024-11-22 17:54:23 +09:00
79f5a0f520 Merge pull request '내 콘텐츠 수정, 삭제 시 콘텐츠 조회 함수' (#239) from test into main
Reviewed-on: #239
2024-11-21 06:34:30 +00:00
831bd731ca 내 콘텐츠 수정, 삭제 시 콘텐츠 조회 함수
- 내 콘텐츠는 비활성화 된 콘텐츠도 조회할 수 있도록 수정
2024-11-21 15:27:53 +09:00
7f6c0f7f04 Merge pull request 'Redis connection 수정' (#238) from test into main
Reviewed-on: #238
2024-11-20 09:58:56 +00:00
354ae68dd1 Redis connection 수정 2024-11-20 18:43:19 +09:00
234a46d2ac Redis connection 수정 2024-11-20 18:30:24 +09:00
f658df4dca Merge pull request 'Redis connection' (#237) from test into main
Reviewed-on: #237
2024-11-20 07:52:58 +00:00
5c7bf8086c Redis connection
- clientOption 변수로 분리
2024-11-20 16:48:08 +09:00
9d43b8e23a Merge pull request 'Redis connection' (#236) from test into main
Reviewed-on: #236
2024-11-20 06:47:52 +00:00
dd614c07e2 Redis connection
- 클러스터 모드 설정
2024-11-18 14:11:09 +09:00
4270aef79b Merge pull request 'test' (#235) from test into main
Reviewed-on: #235
2024-11-11 15:34:35 +00:00
f134bc4599 라이브 방
- 현재 라이브 하트 랭킹 API
2024-11-12 00:05:40 +09:00
19dc676c36 라이브 방
- 현재 라이브 하트 랭킹 API
2024-11-11 21:07:02 +09:00
adf181f790 라이브 방
- 현재 라이브 하트 랭킹 API 404나와서 임시 제거
2024-11-11 20:49:48 +09:00
82b07897f8 라이브 방
- 현재 라이브 하트 랭킹 API 추가
2024-11-11 20:29:48 +09:00
f9dd3bc7e2 라이브 방
- 현재 라이브 하트 랭킹 API 추가
2024-11-11 16:09:46 +09:00
7cec01897f 라이브 방 - 후원 메시지 리스트
- 비밀 후원과 일반 후원 메시지 분리
2024-11-11 14:02:48 +09:00
297f6555f3 라이브 방 - 후원 메시지 리스트
- 방장은 모든 후원 메시지를 볼 수 있도록 수정
2024-11-11 12:37:14 +09:00
6111409d66 라이브 방 - 후원 메시지 리스트
- 비밀 후원 메시지 추가
2024-11-11 12:29:29 +09:00
1c0dc82d44 Merge pull request '콘텐츠 구매 - 소장만 추가' (#234) from test into main
Reviewed-on: #234
2024-11-08 12:40:29 +00:00
5820117c1a 콘텐츠 상세
- 기존 데이터와의 호환성을 위해 isOnlyRental == true <=> PurchaseOption.RENT_ONLY으로 표현되도록 수정
2024-11-08 17:15:19 +09:00
cc695c115b 콘텐츠 구매
- 대여만 가능한 콘텐츠를 소장 하려는 경우 안내메시지 추가
2024-11-08 14:54:53 +09:00
86dac7e2b4 콘텐츠 구매
- 소장만 가능한 콘텐츠를 대여 하려는 경우 안내메시지 추가
2024-11-08 14:50:56 +09:00
52ddefa631 콘텐츠 업로드
- 구매옵션이 RENT_ONLY 인 경우 기존에 있던 isOnlyRental 필드 true 로 저장
2024-11-08 14:46:48 +09:00
c46d6621ec 콘텐츠 상세
- 구매옵션(모두, 소장만, 대여만) 추가
2024-11-08 00:48:30 +09:00
d94ef1eb25 콘텐츠 등록
- 구매옵션(모두, 소장만, 대여만) 추가
2024-11-08 00:47:12 +09:00
eee59855cc BundleAudioContent 제거 2024-11-07 18:48:29 +09:00
c1e325aadf Merge pull request 'test' (#233) from test into main
Reviewed-on: #233
2024-11-05 07:26:19 +00:00
efdf1d3eed 라이브방 후원리스트
- 정렬 수정
2024-11-05 16:00:23 +09:00
65b28f92d5 라이브방 후원리스트
- 내가 후원한 비밀후원은 조회되도록 수정
2024-11-05 15:19:14 +09:00
a5845c90c2 라이브방 후원리스트
- 내가 후원한 비밀후원은 조회되도록 수정
2024-11-05 15:14:39 +09:00
cec87da69d Merge pull request '콘텐츠 대여가격' (#232) from test into main
Reviewed-on: #232
2024-10-31 05:23:01 +00:00
8ea51774e6 콘텐츠 대여가격
- 소장가격의 60% -> 70%로 변경
2024-10-31 14:13:39 +09:00
f68f24cb2c Merge pull request 'test' (#231) from test into main
Reviewed-on: #231
2024-10-31 03:09:13 +00:00
baeea79e66 이벤트 조회
- 시작날짜, 종료날짜 format 'yyyy-MM-dd'로 변경
2024-10-30 23:55:07 +09:00
85f14edc0a 이벤트 조회
- 시작날짜, 종료날짜 추가
2024-10-30 23:28:36 +09:00
116aea3431 이벤트 조회
- 시작날짜, 종료날짜 추가
2024-10-30 23:23:27 +09:00
b8299bc139 이벤트
- 시작날짜, 종료날짜 추가
2024-10-30 23:19:13 +09:00
ed094347fc Merge pull request '라이브 방 후원랭킹' (#230) from test into main
Reviewed-on: #230
2024-10-30 05:16:05 +00:00
7f1fadf068 라이브 방 후원랭킹
- 방장(크리에이터)인 경우 일반후원과 비밀후원의 합을 내림차순으로 정렬
- 리스너인 경우 일반후원의 합만 내림차순으로 정렬
2024-10-30 14:08:00 +09:00
b8afdffbe1 Merge pull request 'test' (#229) from test into main
Reviewed-on: #229
2024-10-29 08:35:58 +00:00
9fcd172581 라이브 방 후원리스트
- 비밀후원 캔 0보다 크거나 일반후원 캔이 0보다 큰 경우에만 데이터 반환
2024-10-29 00:28:03 +09:00
9425889e93 라이브 방 후원리스트
- 후원리스트 합계 일반후원과 비밀후원 값을 분리
2024-10-28 16:32:08 +09:00
4c9277f61a 라이브 방 후원리스트
- 후원리스트에 일반후원과 비밀후원 값을 분리
2024-10-28 15:25:50 +09:00
f6ba79f31c Merge pull request 'test' (#228) from test into main
Reviewed-on: #228
2024-10-25 03:45:26 +00:00
91a8aa0afe 라이브 방 룰렛 업데이트
- 룰렛을 모두 비활성화 했더라도 룰렛이 활성화 된 것으로 반환하던 버그 수정
2024-10-23 01:06:21 +09:00
fa99c296a5 (크리에이터) 관리자 라이브 정산
- 캔 사용 구분에 "하트" 추가
2024-10-22 22:37:37 +09:00
dcf0637420 라이브 방 룰렛 업데이트
- 룰렛을 모두 비활성화 했더라도 룰렛이 활성화 된 것으로 반환하던 버그 수정
2024-10-22 22:04:50 +09:00
98b337c5ee 라이브 방
- 하트 후원 합계 조회 API 추가
2024-10-16 18:21:54 +09:00
ba0151bca0 라이브 방
- 하트 후원 기능 추가
2024-10-16 17:50:39 +09:00
c8c081b3fd 라이브 방
- 하트 후원 기능 추가
2024-10-16 17:32:50 +09:00
5f3b1663d2 Merge pull request '관리자 - 콘텐츠 리스트' (#227) from test into main
Reviewed-on: #227
2024-10-16 03:33:23 +00:00
078e601100 관리자 - 콘텐츠 리스트
- 찾기에 content release status 추가
2024-10-16 12:25:26 +09:00
4eb4c19386 관리자 - 콘텐츠 리스트
- 개수 조회 함수에도 status 추가
2024-10-16 12:23:16 +09:00
656fd9a1fa 관리자 - 콘텐츠 리스트 리팩토링
- imageHost 값 repository로 이동
2024-10-16 12:12:21 +09:00
7f70e508e4 관리자 - 콘텐츠 리스트
- 오픈 / 오픈 예정 추가
2024-10-16 12:09:13 +09:00
66e786b4bb Merge pull request '관리자 - 시리즈 리스트 API' (#226) from test into main
Reviewed-on: #226
2024-10-14 15:39:45 +00:00
ebb9e5f4ae 관리자 - 시리즈 리스트 API
- 작품개수 추가
2024-10-15 00:32:35 +09:00
adfad4ad40 관리자 - 시리즈 리스트 API
- 작품개수 추가
2024-10-15 00:10:52 +09:00
6409078c03 관리자 - 시리즈 리스트 API
- 작품개수 추가
2024-10-15 00:01:47 +09:00
f671114574 Merge pull request '관리자 - 시리즈 리스트 API' (#225) from test into main
Reviewed-on: #225
2024-10-14 10:37:28 +00:00
10b2bd1480 관리자 - 시리즈 리스트 API
- 정렬 순서 수정 (orders -> member 정렬 후 orders 정렬)
2024-10-14 19:26:20 +09:00
ce37060d94 Merge pull request '관리자 - 시리즈 리스트 API 추가' (#224) from test into main
Reviewed-on: #224
2024-10-14 10:10:43 +00:00
79e3d59a9a 관리자 - 시리즈 리스트 API 추가 2024-10-14 18:58:48 +09:00
7d19a4d184 Merge pull request 'test' (#223) from test into main
Reviewed-on: #223
2024-10-13 17:30:25 +00:00
cda42c26e8 큐레이션 콘텐츠
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품이 조회되도록 수정
2024-10-14 02:13:55 +09:00
170cf9f217 콘텐츠 메인 - 추천시리즈
- 비성인 콘텐츠만 보기를 했을 때 조회가 되지 않는 버그 수정
2024-10-14 02:02:55 +09:00
22f28a2f8a Merge pull request '콘텐츠 메인 - 추천시리즈, 새로운 콘텐츠, 큐레이션' (#222) from test into main
Reviewed-on: #222
2024-10-13 16:33:15 +00:00
43662738fc 콘텐츠 메인 - 추천시리즈, 새로운 콘텐츠, 큐레이션
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 작품이 조회되도록 수정
2024-10-14 01:17:47 +09:00
ceef9ca979 Merge pull request 'test' (#221) from test into main
Reviewed-on: #221
2024-10-11 05:06:22 +00:00
057ff17240 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 남성향이면 여성 크리에이터, 여성향이면 남성 크리에이터 19작품 + 비성인 작품이 조회되도록 수정
2024-10-10 15:47:47 +09:00
b9ad3bdb72 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 19금 콘텐츠 성향 데이터가 있을 때 성향 + 비성인 콘텐츠가 조회되도록 수정
2024-10-10 15:36:21 +09:00
fc8b944031 콘텐츠 메인 - 새로운 콘텐츠
- 본인인증을 했더라도 19금 콘텐츠 보기를 활성화 하지 않으면 19금 콘텐츠가 보이지 않도록 수정
2024-10-10 13:27:11 +09:00
5bfcbe9126 라이브 방 조회
- 본인인증을 했더라도 19금 콘텐츠 보기를 활성화 하지 않으면 19금 라이브가 보이지 않도록 수정
2024-10-09 00:25:00 +09:00
0d6e4804f5 콘텐츠 메인 - 새로운 콘텐츠, 큐레이션
- 본인인증을 했더라도 19금 콘텐츠 보기를 활성화 하지 않으면 19금 콘텐츠가 보이지 않도록 수정
- 콘텐츠 유형(남성향/여성향)을 선택할 수 있도록 수정
2024-10-09 00:18:56 +09:00
d024e0fa23 라이브 메뉴 - 생성 or 수정 API
- RequestParam -> RequestBody로 변경
2024-10-08 15:20:22 +09:00
f813d8eae4 라이브 메뉴
- 생성 or 수정 API 추가
2024-10-08 15:10:24 +09:00
efe8f4f939 Merge pull request '콘텐츠 메인 - 새로운 콘텐츠 섹션 두번째 정렬 조건 추가' (#220) from test into main
Reviewed-on: #220
2024-10-04 07:21:38 +00:00
e0424b1678 콘텐츠 메인 - 새로운 콘텐츠 섹션 두번째 정렬 조건 추가
- 최신순은 id값 내림차순, 오래된 순은 id값 오름차순 추가
2024-10-04 16:15:14 +09:00
ba692a1195 Merge pull request '시리즈 상세 - 콘텐츠 리스트 두번째 정렬 조건 추가' (#219) from test into main
Reviewed-on: #219
2024-10-04 02:41:28 +00:00
afc7bd68c1 시리즈 상세 - 콘텐츠 리스트 두번째 정렬 조건 추가
- 최신순은 id값 내림차순, 오래된 순은 id값 오름차순 추가
2024-10-04 11:35:56 +09:00
d732bad042 Merge pull request '시리즈 상세' (#218) from test into main
Reviewed-on: #218
2024-10-02 09:18:19 +00:00
d70a70b19c 시리즈 상세
- 연재 요일을 표시할 때 일~토 모두가 포함되어 있을 때 '매일'로 표시
2024-10-02 18:08:23 +09:00
4c935c3bee Merge pull request '예약 라이브 개수 제한' (#217) from test into main
Reviewed-on: #217
2024-09-25 05:42:45 +00:00
f1611efe7c 라이브 예약
- 3개를 초과할 때 안내 문구 수정
2024-09-25 14:29:35 +09:00
9f2b43ea0c 라이브
- 예약 개수 최대 3개까지로 한정
2024-09-25 14:16:16 +09:00
c160dd791f Merge pull request 'test' (#216) from test into main
Reviewed-on: #216
2024-09-24 10:17:58 +00:00
35e5e518f5 라이브
- 스피커 인원 최대 5명으로 수정
2024-09-24 19:11:10 +09:00
afebc190f6 콘텐츠 업로드
- 미리 듣기 최소 시간 30초 -> 15초로 변경
2024-09-24 13:39:31 +09:00
23cd1b4601 Merge pull request '라이브 후원현황 API' (#215) from test into main
Reviewed-on: #215
2024-09-23 13:58:14 +00:00
e1b142a044 라이브 후원현황 API
- 쿼리시 조인하지 않은 테이블의 컬럼을 조회해서 발생한 예외현황 수정
2024-09-23 22:43:12 +09:00
031fc8ba1b Merge pull request 'test' (#214) from test into main
Reviewed-on: #214
2024-09-23 06:24:12 +00:00
7204acb2bb 라이브 생성/시작, 콘텐츠 업로드, 커뮤니티 글 업로드
- 알림을 허용한 유저만 푸시를 받도록 수정
2024-09-20 12:34:12 +09:00
738f1ea9fe isNotice -> isNotify로 변경 2024-09-20 12:04:17 +09:00
2a70a7824f 크리에이터 내 채널 - 팔로워 리스트
- 팔로우/알람 여부 - 크리에이터이면 항상 Boolean 값을 반환하고 일반유저면 null을 반환하도록 수정
2024-09-20 01:27:37 +09:00
9f848e1bdc 크리에이터 내 채널 - 팔로워 리스트
- 알림설정 여부 null이 들어가도록 수정
2024-09-20 01:20:23 +09:00
0c153aeb6a 크리에이터 내 채널 - 팔로워 리스트
- 알림설정 여부 추가
2024-09-20 01:10:29 +09:00
2c4c19990a 라이브 - 후원현황 API
- 방장은 비밀후원 내역도 반영되도록 수정
2024-09-20 01:04:02 +09:00
dd0b751a43 라이브
- repository 호출 메소드에서 파라미터로 넘기던 cloudFrontHost 제거
2024-09-20 00:56:27 +09:00
a25b7d5cc2 콘텐츠 랭킹
- 한정판 제외
2024-09-19 16:36:55 +09:00
fd3d596d57 콘텐츠 상세, 팔로잉 리스트, 시리즈 상세 API
- 알림과 팔로우 상태값 추가
2024-09-13 19:24:40 +09:00
20938e7d43 크리에이터 프로필 API
- 알림과 팔로우 상태값 null 처리
2024-09-13 12:21:07 +09:00
9516ce1c0f 크리에이터 프로필 API
- 알림과 팔로우 상태값 분리
2024-09-13 12:02:43 +09:00
10f484357c 크리에이터 팔로우 API 알림 상태 수정
- 알림과 팔로우 상태 모두 true 일 때만 알림이 true가 되고 둘 중 하나라도 false이면 false가 되도록 수정
2024-09-13 01:48:16 +09:00
1051846c5b 크리에이터 팔로우 API
- 팔로우 / 언팔로우 / 알림 받기 / 알림 받지 않기 를 모두 처리할 수 있도록 수정
2024-09-12 23:51:28 +09:00
c6853289ad Merge pull request 'test' (#213) from test into main
Reviewed-on: #213
2024-09-11 08:23:08 +00:00
4c50143834 시리즈 콘텐츠 정렬
- 콘텐츠 오픈 날짜를 기준으로 정렬되도록 수정
2024-09-11 17:13:54 +09:00
d5e6ea8677 라이브 상세
- 본인인증을 하지 않은 유저가 19+방을 조회하는 경우 안내 멘트 수정
2024-09-11 16:52:53 +09:00
2497bb69bc Merge pull request 'test' (#212) from test into main
Reviewed-on: #212
2024-09-11 07:47:35 +00:00
edb77d7ad7 required = false 인 파라미터 전부 옵셔널 파라미터로 변경 2024-09-11 16:41:25 +09:00
2f58bdb381 시리즈 콘텐츠 정렬
- defaultValue 대신 required = false로 수정
2024-09-11 16:36:38 +09:00
a58a67e0a2 Merge pull request 'test' (#211) from test into main
Reviewed-on: #211
2024-09-11 06:00:31 +00:00
472cfdb45b 라이브 방 정보 수정
- 연령제한 수정 기능 추가
2024-09-10 23:43:49 +09:00
d0a0b5ecbe 시리즈 콘텐츠 리스트 정렬
- required = false 대신 defaultValue를 이용하여 기본값 설정
2024-09-10 17:46:17 +09:00
5e01ece02b 시리즈 콘텐츠 리스트 정렬
- 기본값을 NEWEST로 변경
2024-09-10 16:26:17 +09:00
47748875c0 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-10 16:11:13 +09:00
d804ad268a 시리즈 콘텐츠 리스트
- 정렬(최신순, 등록순) 추가
2024-09-09 23:41:29 +09:00
4315fe12a5 Merge pull request 'test' (#210) from test into main
Reviewed-on: #210
2024-09-06 19:00:39 +00:00
8e5b43a14e 불필요한 코드 제거 2024-09-07 01:56:23 +09:00
7f855bfc56 다른 회원 프로필 조회 API
- 조회하려는 회원이 정상적으로 조회되도록 조건 추가
2024-09-07 00:51:23 +09:00
307eea3ea2 다른 회원 프로필 조회 API
- 닉네임 추가
2024-09-07 00:24:47 +09:00
e8cb4c6ea2 다른 회원 프로필 조회 API 추가 2024-09-06 23:39:09 +09:00
da3175292b 콘텐츠 댓글
- 조회 방식 수정
- 댓글 작성자의 차단 여부 추가
2024-09-06 19:21:18 +09:00
b70c9518eb 관리자 콘텐츠 후원 정산
- 콘텐츠 정렬을 추가하여 페이지 넘어갈 때 동일한 데이터가 섞이는 버그 수정
2024-09-06 18:54:43 +09:00
1cca469577 관리자 커뮤니티 정산
- 커뮤니티 글 정렬을 추가하여 페이지 넘어갈 때 동일한 데이터가 섞이는 버그 수정
2024-09-06 18:52:19 +09:00
42f10a8899 Merge pull request 'test' (#209) from test into main
Reviewed-on: #209
2024-09-05 10:12:14 +00:00
dd229f15ac 차단 유저 아이디 리스트 가져오기 API 추가 2024-09-04 21:37:40 +09:00
2bd1c99409 팔로잉 채널리스트
- 탈퇴한 유저는 나오지 않도록 수정
2024-09-04 12:08:34 +09:00
b45c762bbe 차단한 유저리스트 조회 API
- 차단여부가 회원탈퇴 여부로 표시되는 버그 수정
2024-09-04 12:06:43 +09:00
a7b58089dd 차단한 유저리스트 조회 API
- 프로필 이미지 Url에 / 추가
2024-09-04 12:04:00 +09:00
e8d16217b0 차단한 유저리스트 조회 API 추가 2024-09-03 22:19:07 +09:00
ebd82ee2c7 차단유저(BlockMember) 엔티티
- Member와의 관계설정
2024-09-03 22:05:21 +09:00
b0c7819b5a 차단한 유저리스트 조회 API 추가 2024-09-03 17:52:34 +09:00
1e4b47f989 Merge pull request 'test' (#208) from test into main
Reviewed-on: #208
2024-08-30 09:17:41 +00:00
93410af224 콘텐츠 댓글 조회 수정
- 콘텐츠 크리에이터 - 댓글 전체 조회
- 일반 유저 - 공개 댓글 + 내가 쓴 댓글
2024-08-29 23:40:44 +09:00
45e8b0d155 콘텐츠 댓글 등록
- 비밀 댓글 등록 기능 추가
2024-08-29 22:41:30 +09:00
ff255dbfae Merge pull request 'test' (#207) from test into main
Reviewed-on: #207
2024-08-27 07:31:05 +00:00
c8d7bdb8b7 pg 결제
- pg사가 카카오페이이면 "카카오페이-'결제수단'"으로 기록
2024-08-27 16:24:17 +09:00
bb897fe965 pg 결제
- 결제수단 pg사 이름으로 추가
2024-08-27 16:14:29 +09:00
1551e3231c pg 결제
- pg 사 기록시 pg로 기록하지 않고 결제에서 받아오는 pg사 이름으로 수정
2024-08-27 16:04:20 +09:00
dbe9b72feb Merge pull request 'test' (#206) from test into main
Reviewed-on: #206
2024-08-23 13:48:24 +00:00
3a1db0f595 라이브 후원
- 비밀 후원 구분자 기본값 false 추가
2024-08-23 22:43:43 +09:00
ac7b91b7e8 탐색
- 특정 크리에이터가 남녀 크리에이터에 표시되지 않도록 수정
2024-08-23 22:36:24 +09:00
aa682aa10a 라이브 후원리스트, 후원합계
- 비밀 후원은 조회되지 않도록 수정
2024-08-23 15:14:53 +09:00
e373e6ab0f 라이브 후원
- 비밀 후원 추가
2024-08-23 15:10:34 +09:00
95a714b391 Merge pull request '탐색' (#205) from test into main
Reviewed-on: #205
2024-08-19 13:21:31 +00:00
6dd2a3136f 탐색
- 특정 크리에이터가 남녀 크리에이터에 표시되지 않도록 수정
2024-08-19 22:17:06 +09:00
28f58c7f56 Merge pull request '라이브' (#204) from test into main
Reviewed-on: #204
2024-08-14 09:34:52 +00:00
8b69458d75 라이브
- 후원 메시지, 룰렛 결과 모든 유저에게 보이도록 수정
2024-08-14 18:19:56 +09:00
8bd46d8f21 Merge pull request '크리에이터 관리자 시리즈' (#203) from test into main
Reviewed-on: #203
2024-08-14 07:41:33 +00:00
50974d55c2 크리에이터 관리자 시리즈
- 순서 변경 로직 추가
2024-08-14 16:20:35 +09:00
e1bb8e54ed Merge pull request '크리에이터 커뮤니티' (#202) from test into main
Reviewed-on: #202
2024-08-06 11:41:11 +00:00
9e679bf787 크리에이터 커뮤니티
- 유료 글일 떄 크리에이터 본인인 경우 오디오 URL이 생성되도록 수정
2024-08-06 20:32:59 +09:00
1de705b063 Merge pull request '크리에이터 커뮤니티' (#201) from test into main
Reviewed-on: #201
2024-08-06 06:37:30 +00:00
80b91aa445 크리에이터 커뮤니티 업로드
- 파라미터 이름 contentFile -> audioFile
2024-08-05 20:36:46 +09:00
c2d7d12767 크리에이터 커뮤니티 조회
- 오디오 콘텐츠가 있는 포스트는 오디오 URL 추가
2024-08-05 17:57:12 +09:00
429dc2b848 크리에이터 커뮤니티
- 오디오 업로드 버킷 변경
2024-08-05 17:31:47 +09:00
ca4ea0e5ea 크리에이터 커뮤니티
- 오디오 등록 기능 추가
2024-08-02 15:17:59 +09:00
f6926ad356 Merge pull request '남/여 크리에이터에서 특정 크리에이터 제거' (#200) from test into main
Reviewed-on: #200
2024-07-26 07:54:18 +00:00
2860deb90a 남/여 크리에이터에서 특정 크리에이터 제거 2024-07-26 16:43:22 +09:00
2cdbbb1b37 Merge pull request 'test' (#199) from test into main
Reviewed-on: #199
2024-07-25 16:08:20 +00:00
4a264d90d4 구매내역
- 알람 슬롯 구매 내역 표시
2024-07-25 22:07:43 +09:00
766d9668c2 알람
- 추가 슬롯 구매 기능 추가
2024-07-25 21:53:29 +09:00
abb60f5743 알람
- 추가 슬롯 개수와 가격 조회 기능 추가
2024-07-25 21:28:17 +09:00
4dce8c8f03 Merge pull request '크리에이터 커뮤니티' (#198) from test into main
Reviewed-on: #198
2024-07-10 05:24:58 +00:00
77a9f1a13c 크리에이터 커뮤니티
- where절의 순서를 변경하여 혹시 생길 수 있는 불상사 방지
2024-07-10 14:21:28 +09:00
bf01bb66e5 크리에이터 커뮤니티
- 구매한 유료게시물은 크리에이터가 삭제하더라도 조회되도록 수정
2024-07-10 14:12:18 +09:00
97a5bace6f Merge pull request 'test' (#197) from test into main
Reviewed-on: #197
2024-07-08 14:17:42 +00:00
dbc5d6c31e 크리에이터 기준 커뮤니티 합계 정산 API 추가 2024-07-08 21:39:30 +09:00
586db3f008 크리에이터 기준 콘텐츠 합계 정산 API 추가 2024-07-08 21:29:41 +09:00
331a1549da 크리에이터 기준 라이브 합계 정산 API 추가 2024-07-08 15:41:51 +09:00
a9d74605af 크리에이터 기준 라이브 합계 정산 API 추가 2024-07-08 14:04:52 +09:00
9bb1195fb6 크리에이터 기준 라이브 합계 정산 API 추가 2024-07-08 13:52:14 +09:00
27bf60b94f 크리에이터 기준 라이브 합계 정산 API 추가 2024-07-08 13:43:07 +09:00
e3bacfb8cb 유료라이브 캔 지불 여부 판단로직
- 환불된 건은 지불되지 않은 걸로 판단하도록 수정
2024-07-08 12:09:02 +09:00
c4e58890ea 크리에이터 기준 라이브 합계 정산 API 추가 2024-07-06 00:15:40 +09:00
80a78a036e 커뮤니티 정산
- 3시간 캐시 적용
2024-07-05 23:08:44 +09:00
8700030707 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 21:29:48 +09:00
0e0114a7d1 룰렛 합계 검증
- Float 값이라 정확히 100.0이 나오지 않으므로 totalPercentage가 99.9보다 크고 100.1보다 작으면 통과되도록 조건 수정
2024-07-02 21:14:15 +09:00
263ca3ac9a 룰렛 합계 검증
- Float를 Decimal로 변경한 후 검증하도록 수정
2024-07-02 19:20:00 +09:00
d4d51ec48f Merge pull request 'test' (#196) from test into main
Reviewed-on: #196
2024-07-02 08:57:11 +00:00
27fbfd49e2 라이브 정산, 일자별 콘텐츠 정산, 콘텐츠별 누적 현황, 후원정산, 커뮤니티 정산
- float 대신 bigdecimal로 변경
2024-07-02 17:52:45 +09:00
d3483c8062 라이브 정산, 일자별 콘텐츠 정산, 콘텐츠별 누적 현황, 후원정산, 커뮤니티 정산
- float 대신 bigdecimal로 변경
2024-07-02 17:40:41 +09:00
2c77143fc2 일자별 콘텐츠 정산, 콘텐츠별 누적 현황, 후원정산, 커뮤니티 정산
- float 대신 bigdecimal로 변경
2024-07-02 16:47:31 +09:00
fded23b97d 라이브 정산
- float 대신 bigdecimal로 변경
2024-07-02 14:44:36 +09:00
9d7bd8e9ab PG결제
- 헥토파이낸셜 검증코드 추가
2024-06-29 19:00:16 +09:00
fb91398462 Merge pull request '커뮤니티 게시물' (#195) from test into main
Reviewed-on: #195
2024-06-17 14:09:26 +00:00
e87f19a8df 커뮤니티 게시물
- 글자 수 계산시 codePointCount을 사용하여 글자수를 유니코드 기준으로 계산 하도록 수정
2024-06-17 23:02:20 +09:00
105dadd798 Merge pull request '커뮤니티 게시물' (#194) from test into main
Reviewed-on: #194
2024-06-15 11:57:33 +00:00
6daaf22dc0 커뮤니티 게시물
- 유료 게시물 글자 수를 기준으로 자를 때 offsetByCodePoints를 사용하여 유니코드 코드 포인트 기준으로 문자열의 length만큼의 끝 인덱스를 계산하여 이모티콘이 잘려서 정상적으로 표시되지 않는 버그 수정
2024-06-15 20:49:58 +09:00
2abf2837d3 Merge pull request '커뮤니티 게시물' (#193) from test into main
Reviewed-on: #193
2024-06-11 12:13:47 +00:00
0ea985178e 커뮤니티 게시물
- 구매하지 않은 유료게시물은 15글자까지만 보이도록 수정
- 15글자가 넘지 않는 글은 절반만 보이도록 수정
2024-06-11 21:07:03 +09:00
422aa67af6 Merge pull request '커뮤니티 게시물' (#192) from test into main
Reviewed-on: #192
2024-06-11 11:55:05 +00:00
fc3b62ee37 커뮤니티 게시물
- 구매하지 않은 유료게시물은 15글자까지만 보이도록 수정
2024-06-11 20:45:00 +09:00
7fffab6985 Merge pull request '크리에이터 정산 - 입력된 비율로 계산' (#191) from test into main
Reviewed-on: #191
2024-06-11 08:07:22 +00:00
e2fa126a67 관리자 콘텐츠, 커뮤니티 정산
- 크리에이터 정산 추가데이터가 있으면 해당 데이터에 입력된 정산비율로 계산되도록 수정
2024-06-11 16:53:24 +09:00
be4a58d1c6 관리자 라이브 정산
- 크리에이터 정산 추가데이터가 있으면 해당 데이터에 입력된 정산비율로 계산되도록 수정
2024-06-11 16:37:09 +09:00
9396f70f85 관리자 라이브 정산
- 크리에이터 정산 추가데이터가 있으면 해당 데이터에 입력된 정산비율로 계산되도록 수정
2024-06-11 16:30:00 +09:00
7f5e138cf7 관리자
- 크리에이터 정산 추가데이터 insert, select 추가
2024-06-11 07:39:07 +09:00
5a4be3d2c1 Merge pull request '콘텐츠 상세' (#190) from test into main
Reviewed-on: #190
2024-06-07 10:30:06 +00:00
ffc146df06 콘텐츠 상세
- 활성화 되어 있는 태그만 가져오도록 수정
2024-06-07 18:01:57 +09:00
f39a7681db Merge pull request 'test' (#189) from test into main
Reviewed-on: #189
2024-06-04 03:39:23 +00:00
b5f37cb54b 시리즈 리스트
- 데이터 중복 버그 수정
2024-06-04 12:34:53 +09:00
6aaa4f8cf6 시리즈 리스트
- 데이터 중복 버그 수정
2024-06-04 12:29:46 +09:00
c60a7580ba Merge pull request 'test' (#188) from test into main
Reviewed-on: #188
2024-06-03 22:13:56 +00:00
3dca59685e 콘텐츠 리스트
- 대여/소장/매진 여부 추가
2024-05-30 14:22:06 +09:00
a529f05825 콘텐츠 리스트
- 대여/소장/매진 여부 추가
2024-05-30 14:05:36 +09:00
4cee0e3585 시리즈 리스트
- 새로운 콘텐츠가 올라온 순서대로 정렬
2024-05-30 13:17:39 +09:00
46da172806 관리자 시그니처 캔 리스트
- 정렬 추가
2024-05-30 13:08:18 +09:00
b72a2884f6 (크리에이터) 관리자 커뮤니티 정산
- 게시물 가격 추가
2024-05-30 12:56:46 +09:00
26b8dcee1b 크리에이터 커뮤니티 게시글 내용
- 전체 내용을 내려주도록 수정
2024-05-30 02:39:51 +09:00
0c0c4019aa 불필요한 !! 제거 2024-05-30 02:25:13 +09:00
97edb56edc Merge pull request 'test' (#187) from test into main
Reviewed-on: #187
2024-05-29 17:04:39 +00:00
78d13aee1a 크리에이터 관리자 - 시그니처 관리 페이지
- sortType을 필수 parameter에서 제거
2024-05-30 01:28:38 +09:00
e41ec1c91c 크리에이터 관리자 - 시그니처 관리 페이지
- 등록순, 캔 낮은 순, 캔 높은 순 정렬 추가
2024-05-30 01:01:54 +09:00
e3f65c8941 크리에이터 관리자 - 콘텐츠 수정
- 가격 수정시 변경사항 로그로 기록
2024-05-29 15:16:24 +09:00
f0aa0bc021 크리에이터 관리자 - 콘텐츠 수정
- 가격 수정 기능 추가
2024-05-29 13:35:39 +09:00
6ebca8d22b Merge pull request '관리자 - 라이브 리스트' (#186) from test into main
Reviewed-on: #186
2024-05-28 18:18:44 +00:00
8595af7173 관리자 - 라이브 리스트
- 참여자 수 데이터 추가
2024-05-29 03:00:53 +09:00
95371ad934 Merge pull request '(크리에이터)관리자 커뮤니티 게시물 정산' (#185) from test into main
Reviewed-on: #185
2024-05-28 16:54:44 +00:00
04f757a08c (크리에이터)관리자 커뮤니티 게시물 정산
- 페이징 추가
2024-05-29 01:22:02 +09:00
2c176825fd Merge pull request 'test' (#184) from test into main
Reviewed-on: #184
2024-05-28 16:09:51 +00:00
cf08d0d490 날짜 변환 확장함수
- 현재 타임존 수정 (ASIA/Seoul -> Asia/Seoul)
2024-05-29 00:25:57 +09:00
808030100f 관리자
- 커뮤니티 게시물 정산 페이징 적용
2024-05-29 00:13:58 +09:00
aa7c088504 관리자, 크리에이터 관리자
- 정산 리스트 결과값 리팩토링
2024-05-29 00:08:20 +09:00
0040a52169 관리자
- 커뮤니티 게시물 정산 API 추가
2024-05-28 17:51:48 +09:00
fae7de48d3 Merge pull request 'test' (#183) from test into main
Reviewed-on: #183
2024-05-27 08:28:27 +00:00
a7db9bed44 크리에이터 관리자
- 커뮤니티 게시물 정산 API 추가
2024-05-27 15:39:25 +09:00
21bf0910c5 크리에이터 관리자
- 커뮤니티 게시물 정산 API 추가
2024-05-27 15:25:50 +09:00
b8230646a2 Merge pull request '커뮤니티 게시글 유료화' (#182) from test into main
Reviewed-on: #182
2024-05-24 14:44:14 +00:00
fcfcb9845f 캔 사용내역 제목 수정
- 콘텐츠 구매 -> [콘텐츠 구매] 콘텐츠 제목
- 게시글 보기 -> [게시글 보기] 크리에이터 닉네임
2024-05-23 21:44:54 +09:00
059d5260a9 커뮤니티 게시글 구매 API 2024-05-23 21:36:11 +09:00
37853bdedd 커뮤니티 게시글 조회
- 내가 쓴 게시글은 무조건 보이도록 수정
2024-05-23 12:49:18 +09:00
87f22f45aa 커뮤니티 게시글 조회
- 응답값에 가격, 구매여부 추가
2024-05-23 12:32:22 +09:00
d241b4fa7a 커뮤니티 게시글 등록
- 유료 게시글 등록은 이미지가 있어야 등록이 가능하도록 수정
2024-05-23 01:28:30 +09:00
685a3998a2 커뮤니티 게시글 등록
- 유료 게시글 등록은 이미지가 있어야 등록이 가능하도록 수정
2024-05-23 00:50:20 +09:00
dad72b904f 커뮤니티 게시글 등록
- 유료 게시글 등록을 위해 가격 추가
2024-05-23 00:10:05 +09:00
43279541dd Merge pull request '콘텐츠별 누적정산' (#181) from test into main
Reviewed-on: #181
2024-05-21 06:37:51 +00:00
c406d21674 콘텐츠별 누적정산
- 취소된 주문은 반영되지 않도록 수정
2024-05-21 15:29:30 +09:00
b4791977c1 Merge pull request 'PG 심사를 위한 캔 충전 로직 추가' (#180) from test into main
Reviewed-on: #180
2024-05-20 06:38:40 +00:00
b0988cca70 PG 심사를 위한 캔 충전 로직 추가 2024-05-17 21:14:49 +09:00
81e1f7f6b1 PG 심사를 위한 캔 충전 로직 추가 2024-05-17 18:16:27 +09:00
ef917ecc25 Merge pull request '라이브 방 - 크리에이터 입장 가능 설정 추가' (#179) from test into main
Reviewed-on: #179
2024-05-14 12:09:53 +00:00
fc6916fc2d 크리에이터 채널 라이브 리스트
- 입장 가능한 라이브만 조회되도록 수정
2024-05-14 17:46:51 +09:00
c63949992f 라이브 추천/팔로잉 채널
- 입장 가능한 라이브가 있을 떄만 OnAir를 표시하도록 수정
2024-05-14 17:44:32 +09:00
39b27b2a17 라이브 방 생성, 시작
- 크리에이터가 입장 불가능한 라이브의 경우 크리에이터에게는 푸시발송이 되지 않도록 수정
2024-05-14 17:13:36 +09:00
ae4a790236 라이브 방 조회
- 조회하는 사람이 크리에이터이면 내가 만든 라이브와 크리에이터가 들어와도 된다고 설정한 방만 보이도록 수정
2024-05-14 16:00:54 +09:00
33130140fd 라이브 방
- 엔티티 크리에이터 입장 가능 플래그 추가
- 라이브 방 생성시 크리에이터 입장 가능 여부 추가
- 라이브 방 조회시 크리에이터 입장 가능 여부 추가
2024-05-14 15:16:04 +09:00
a93faad951 Merge pull request '룰렛 방식 수정' (#178) from test into main
Reviewed-on: #178
2024-05-10 18:00:40 +00:00
f9a8b431e0 룰렛 등록/수정
- 룰렛이 한 개 이상 활성화 되어 있으면 true 반환
2024-05-10 03:41:26 +09:00
9012dd14e2 룰렛
- 여러개가 동시에 활성화 될 수 있도록 수정
2024-05-10 00:26:13 +09:00
4a6330d016 룰렛
- 확률 수동설정 하도록 변경
- 결과 로직 서버에서 실행되도록 변경
- 활성화 된 룰렛 중 선택하여 돌리도록 변경
- 룰렛 결과를 후원메시지 리스트에서 볼 수 있도록 기록
2024-05-10 00:19:23 +09:00
985e5e2bea RouletteItem 에 percentage(확률) 기본값 0.0 -> 0.00으로 변경 2024-05-09 00:49:28 +09:00
4f77818406 룰렛 전체 가져오기 로직 추가
- RouletteItem 에 percentage(확률) 추가
2024-05-08 22:25:16 +09:00
e6dec42a00 불필요한 마이그레이션 로직 제거 2024-05-08 20:26:41 +09:00
fd001d24d3 Merge pull request '추천시리즈 API 추가' (#177) from test into main
Reviewed-on: #177
2024-05-07 10:34:10 +00:00
849fd9d984 추천시리즈 API 추가 2024-05-07 16:36:22 +09:00
9b5c0696b1 추천시리즈 API 추가 2024-05-07 16:10:21 +09:00
7aa5884797 Merge pull request '구글 인 앱 결제 검증코드 수정' (#176) from test into main
Reviewed-on: #176
2024-05-03 10:06:39 +00:00
a3442b8f2f 구글 인 앱 결제 검증코드 수정 2024-05-03 18:58:42 +09:00
5b237a1547 Merge pull request 'test' (#175) from test into main
Reviewed-on: #175
2024-05-03 06:08:55 +00:00
30793b75d5 탐색 - 남/여 크리에이터 리스트
- 표시되지 말아야 할 데이터 제외
2024-05-03 15:03:54 +09:00
0e3b4200d8 탐색 - 여 크리에이터 리스트
- 강조색 변경
2024-05-03 14:02:34 +09:00
2e37990d87 Merge pull request '탐색 - 남/여 크리에이터 리스트' (#174) from test into main
Reviewed-on: #174
2024-05-02 17:24:18 +00:00
6f0f53a9de 탐색 - 남/여 크리에이터 리스트
- 정렬순서에 숫자도 추가
2024-05-03 02:11:23 +09:00
dd07d724a8 Merge pull request 'test' (#173) from test into main
Reviewed-on: #173
2024-05-02 16:41:50 +00:00
6b138246a9 탐색
- 남/여 크리에이터 리스트 추가
- 인기크리, 새로시작 외 나머지 섹션 제거
2024-05-03 01:34:30 +09:00
82f49667a9 탐색
- 남/여 크리에이터 리스트 추가
- 인기크리, 새로시작 외 나머지 섹션 제거
2024-05-03 01:24:33 +09:00
54072412f3 탐색
- 남/여 크리에이터 리스트 추가
- 인기크리, 새로시작 외 나머지 섹션 제거
2024-05-03 01:06:57 +09:00
047446ba3a 탐색
- 남/여 크리에이터 리스트 추가
- 인기크리, 새로시작 외 나머지 섹션 제거
2024-05-03 00:34:56 +09:00
6d6e1dd9ea 탐색
- 남/여 크리에이터 리스트 추가
- 인기크리, 새로시작 외 나머지 섹션 제거
2024-05-03 00:22:35 +09:00
03ce8618e7 Merge pull request '관리자 시그니처 설정' (#172) from test into main
Reviewed-on: #172
2024-05-02 07:13:21 +00:00
6cba58c301 관리자 시그니처 설정
- 재생 시간, 캔 유효성 검사 추가
2024-05-02 15:57:32 +09:00
a168d90dd3 관리자 시그니처 설정
- 재생 시간, 캔 유효성 검사 추가
2024-05-02 15:45:41 +09:00
db1a7a7fd6 Merge pull request '시그니처 후원 시간 추가' (#171) from test into main
Reviewed-on: #171
2024-05-02 06:23:22 +00:00
f4d9fa69e4 크리에이터 관리자 시그니처 설정
- 재생 시간, 캔 유효성 검사 추가
2024-05-01 19:13:27 +09:00
1a89177ecc 시그니처 후원
- 재생 시간, 캔 유효성 검사 추가
2024-05-01 15:16:48 +09:00
382de101dd 시그니처 후원
- 재생 시간 추가
2024-05-01 13:35:51 +09:00
cf03eae4ec 크리에이터 관리자 시그니처
- 재생 시간 등록/수정 추가
2024-05-01 12:23:09 +09:00
36a82d7f53 Merge pull request '시리즈, 시리즈 콘텐츠' (#170) from test into main
Reviewed-on: #170
2024-04-30 14:00:01 +00:00
b775781fd7 시리즈, 시리즈 콘텐츠
- 정렬순서 추가
2024-04-30 22:53:01 +09:00
3a34401113 Merge pull request '시리즈 상세' (#169) from test into main
Reviewed-on: #169
2024-04-30 09:44:55 +00:00
2acf723c86 시리즈 상세
- 대여가격 70% -> 60%로 수정
2024-04-30 18:39:08 +09:00
9927268330 Merge pull request '구글 인 앱 결제' (#168) from test into main
Reviewed-on: #168
2024-04-30 08:24:03 +00:00
bbb193a787 구글 인 앱 결제
- consumeWithRetry 조건에서 acknowledge 제거
2024-04-30 17:17:33 +09:00
c45c97e29d Merge pull request '시리즈' (#167) from test into main
Reviewed-on: #167
2024-04-26 18:51:10 +00:00
976aeb0f75 시리즈 콘텐츠 리스트
- offset, limit 추가
2024-04-27 00:17:55 +09:00
3b807543b7 시리즈 리스트 상세 API
- 콘텐츠 리스트 추가
2024-04-26 21:18:13 +09:00
0350f86322 시리즈 리스트 상세 API 2024-04-26 04:27:36 +09:00
007be6b1ff 시리즈 리스트 상세 API 2024-04-26 04:19:21 +09:00
3ec0cf4fac 시리즈 리스트 상세 API 2024-04-26 03:57:45 +09:00
bb8dda6da0 시리즈 리스트 상세 API 2024-04-26 03:34:04 +09:00
c599e2ee00 시리즈 리스트 상세 API 2024-04-26 03:26:55 +09:00
1b49e3837c 시리즈 리스트 상세 API 2024-04-26 03:05:30 +09:00
86e15d4155 시리즈 리스트 상세 API 2024-04-26 02:31:44 +09:00
6db8013e34 시리즈 리스트 상세 API 2024-04-26 00:39:37 +09:00
b10af9d9f1 시리즈 리스트 조회 API
- offset, limit 적용
2024-04-25 21:55:47 +09:00
7b04803aa0 시리즈 리스트 조회 API
- DTO 조회시 EnumCollection 부분의 문제로 series 필드 전체 조회로 변경
2024-04-25 04:31:36 +09:00
d3b9fd7d78 시리즈 리스트 조회 API
- 필요한 필드만 조회하도록 수정
2024-04-25 03:55:12 +09:00
c077f7322d 시리즈 리스트 조회 API
- 필요한 필드만 조회하도록 수정
2024-04-25 03:47:29 +09:00
c95b3db6eb DTO로 조회를 했을 때 org.hibernate.QueryException: not an entity 오류나던 부분 series 모든 필드 조회로 변경 2024-04-25 03:22:05 +09:00
0d709742ed org.hibernate.QueryException: not an entity 수정 2024-04-25 03:03:40 +09:00
246136b1ad . 2024-04-25 02:25:16 +09:00
44ffe20e88 . 2024-04-25 02:14:56 +09:00
290c4600fa 크리에이터 채널 - 시리즈 리스트 추가 2024-04-25 00:20:09 +09:00
84007e1b72 시리즈 리스트 API 2024-04-24 23:54:39 +09:00
9bc1c610ac 시리즈에 속하지 않은 콘텐츠 찾기 API 추가 2024-04-24 14:07:09 +09:00
613298bdea 시리즈 콘텐츠 조회
- 해당 시리즈의 콘텐츠만 조회되도록 수정
2024-04-24 12:51:19 +09:00
0c325185fb 시리즈 콘텐츠 추가
- 시리즈 콘텐츠를 시리즈에 추가할 때 set 타입으로 변환하여 중복 제거
2024-04-24 01:35:23 +09:00
0acf98aef3 시리즈 콘텐츠 제거
- removeIf를 이용하여 콘텐츠를 제거했지만 제거되지 않던 버그 수정
2024-04-24 00:33:23 +09:00
2847cffa76 시리즈 콘텐츠 제거
- removeIf를 이용하여 콘텐츠를 제거했지만 제거되지 않던 버그 수정
2024-04-24 00:19:44 +09:00
05c293ed6d 시리즈 상세
- 커버이미지 뒤에 !!가 잘못들어가던 버그 수정
2024-04-23 18:30:17 +09:00
3f86862ae9 시리즈 생성
- keyword 중복 제거
2024-04-22 23:29:06 +09:00
9a80979a42 시리즈 생성
- keyword 중복 제거
2024-04-22 22:55:14 +09:00
091ed270b0 시리즈 수정
- 시리즈 수정시 연재요일이 수정되지 않는 버그 수정(연재요일을 저장하는 필드에 Cascade 조건 추가)
2024-04-22 21:04:13 +09:00
92c19092fe 시리즈 수정
- 시리즈 수정시 연재요일이 수정되지 않는 버그 수정(연재요일을 저장하는 필드에 Cascade 조건 추가)
2024-04-22 21:00:36 +09:00
42e55c0617 시리즈 수정
- 완결여부 수정 기능 추가
2024-04-22 20:35:25 +09:00
2792caa387 시리즈
- 시리즈 수정시 연재요일이 수정되지 않는 버그 수정(연재요일을 저장하는 필드에 Cascade 조건 추가)
2024-04-22 19:57:41 +09:00
a438aae9bc 크리에이터 관리자 - 시리즈 등록
- 키워드 등록시 중복등록이 되지 않도록 수정
2024-04-22 19:49:43 +09:00
5f607e2b75 크리에이터 관리자 - 시리즈 리스트
- 응답값에 상세 내용 추가(소개, 연재요일, 장르, 연령제한여부, 완결여부, 활성화 여부, 작가, 제작사)
2024-04-22 19:31:57 +09:00
7333b5d755 시리즈 수정
- validate에 isActive추가
2024-04-22 18:49:09 +09:00
c64a315226 Merge pull request 'test' (#166) from test into main
Reviewed-on: #166
2024-04-18 16:40:55 +00:00
1d6c74162e 구글 인 앱 결제
- 재시도 횟수 3으로 수정
2024-04-19 01:36:49 +09:00
8f84483826 구글 인 앱 결제
- acknowledge 과정 추가
2024-04-19 01:00:09 +09:00
a4cafca6ab Merge pull request 'test' (#165) from test into main
Reviewed-on: #165
2024-04-18 10:02:45 +00:00
69b46d791f 관리자 API - 시리즈 장르 수정 API
- 시리즈 삭제시 genre에 _deleted 접미사 추가
2024-04-18 13:35:28 +09:00
4512aed44a 관리자 API - 시리즈 장르 등록 API
- 연령제한 파라미터 추가
2024-04-18 13:33:39 +09:00
0bf2b1b4ae 관리자 API
- 시리즈 장르 리스트 조회 API 추가
2024-04-18 12:49:15 +09:00
0185c09d55 크리에이터 관리자 API
- 시리즈 콘텐츠 삭제 API 추가
2024-04-17 19:18:36 +09:00
02e6e77453 크리에이터 관리자 API
- 기존 콘텐츠를 시리즈에 추가하는 API 추가
2024-04-17 18:52:10 +09:00
b849de00dc 크리에이터 관리자 API
- 시리즈 콘텐츠 API 추가
2024-04-17 18:08:40 +09:00
50f02892e3 크리에이터 관리자 API
- 시리즈 상세 API 추가
2024-04-17 17:04:15 +09:00
2a9eaead83 크리에이터 관리자 API
- 시리즈 리스트 API 추가
2024-04-17 00:02:26 +09:00
3b008155e1 크리에이터 관리자 API
- 시리즈 수정/삭제 API 추가
2024-04-16 23:13:05 +09:00
501fa36fad 크리에이터 관리자 API
- 시리즈 생성 API 추가
2024-04-16 19:43:57 +09:00
4089590bdf 안드로이드 인 앱 결제 - consume 재시도 5번으로 변경 2024-04-16 10:50:20 +09:00
46284a0660 Merge pull request 'test' (#164) from test into main
Reviewed-on: #164
2024-04-15 12:31:42 +00:00
c40220f766 안드로이드 인 앱 결제 - 방어코드 추가 2024-04-15 21:24:39 +09:00
dd3c1f45c8 관리자
- 시리즈 장르 등록, 수정, 삭제 API 추가

크리에이터 관리자
- 시리즈 장르 조회 API 추가
2024-04-15 20:24:50 +09:00
05df86e15a Merge pull request 'test' (#163) from test into main
Reviewed-on: #163
2024-04-09 14:40:33 +00:00
4e84678a3c 라이브 중 리스트 - 정렬 조건 수정
- 라이브 랭킹 값이 없는 경우 0으로 처리해서 정렬 되도록 수정
2024-04-09 23:22:53 +09:00
2ca4204775 크리에이터 채널 - 팔로워 수
- 탈퇴한 회원은 카운팅에서 제외
2024-04-09 23:09:22 +09:00
8b433027e2 Merge pull request 'test' (#162) from test into main
Reviewed-on: #162
2024-04-09 13:27:24 +00:00
476e4e8eb1 라이브 중 리스트
- offset, limit 추가
2024-04-09 16:49:43 +09:00
205deea88a 라이브 중 리스트
- 리스트 정렬 조건 수정
- 1. 15분 동안 받은 캔
- 2. 15분 동안 참여한 참여자
- 3. 최근 시작한 라이브
- 4. 최근 가입한 크리에이터
2024-04-09 16:24:49 +09:00
5bd4ff7610 Merge pull request '결제 테이블에 구글결제의 경우 orderId 추가' (#161) from test into main
Reviewed-on: #161
2024-04-05 03:10:00 +00:00
bfc78b0ef9 결제 테이블에 구글결제의 경우 orderId 추가 2024-04-05 11:55:46 +09:00
d693c397ea Merge pull request '.' (#160) from test into main
Reviewed-on: #160
2024-04-03 06:49:36 +00:00
3a403e5192 . 2024-04-03 15:42:18 +09:00
1d8d1ec9a5 Merge pull request 'test' (#159) from test into main
Reviewed-on: #159
2024-04-03 06:27:26 +00:00
aad4f8dbb5 test 2024-04-03 15:22:56 +09:00
8d185c274a test 2024-04-03 15:15:50 +09:00
3b59d6c546 크리에이터 관리자 라이브 정산
- 본인 것 외에 보이지 않도록 수정
2024-04-03 15:08:48 +09:00
5e491f11ee Merge pull request '크리에이터 관리자 라이브 정산' (#158) from test into main
Reviewed-on: #158
2024-04-03 03:45:31 +00:00
d33ed42853 크리에이터 관리자 라이브 정산
- 인 앱 결제 캔과 pg 결제 캔을 동시에 사용된 경우 결제 건 수가 2배로 잡히는 버그 수정
2024-04-03 12:39:26 +09:00
7cedea06ac Merge pull request '캔 사용' (#157) from test into main
Reviewed-on: #157
2024-04-01 12:42:44 +00:00
8e5ed5bbc1 캔 사용
- 구글 인 앱 결제한 캔을 사용하면 pg캔이 사용된 것처럼 보이는 것 수정
2024-04-01 21:32:48 +09:00
2e5f750e50 Merge pull request 'test' (#156) from test into main
Reviewed-on: #156
2024-04-01 10:20:09 +00:00
f8b02e5964 관리자 - 캔 충전현황
- 응답값에 화폐 locale 추가
2024-04-01 19:07:18 +09:00
67457967c0 관리자 - 캔 충전현황
- 응답값에 화폐 locale 추가
2024-04-01 18:41:13 +09:00
b3b7ef90e8 불필요한 print 제거 2024-03-30 02:34:51 +09:00
aa4af439be test 2024-03-30 01:48:25 +09:00
20289cad10 Merge pull request '콘텐츠 상세' (#155) from test into main
Reviewed-on: #155
2024-03-29 10:10:04 +00:00
b03d424e2f 콘텐츠 상세
- 차단된 사용자는 댓글을 쓰지 못하도록 수정
2024-03-29 18:58:10 +09:00
e0d64c31c7 Merge pull request '콘텐츠 상세' (#154) from test into main
Reviewed-on: #154
2024-03-29 08:18:15 +00:00
2dfaf4ebae 콘텐츠 상세
- 차단된 사용자여도 이미 구매한 콘텐츠는 접근할 수 있도록 수정
2024-03-29 17:09:00 +09:00
8c1b95dc97 Merge pull request '구글 인 앱구매 검증' (#153) from test into main
Reviewed-on: #153
2024-03-28 17:13:57 +00:00
ccf8b0220e 구글 인 앱구매 검증
- 충전 이벤트가 private method consumeWithRetry 에 구현되어 있어서 실행되지 않던 현상 수정
2024-03-29 01:56:24 +09:00
fb5641343e Merge pull request 'test' (#152) from test into main
Reviewed-on: #152
2024-03-28 06:31:43 +00:00
6b7b3efcb1 관리자, 크리에이터 관리자 콘텐츠 리스트
- 한정판 총 개수 및 남은 개수 추가
2024-03-27 19:29:58 +09:00
b929b51525 콘텐츠 등록
- 불필요한 파라미터 제거
2024-03-27 18:17:56 +09:00
327f7ad341 콘텐츠 상세
- 한정판 응답값 추가
2024-03-26 20:04:20 +09:00
2d1f333095 한정판 콘텐츠 주문 기능 추가 2024-03-26 17:44:26 +09:00
17c5bade83 콘텐츠 등록
- 한정판 기능을 위해 개수 제한 추가
2024-03-26 15:04:03 +09:00
87765941eb Merge pull request '구글 인 앱 결제 검증' (#151) from test into main
Reviewed-on: #151
2024-03-22 20:10:01 +00:00
fbc90b69d9 구글 인 앱 결제 검증
- 재시도 로직 추가
2024-03-23 05:04:13 +09:00
1809862c16 Merge pull request '구글 인 앱 결제 처리과정 축소' (#150) from test into main
Reviewed-on: #150
2024-03-22 15:27:25 +00:00
d72055c7d3 구글 인 앱 결제 처리과정 축소
- 기존에는 충전과 검증 API가 분리되어 있었지만 PG와 다르게 chargeId가 필요하지 않아 충전 API에서 구글서버에 검증을 하도록 수정
2024-03-23 00:20:02 +09:00
300f784f7d Merge pull request '구글 인 앱 결제 검증 과정 try/catch로 예외 처리' (#149) from test into main
Reviewed-on: #149
2024-03-22 11:59:37 +00:00
2e9a187935 구글 인 앱 결제 검증 과정 try/catch로 예외 처리 2024-03-22 20:38:08 +09:00
67a045eae6 Merge pull request '구글 인 앱 결제 검증 수정' (#148) from test into main
Reviewed-on: #148
2024-03-22 11:36:35 +00:00
e3f0145264 구글 인 앱 결제 검증 수정
- 현재 : 구매상태 확인 후 충전 완료 처리
- 수정 : 구매상태와 소비상태를 확인 후 소비되지 않았으면 소비 후 충전 완료 처리
2024-03-22 20:22:35 +09:00
2a79903a28 Merge pull request 'test' (#147) from test into main
Reviewed-on: #147
2024-03-22 10:08:00 +00:00
34b5dcccfc 구글 인 앱 결제 검증 수정
- 현재 : 구매상태 확인 후 충전 완료 처리
- 수정 : 구매상태와 소비상태를 확인 후 소비되지 않았으면 소비 후 충전 완료 처리
2024-03-22 19:02:15 +09:00
4023476685 구글 인 앱 결제 검증 수정
- 현재 : 구매상태 확인 후 충전 완료 처리
- 수정 : 구매상태와 소비상태를 확인 후 소비되지 않았으면 소비 후 충전 완료 처리
2024-03-22 18:54:03 +09:00
df690dae0e 구글 인 앱 결제 검증 수정
- 현재 : 구매상태 확인 후 충전 완료 처리
- 수정 : 구매상태와 소비상태를 확인 후 소비되지 않았으면 소비 후 충전 완료 처리
2024-03-22 18:42:47 +09:00
d17f76261e 구글 인 앱 결제 검증 수정
- 현재 : 구매상태 확인 후 충전 완료 처리
- 수정 : 구매상태와 소비상태를 확인 후 소비되지 않았으면 소비 후 충전 완료 처리
2024-03-22 18:32:50 +09:00
d3222ce083 Merge pull request '관리자 캔 충전현황' (#146) from test into main
Reviewed-on: #146
2024-03-21 15:56:02 +00:00
71dffc4c0b 관리자 캔 충전현황
- 충전금액 표시 로직 수정
- PG일 떄 can 테이블에 표시된 가격을 가져오고 나머지는 payment 테이블에 있는 가격을 가져오도록 수정
2024-03-22 00:49:46 +09:00
406a421742 Merge pull request '구글 인 앱 결제 검증 코드 수정' (#145) from test into main
Reviewed-on: #145
2024-03-21 15:22:34 +00:00
acb6eb237c 구글 인 앱 결제 검증 코드 수정
- @Transactional 추가
2024-03-21 23:59:52 +09:00
10bf728faf Merge pull request '구글 인 앱 결제 검증 코드 수정' (#144) from test into main
Reviewed-on: #144
2024-03-21 14:37:09 +00:00
663b50654d 구글 인 앱 결제 검증 코드 수정
- AndroidPublisher Spring Bean으로 등록된 것을 사용하도록 설정 변경
2024-03-21 23:30:27 +09:00
607617747c Merge pull request '구글 인 앱 결제 검증 코드 수정' (#143) from test into main
Reviewed-on: #143
2024-03-21 12:47:51 +00:00
7c7f2e0f2c 구글 인 앱 결제 검증 코드 수정
- oauth 테스트를 위해 android publisher 생성 코드 chargeservice 로 가져옴
2024-03-21 19:35:53 +09:00
f0a69eb1a2 Merge pull request 'test' (#142) from test into main
Reviewed-on: #142
2024-03-21 07:45:02 +00:00
1c8f5ef7ac 구글 인 앱 결제 검증 코드 추가 2024-03-21 16:10:19 +09:00
69331edabb 관리자 콘텐츠 리스트
- group_concat 제거
2024-03-19 22:16:07 +09:00
7c1c4b907b 관리자 콘텐츠 리스트
- group_concat SEPARATOR 제거
2024-03-19 22:04:40 +09:00
788a121994 관리자 콘텐츠 리스트
- group_concat 닫는 괄호 추가
2024-03-19 21:58:15 +09:00
eb5710837b 관리자 콘텐츠 리스트
- 반복문으로 하던 태그, 커버이미지 작업을 db쪽으로 넘겨서 처리
2024-03-19 20:37:26 +09:00
6b307a6e17 Merge pull request 'test' (#141) from test into main
Reviewed-on: #141
2024-03-13 11:28:13 +00:00
c3e18d658c 시그니처 수정
- 캔 수정 추가
2024-03-13 19:10:21 +09:00
b8d422f45c 시그니처 조회
- 19금 여부 추가
2024-03-13 18:59:57 +09:00
5732ecfbfa 시그니처
- 19금 여부 추가
2024-03-13 16:21:10 +09:00
08d08a934a Merge pull request 'test' (#140) from test into main
Reviewed-on: #140
2024-03-12 06:20:02 +00:00
536923c00b 시그니처 조회
- 조회하는 크리에이터 본인 것만 조회하도록 수정
2024-03-12 01:00:17 +09:00
28124fc059 @Transactional 추가 2024-03-12 00:56:49 +09:00
48706f0bd5 . 2024-03-12 00:40:39 +09:00
e06b4c3179 크리에이터 관리자
- 시그니처 조회/등록/수정/삭제 API 추가
2024-03-11 22:55:44 +09:00
e4d251a0b3 시그니처 캔
- repository 이름 변경
2024-03-11 17:28:11 +09:00
c500c12668 Merge pull request 'test' (#139) from test into main
Reviewed-on: #139
2024-03-08 13:40:27 +00:00
3d581b1677 시그니처 캔
- isActive default true
2024-03-07 23:57:05 +09:00
400d281cf6 시그니처 캔 등록
- DB에 저장하지 않던 버그 수정
2024-03-07 23:49:03 +09:00
64c6cc05de 시그니처 캔 수정
- 이미지, 활성화 데이터 필수에서 제외
2024-03-07 23:32:42 +09:00
6d40ef6f4d 시그니처 캔 리스트
- 총 개수 추가
2024-03-07 21:34:07 +09:00
71937ce89c 후원
- 결과값에 시그니처 이미지 URL 데이터 추가
2024-03-07 19:38:25 +09:00
0d402b608c 관리자 - 시그니처 캔 등록/수정 API 2024-03-07 17:44:38 +09:00
c66421b45d 라이브 수정
- 메뉴판 활성화/비활성화 로직 수정
2024-03-07 05:39:16 +09:00
5609bdb6f4 메뉴판
- 활성화 여부를 표시하는 isActive 추가
2024-03-07 04:34:41 +09:00
bbfcf5fd4f 라이브 종료
- 메뉴판 비활성화
2024-03-06 13:45:32 +09:00
97d44104e1 메뉴판 가져오기
- 메뉴판이 없을 때 예외처리 수정
2024-03-06 13:43:03 +09:00
ae04b22b2a 라이브 만들기, 수정
- 메뉴판 만들기, 수정 기능 추가
2024-03-06 01:40:25 +09:00
31e33d49df 라이브 메뉴판
- 불필요한 API 제거
2024-03-06 00:55:37 +09:00
59a035e5c2 라이브 정보 조회 API
- 응답에 메뉴판 정보 추가
2024-03-05 22:20:40 +09:00
945fd5d5a3 메뉴판 프리셋 API 2024-03-05 15:40:24 +09:00
62060adeba Merge pull request '채널 후원 랭킹' (#138) from test into main
Reviewed-on: #138
2024-02-29 10:28:51 +00:00
157936acef 채널 후원 랭킹
- 회원 탈퇴한 사람 제외
2024-02-29 19:17:35 +09:00
b2fc75edb8 Merge pull request '룰렛 정렬 순서 수정' (#137) from test into main
Reviewed-on: #137
2024-02-27 17:29:36 +00:00
72b48e136f 룰렛 정렬 순서 수정
- id 순서대로 정렬
2024-02-28 02:21:33 +09:00
a999dd2085 Merge pull request 'test' (#136) from test into main
Reviewed-on: #136
2024-02-27 16:16:30 +00:00
2f08149e48 룰렛 정렬 순서 수정
1순위 - 활성화 된 상태
2순위 - id
2024-02-28 01:09:42 +09:00
48d5f1674f 크리에이터 콘텐츠 조회
- 재생시간이 있는 콘텐츠만 조회되도록 수정
2024-02-27 17:19:49 +09:00
49f95ab100 Merge pull request '회원테이블에 adid 추가' (#135) from test into main
Reviewed-on: #135
2024-02-27 05:49:47 +00:00
ede4c465c6 회원테이블에 adid 추가 2024-02-26 20:02:44 +09:00
1a84d5b30c Merge pull request 'test' (#134) from test into main
Reviewed-on: #134
2024-02-24 20:35:57 +00:00
0826db0a5b 룰렛 만들기
- 만드는 룰렛이 활성화하면 나머지 룰렛을 비활성화 하도록 수정
2024-02-24 02:25:46 +09:00
9f3a25bd7d 룰렛 업데이트
- 룰렛 프리셋 중 하나라도 활성화 되어 있으면 결과값을 활성화로 리턴하도록 수정
2024-02-23 17:47:09 +09:00
348936a67e 라이브 정보 가져오기
- 룰렛 활성화 체크 로직 수정
2024-02-23 16:53:26 +09:00
bcd094c5dd 활성화된 룰렛 조회 로직 수정 2024-02-23 16:47:49 +09:00
0e9863050f @Indexed 추가 2024-02-23 16:31:57 +09:00
c097cb54f1 룰렛 업데이트
- 업데이트 하는 룰렛이 활성화 되는 경우 다른 룰렛 모두 비활성화
2024-02-23 14:20:02 +09:00
01aeb8e759 라이브 종료
- 모든 룰렛 비활성화
2024-02-23 14:12:30 +09:00
b163427514 디버깅용 print 제거 2024-02-23 14:04:23 +09:00
7328283b6b 디버깅용 print 추가 2024-02-22 18:34:07 +09:00
4fb97a7c95 디버깅용 print 제거 2024-02-22 18:27:56 +09:00
3e89025fd9 룰렛 프리셋 API 추가
- 룰렛 만들기
- 룰렛 업데이트
- 룰렛 프리셋 전체 가져오기
2024-02-22 18:01:19 +09:00
183f8098be findBy -> findAllBy 2024-02-22 17:26:11 +09:00
6c9d57b18a 디버깅용 print 추가 2024-02-22 16:11:06 +09:00
eb552f01f0 룰렛 프리셋 API 추가
- 룰렛 만들기
- 룰렛 업데이트
- 룰렛 프리셋 전체 가져오기
2024-02-22 16:04:06 +09:00
95717824c6 룰렛 프리셋 API 추가
- 룰렛 만들기
- 룰렛 업데이트
- 룰렛 프리셋 전체 가져오기
2024-02-22 16:03:37 +09:00
f369650711 룰렛 프리셋 API 추가
- 룰렛 만들기
- 룰렛 업데이트
- 룰렛 프리셋 전체 가져오기
2024-02-22 12:39:49 +09:00
81a94082aa redis ssl true 2024-02-19 14:26:42 +09:00
dd8634c47b redis ssl false 2024-02-19 12:26:03 +09:00
f577e137c2 redis ssl true 2024-02-19 12:02:32 +09:00
3b65050632 Merge pull request 'redis ssl false' (#133) from test into main
Reviewed-on: #133
2024-02-17 13:00:25 +00:00
728b00abdb redis ssl false 2024-02-17 21:55:32 +09:00
d0df31674c Merge pull request 'test' (#132) from test into main
Reviewed-on: #132
2024-02-17 12:44:52 +00:00
b6cdeee548 콘텐츠 테마 API 권한 수정 2024-02-17 21:00:13 +09:00
70255273ed redis ssl true 2024-02-17 00:24:32 +09:00
1fe88402e2 Merge pull request 'test' (#131) from test into main
Reviewed-on: #131
2024-02-14 07:12:41 +00:00
5890f9932b 테마별 콘텐츠 가져오기 API
- 총 개수 추가
2024-02-14 00:02:17 +09:00
88d326023b 테마별 콘텐츠 가져오기 API
- 정렬기준 추가
2024-02-13 23:11:51 +09:00
144bb4945b 테마별 콘텐츠 가져오기 API 추가 2024-02-13 15:56:36 +09:00
d621e271a0 커뮤니티 등록시 알림문구
- 새 글이 등록되었습니다. 로 변경
2024-02-13 03:25:30 +09:00
1d904c5cde 콘텐츠 리스트 - 가격과 시간 추가 2024-02-13 03:17:34 +09:00
67097696e6 Merge pull request '커뮤니티 게시물 시간' (#130) from test into main
Reviewed-on: #130
2024-02-12 08:14:24 +00:00
d32f503633 커뮤니티 게시물 시간
- 1개월 -> 1분 으로 표시되는 오류 수정
2024-02-12 17:09:05 +09:00
8e7e77067a Merge pull request 'test' (#129) from test into main
Reviewed-on: #129
2024-02-12 07:53:26 +00:00
537cdbb307 커뮤니티 게시물 시간
- 1개월 -> 1분 으로 표시되는 오류 수정
2024-02-12 16:08:25 +09:00
18f53df9f8 전체 새로운 콘텐츠
- 차단한 크리에이터의 콘텐츠를 차단 당한 유저에게 보이지 않도록 수정
2024-02-09 03:42:18 +09:00
9899390b61 Merge pull request '관리자 콘텐츠 리스트, 수정' (#128) from test into main
Reviewed-on: #128
2024-02-08 18:19:50 +00:00
dcd26308d3 관리자 콘텐츠 리스트
- 응답값에 테마 id 추가
2024-02-09 03:06:50 +09:00
0f1ed03caf 관리자 콘텐츠 수정
- 테마 수정 기능 추가
2024-02-09 01:44:03 +09:00
80c476a908 Merge pull request '관리자 콘텐츠 리스트' (#127) from test into main
Reviewed-on: #127
2024-02-08 14:45:54 +00:00
c935cdd8ee 관리자 콘텐츠 리스트
- 댓글여부 추가
2024-02-08 23:39:09 +09:00
59da1d6e49 Merge pull request '카테고리 콘텐츠' (#126) from test into main
Reviewed-on: #126
2024-02-07 13:33:37 +00:00
c9bb3aa489 카테고리 콘텐츠
- 특정 카테고리를 선택할 때와 전체 조회시 로직 분리
2024-02-07 22:25:19 +09:00
5aef7dac33 Merge pull request 'test' (#125) from test into main
Reviewed-on: #125
2024-02-07 09:39:09 +00:00
aea1f1889e 크리에이터 관리자 카테고리 콘텐츠 검색
- 검색 조건 수정
2024-02-07 09:38:52 +09:00
3df0b1fcec 크리에이터 관리자 카테고리 콘텐츠 검색
- 검색 조건 수정
2024-02-07 09:12:37 +09:00
113d29fab0 크리에이터 관리자 카테고리 콘텐츠 리스트
- 페이징 처리, 정렬순서 최근에 추가한 콘텐츠가 먼저 나오도록 수정
2024-02-07 06:21:03 +09:00
ebb969c039 크리에이터 관리자 카테고리 콘텐츠 검색
- 해당 카테고리에 속한 콘텐츠는 검색되지 않도록 수정
2024-02-07 06:08:44 +09:00
9a30dd6de5 크리에이터 관리자 카테고리 콘텐츠 검색
- 해당 카테고리에 속한 콘텐츠는 검색되지 않도록 수정
2024-02-07 05:36:08 +09:00
525f837e21 크리에이터 관리자 카테고리 콘텐츠 검색
- 해당 카테고리에 속한 콘텐츠는 검색되지 않도록 수정
2024-02-07 05:11:33 +09:00
db0e526896 크리에이터 관리자 카테고리 콘텐츠 리스트 API
- 커버이미지 추가
2024-02-07 04:15:01 +09:00
e76eed7274 크리에이터 관리자 카테고리 콘텐츠 리스트 API
- 전체 개수 추가
2024-02-07 00:26:17 +09:00
286629836c 크리에이터 관리자 카테고리 콘텐츠 리스트 API 추가 2024-02-06 23:59:51 +09:00
354c8c3d4a 크리에이터 관리자 콘텐츠 리스트
- 불필요한 로직 제거
2024-02-06 22:42:40 +09:00
d3ffa1d40a 콘텐츠 리스트 - 정렬 조건 추가 2024-02-06 22:06:05 +09:00
a6a279837d 카테고리 순서 변경 API 추가 2024-02-06 21:32:29 +09:00
e3cf7fbfa0 크리에이터 관리자 카테고리 리스트
- 활성화된 것만 가져오도록 수정
2024-02-06 21:24:32 +09:00
7bafb6ba0d 크리에이터 관리자 콘텐츠 리스트
- 카테고리 조회 추가
2024-02-06 16:11:57 +09:00
faf7aa06b6 Merge pull request 'test' (#124) from test into main
Reviewed-on: #124
2024-02-05 07:03:45 +00:00
738d7fba2a 라이브 생성, 시작
- 푸시 발송 대상 조회 로직 수정
2024-02-05 15:50:33 +09:00
0aaac97915 라이브 생성, 시작
- 푸시 발송 대상 조회 로직 수정
2024-02-05 15:31:34 +09:00
38ef6e5583 Merge pull request 'test' (#123) from test into main
Reviewed-on: #123
2024-02-05 02:12:10 +00:00
720ee63505 전체 콘텐츠 리스트
- 카테고리를 지정하지 않을 때 조회되지 않는 버그 수정
2024-02-02 21:52:10 +09:00
e1ea1f14a5 콘텐츠 정산
- 하나의 콘텐츠를 구매할 때 PG, APPLE_IAP 와 같이 2개 이상의 다른 캔으로 결제 했을 때 2개 이상 구매된 것으로 표시되는 버그 수정
2024-02-02 21:48:11 +09:00
6efab40a85 콘텐츠 카테고리
- 정렬순서를 표시하는 orders 추가
2024-02-02 20:55:28 +09:00
22f274fd32 콘텐츠 조회
- 조회 조건에 카테고리 추가
2024-02-02 20:03:23 +09:00
c50f24b755 콘텐츠 카테고리 만들기, 수정 API
- 카테고리명 검증 추가
2024-02-01 22:56:54 +09:00
b8b387c33d 콘텐츠 카테고리
- 카테고리 수정 API 추가
2024-02-01 22:49:40 +09:00
a2e6d09ee8 콘텐츠 카테고리
- 카테고리 조회 API 추가
2024-02-01 20:00:48 +09:00
8be2ec9319 콘텐츠 카테고리
- 삭제 API 추가
2024-02-01 19:48:59 +09:00
34c08d4345 콘텐츠 카테고리
- 등록 API 추가
2024-02-01 19:37:15 +09:00
c0b15b5d94 Merge pull request '콘텐츠 업로드' (#122) from test into main
Reviewed-on: #122
2024-01-30 03:45:31 +00:00
dac5858541 콘텐츠 업로드
- 무료 콘텐츠의 경우 미리듣기 파일을 생성하지 않도록 수정
2024-01-30 12:34:50 +09:00
2cfc067ea1 Merge pull request '콘텐츠 전체 리스트' (#121) from test into main
Reviewed-on: #121
2024-01-29 09:00:23 +00:00
11dcb399f6 콘텐츠 전체 리스트
- 정렬 조건을 추가하여 콘텐츠가 중복으로 보이는 버그 수정
2024-01-29 17:53:29 +09:00
a91db4f956 Merge pull request '콘텐츠 상단 고정 기능 추가' (#120) from test into main
Reviewed-on: #120
2024-01-29 02:45:41 +00:00
653f1f8d58 콘텐츠 리스트
- 커버이미지 URL 잘못 생성되던 버그 수정
2024-01-29 11:39:21 +09:00
bf634b09db 예약콘텐츠 배포
- 컨버팅 작업이 완료되지 않은 콘텐츠가 해당 시간이 되서 배포되는 버그 수정
2024-01-29 11:20:24 +09:00
89beba25b6 관라자 콘텐츠 리스트
- 정렬 순서 id -> releaseDate 내림차순으로 변경
2024-01-29 11:15:12 +09:00
67522804d9 크리에이터 채널
- 콘텐츠, 라이브 개수 최대 4개에서 3개로 변경
2024-01-29 00:50:56 +09:00
da1c096b45 콘텐츠 조회
- 고정 콘텐츠를 나타내는 플래그 추가
2024-01-29 00:29:29 +09:00
4a27a825a1 콘텐츠 조회
- 고정콘텐츠가 상위에 노출되도록 정렬 수정
2024-01-28 20:55:08 +09:00
77ed131f89 콘텐츠 고정
- active가 true로 변하지 않는 버그 수정
2024-01-28 16:58:54 +09:00
f70e5bae9a 콘텐츠 고정
- 고정 콘텐츠 리스트 정렬 순서 변경
2024-01-27 03:42:27 +09:00
cec4cd0d28 콘텐츠 고정
- 고정 콘텐츠 개수 표시 수정
2024-01-27 02:43:41 +09:00
95e31bb629 콘텐츠 고정
- 중복으로 추가되는 버그 수정
2024-01-27 02:29:09 +09:00
443818efb5 콘텐츠 상세
- 상단에 고정 상태, 고정이 가능한 상태 인지 판단하는 플래그 추가
2024-01-27 00:03:34 +09:00
711842f00d 콘텐츠 고정, 해제 API 추가 2024-01-26 23:36:17 +09:00
8a09780a02 Merge pull request 'test' (#119) from test into main
Reviewed-on: #119
2024-01-26 06:19:49 +00:00
3ea6b5824b 콘텐츠 상세
- 미리듣기 여부 추가
2024-01-26 03:39:09 +09:00
be8f0d66b9 콘텐츠 업로드
AS-IS : 항상 미리듣기 파일 생성
TO-BE : 미리듣기 파일 생성 여부 선택
2024-01-26 01:57:20 +09:00
45e8ec6505 Merge pull request '콘텐츠 정렬 기준' (#118) from test into main
Reviewed-on: #118
2024-01-24 15:03:06 +00:00
1ec7a6f096 콘텐츠 정렬 기준
- AS-IS : 올린 순서 기준 정렬 ( created_at )
- TO-BE : 릴리즈 순서 기준 정렬 ( release_date )
2024-01-24 23:58:05 +09:00
4554b85914 Merge pull request '회원가입 시 닉네임 validation 조건' (#117) from test into main
Reviewed-on: #117
2024-01-24 07:11:23 +00:00
8f44d0b2cd 회원가입 시 닉네임 validation 조건
- 닉네임 빈 칸 제거
2024-01-24 15:53:38 +09:00
8aa79c4a9c Merge pull request '콘텐츠 등록 - 태그 등록' (#116) from test into main
Reviewed-on: #116
2024-01-16 15:19:59 +00:00
71a3c357e7 콘텐츠 등록 - 태그 등록
AS-IS : 태그 분리시 빈 칸을 기준으로만 split하여 #A#B와 같이 태그가 저장되는 경우 있음
TO-BE : replace함수를 사용하여 # 앞에 빈 칸을 추가하여 #A#B로 저장되는 태그를 #A, #B와 같이 따로 저장되도록 수정
2024-01-15 11:10:01 +09:00
c8d3210b57 Merge pull request 'test' (#115) from test into main
Reviewed-on: #115
2024-01-11 09:05:44 +00:00
5ac6750ffe 관리자 콘텐츠 리스트
- 오픈 예정일 포맷 수정 (2024-01-01 17:00)
2024-01-11 17:35:57 +09:00
92bcbbe065 관리자 콘텐츠 리스트
- 오픈 예정일 포맷 수정 (2024-01-01 17:00)
2024-01-11 17:22:11 +09:00
844f9fd79b 관리자 콘텐츠 리스트
- 오픈 예정일 포맷 수정 (2024-01-01 PM 05:00)
2024-01-11 17:10:09 +09:00
d2ecca55b3 관리자 콘텐츠 수정
- isActive가 false이면 releaseDate null 처리
2024-01-11 17:01:54 +09:00
1b7ecc4afe 관리자 콘텐츠 리스트
- 오픈 예정일 추가
2024-01-11 16:59:32 +09:00
2282a49563 Merge pull request 'test' (#114) from test into main
Reviewed-on: #114
2024-01-11 03:49:53 +00:00
abd1626997 콘텐츠 업로드
- 기본 타임존 설정 추가 (Asia/Seoul)
2024-01-11 12:40:57 +09:00
afe529a116 예약 업로드 배포 로직 위치 이동 2024-01-11 12:19:09 +09:00
3e8476431d 푸시 message 발송 - null 예외처리 추가 2024-01-11 03:50:54 +09:00
b82fdfb2c8 Merge pull request '예약 업로드' (#113) from test into main
Reviewed-on: #113
2024-01-10 16:59:51 +00:00
2d015d0a33 콘텐츠 리스트
- 콘텐츠 노출 조건 변경 ( isActive가 true이거나 releaseDate와 duration이 null 이 아닌 경우 노출 )
2024-01-11 01:40:35 +09:00
765aec3620 콘텐츠 상세
- 콘텐츠를 올린 크리에이터도 오픈예정 날짜를 수신 하도록 수정
2024-01-11 00:24:41 +09:00
8a866df5a3 예약 업로드 된 오디오 콘텐츠 릴리즈 2024-01-11 00:04:58 +09:00
0dd1a706fd 불필요한 print 코드 제거 2024-01-10 23:52:57 +09:00
f89a61e23e 예약 업로드 된 오디오 콘텐츠 릴리즈 2024-01-10 23:40:17 +09:00
c479e5ad81 오디오 콘텐츠 상세
- isActive가 false이더라도 releaseDate가 null이 아니면 데이터를 불러오도록 수정
2024-01-10 23:09:14 +09:00
1a02f2383e 오디오 콘텐츠 삭제
- 콘텐츠 삭제시 오픈 예정 날짜 null 처리
2024-01-10 22:33:33 +09:00
319893d60f 오디오 콘텐츠 리스트
- 오픈예정 콘텐츠가 리스트에 보이도록 로직 수정
2024-01-10 22:32:28 +09:00
e007a95982 오디오 콘텐츠 예약 업로드
- 예약 업로드 한 콘텐츠 release와 푸시발송 로직 추가
2024-01-10 21:45:44 +09:00
ca2cc7a6b6 오디오 콘텐츠 리스트 - 오픈 예정 플래그 추가 2024-01-10 01:27:40 +09:00
76ade3daa1 콘텐츠 상세
- 오픈 날짜 Format : "yyyy년 MM월 dd일 HH시 mm분 오픈예정" 로 변경
2024-01-09 23:59:05 +09:00
41be05093d 콘텐츠 상세
- 콘텐츠 URL, 오픈예정 날짜 로직 조건 수정
2024-01-09 23:24:21 +09:00
40f4a12f9b 사용하지 않는 함수 제거 2024-01-09 23:09:56 +09:00
5e093a5555 사용하지 않는 API 제거 2024-01-09 23:02:41 +09:00
06d670df50 콘텐츠
- 오픈예정인 작품은 새로운 콘텐츠에서 보이지 않도록 수정
- 콘텐츠 상세페이지에 오픈 날짜 추가
2024-01-09 22:50:44 +09:00
9192209ca7 본인인증
- 본인인증 오류 메시지 수정
2024-01-09 13:44:41 +09:00
26d9b6cf35 콘텐츠 업로드
- 예약 업로드를 위해 공개날짜를 추가
2024-01-08 22:53:42 +09:00
4923a04c9d 콘텐츠 구매 보관함
- isActive가 true인 콘텐츠만 가져오도록 수정
2024-01-08 20:26:30 +09:00
2d17eac199 Merge pull request '19세 미만이 인증처리 되던 버그 수정' (#112) from test into main
Reviewed-on: #112
2024-01-08 10:07:21 +00:00
52a174d1b3 19세 미만이 인증처리 되던 버그 수정 2024-01-08 19:01:06 +09:00
e482bc3aad Merge pull request '콘텐츠를 올린 크리에이터가 댓글을 삭제할 수 있도록 수정' (#111) from test into main
Reviewed-on: #111
2024-01-04 11:36:43 +00:00
6101b964af 콘텐츠를 올린 크리에이터가 댓글을 삭제할 수 있도록 수정 2024-01-04 20:04:48 +09:00
ec022b74d1 Merge pull request '캔 쿠폰 조회 로직 수정' (#110) from test into main
Reviewed-on: #110
2024-01-04 09:59:20 +00:00
a241b96d59 캔 쿠폰 조회 로직 수정
AS-IS : 캔 쿠폰 아이템 전체 조회
TO-BE : 필요한 필드만 조회
2024-01-04 18:22:48 +09:00
dc42c09ce3 Merge pull request '캔 쿠폰 조회' (#109) from test into main
Reviewed-on: #109
2024-01-03 15:48:09 +00:00
315c957d71 캔 쿠폰 조회
- 사용 수량 수정
2024-01-04 00:37:56 +09:00
046a34d2a4 Merge pull request 'test' (#108) from test into main
Reviewed-on: #108
2024-01-03 15:19:42 +00:00
5a0bf61a36 캔 유효성 검사
- 쿠폰 유효기간과 활성화 쿠폰 검사 추가
2024-01-04 00:09:45 +09:00
3d76220660 캔 쿠폰 수정
- 날짜 패턴 수정
2024-01-03 23:52:59 +09:00
02254c29e4 캔 쿠폰
- 사용 수량 수정
2024-01-03 23:48:39 +09:00
c65bad63ca 캔 쿠폰
- 수정 API 추가
2024-01-03 23:31:12 +09:00
9ff6ec1888 Merge pull request '캔 쿠폰 시스템' (#107) from test into main
Reviewed-on: #107
2024-01-03 11:28:48 +00:00
538d4288bb 충전내역
- 쿠폰으로 충전시 쿠폰이름 혹은 '쿠폰충전'으로 표시
2024-01-03 05:27:44 +09:00
75f3d1dab3 쿠폰 번호 사용 API 수정
- 쿠폰 사용내역을 기록하지 않던 버그 수정
2024-01-03 05:25:36 +09:00
fbaa1aa14c 쿠폰 번호 사용 API 추가 2024-01-03 04:11:32 +09:00
fb66ea3347 쿠폰 번호 관련 로직 이동
- couponRepository -> couponNumberRepository로 이동
2024-01-03 03:21:30 +09:00
123b21cab2 쿠폰 번호 다운로드 API 수정
- MediaType openxmlformats 으로 수정
2024-01-02 06:27:58 +09:00
f45e07c879 쿠폰 번호 다운로드 API 추가 2024-01-02 06:05:59 +09:00
d20f51ceac 쿠폰 번호 리스트 API 수정
- totalCount 추가
2024-01-02 03:13:04 +09:00
f80b8248e8 쿠폰 생성 API 수정
AS-IS: 유효기간 타입 LocalDateTime
TO-BE: 유효기간 타입 String
2024-01-02 01:43:42 +09:00
ba2530ba55 쿠폰 리스트 가져오기 API 수정
AS-IS: 캔 타입 String
TO-BE: 캔 타입 Int
2024-01-02 01:28:05 +09:00
3b97364f24 쿠폰 리스트 가져오기 API 수정
AS-IS: 응답값에 전체 개수 없음
TO-BE: 응답값에 전체 개수 추가
2024-01-02 00:02:43 +09:00
209f1f4bd1 쿠폰 시스템
- 쿠폰 번호 리스트 API 추가
2024-01-01 06:22:03 +09:00
38cf9e453d 쿠폰 시스템
- 쿠폰 생성 API 추가
- 쿠폰 리스트 API 추가
2024-01-01 04:46:57 +09:00
d2950106ec Merge pull request '콘텐츠 랭킹 - 후원 순위 제거, 룰렛 아이템 개수 10로 변경' (#106) from test into main
Reviewed-on: #106
2023-12-26 12:50:09 +00:00
f304242eb4 인기 콘텐츠 랭킹 정렬 수정
AS-IS : -
TO-BE : 후원 순위 제거
2023-12-26 16:08:42 +09:00
9f59578275 룰렛 아이템 개수 수정
AS-IS : 최대 6개
TO-BE : 최대 10개
2023-12-26 14:52:35 +09:00
962f800d2e Merge pull request '팔로우 한 크리에이터 커뮤니티 게시물 조회 - 인증하지 않은 사람은 19금이 아닌 최신 게시물이 조회되도록 수정' (#105) from test into main
Reviewed-on: #105
2023-12-25 08:19:43 +00:00
4a83ebd472 팔로우 한 크리에이터 커뮤니티 게시물 조회 - 인증하지 않은 사람은 19금이 아닌 최신 게시물이 조회되도록 수정 2023-12-25 17:10:38 +09:00
962107e507 Merge pull request '팔로우 한 크리에이터 커뮤니티 게시물 조회 - 차단된 유저는 조회되지 않도록 수정' (#104) from test into main
Reviewed-on: #104
2023-12-21 19:28:10 +00:00
81415e0854 팔로우 한 크리에이터 커뮤니티 게시물 조회 - 차단된 유저는 조회되지 않도록 수정 2023-12-22 04:23:15 +09:00
039bd11963 Merge pull request '커뮤니티 게시물 조회 - 차단된 유저는 조회되지 않도록 수정' (#103) from test into main
Reviewed-on: #103
2023-12-21 19:05:12 +00:00
e6777962a9 커뮤니티 게시물 조회 - 차단된 유저는 조회되지 않도록 수정 2023-12-22 03:59:48 +09:00
5c250ea4ae Merge pull request '크리에이터 커뮤니티' (#102) from test into main
Reviewed-on: #102
2023-12-21 15:10:55 +00:00
8c5b7b811d 커뮤니티 게시물 조회 - 댓글 추가 불가능한 경우 댓글조회를 하지 않도록 수정 2023-12-21 23:53:33 +09:00
7326babc49 커뮤니티 게시물 response - creatorId 추가 2023-12-21 23:36:20 +09:00
555c3bfc73 팔로우 한 크리에이터의 7일 이내의 커뮤니티 게시물 10개 가져오기 API 추가 2023-12-21 23:17:38 +09:00
e4ca84252d 팔로우 한 크리에이터의 7일 이내의 커뮤니티 게시물 10개 가져오기 API 추가 2023-12-21 22:35:52 +09:00
d1d9ef5d24 커뮤니티 게시물 조회 상세 API 추가 2023-12-21 01:24:27 +09:00
e48fcc9d25 커뮤니티 게시물 조회 API - 활성화 된 게시물만 불러오기 2023-12-21 00:30:21 +09:00
024b8ce872 커뮤니티 게시물 수정 API - isActive 수정 기능 추가 2023-12-21 00:22:38 +09:00
421846e08a 커뮤니티 게시물 신고 추가 2023-12-20 23:09:59 +09:00
2735ac32bb 커뮤니티 게시물 신고 추가 2023-12-20 22:44:25 +09:00
ee35a0c13e 커뮤니티 댓글 - 답글이 아닌 댓글만 불러오기 2023-12-20 21:08:00 +09:00
29c7d5c677 커뮤니티 댓글 - 답글 리스트 API 추가 2023-12-20 20:27:04 +09:00
25022e4909 커뮤니티 게시물 좋아요 로직 수정 2023-12-20 03:47:14 +09:00
0df0d71660 커뮤니티 게시물 정렬 - 등록 내림차순으로 수정 2023-12-20 03:32:45 +09:00
7204233663 커뮤니티 게시물 댓글 정렬 - 등록 내림차순으로 수정 2023-12-20 03:18:19 +09:00
e090a2fe7a 커뮤니티 게시물 리스트 - 게시물 ID 추가 2023-12-20 02:11:47 +09:00
290be744a3 채널 프로필 API - community 게시물 리스트 추가 2023-12-20 00:03:13 +09:00
13dc77666a 커뮤니티 API - url path 수정 2023-12-19 23:45:39 +09:00
8a3d11ae59 커뮤니티 API 추가 2023-12-19 21:12:20 +09:00
e3405bcec6 Merge pull request 'test' (#101) from test into main
Reviewed-on: #101
2023-12-13 16:14:50 +00:00
7e02acd22c 라이브 방 리스트 - 크리에이터 프로필 이미지 URL 추가 2023-12-14 00:02:48 +09:00
da4ecf7d23 무료 충전 관련 코드 제거 2023-12-12 11:53:16 +09:00
0fd1c2235f Merge pull request '라이브 정산 - 정렬 순서 추가 (라이브 방 id, 구분)' (#100) from test into main
Reviewed-on: #100
2023-12-10 16:58:05 +00:00
185d7e6666 라이브 정산 - 정렬 순서 추가 (라이브 방 id, 구분) 2023-12-11 01:45:01 +09:00
b20c29b022 Merge pull request 'test' (#99) from test into main
Reviewed-on: #99
2023-12-10 12:34:51 +00:00
80930ebd3d 지금 즉시 라이브 만들기, 라이브 시작
- 라이브 시작시 본인 라이브 만이 아닌 진행 중인 모든 라이브 삭제 되는 버그 수정
2023-12-10 21:26:15 +09:00
13088f53a6 콘텐츠 메인 - 메인 페이지 섹션별 API 추가 2023-12-10 19:07:51 +09:00
12d5dcd298 Merge pull request 'test' (#98) from test into main
Reviewed-on: #98
2023-12-10 09:02:29 +00:00
0c3f190044 지금 즉시 라이브 만들기, 라이브 시작
- 라이브 시작시 기존에 진행 중인 라이브 삭제
2023-12-10 17:46:41 +09:00
35289ea93e 크리에이터 프로필 후원랭킹 - 룰렛 후원 데이터 추가 2023-12-10 15:56:30 +09:00
2c305dc6c6 Merge pull request 'test' (#97) from test into main
Reviewed-on: #97
2023-12-10 06:48:43 +00:00
63f690ee2d 라이브 정산 - 구분에 룰렛 추가 2023-12-10 15:40:44 +09:00
1a68746a7c 룰렛 설정 가져오기 API - 설정된 적이 없을 때 기본 캔 설정 5 -> 0으로 변경 2023-12-07 10:52:37 +09:00
62f76f7433 Merge pull request 'test' (#96) from test into main
Reviewed-on: #96
2023-12-07 01:46:33 +00:00
6ab9871fb9 현재 라이브 후원 총합 가져오기 API - 룰렛후원도 합계에 포함하도록 수정 2023-12-02 05:10:17 +09:00
7badf857cb 룰렛 돌리기 실패시 캔 환불 API 추가 2023-12-02 05:01:58 +09:00
612ca02461 현재 라이브 후원 랭킹 API - 룰렛 후원도 포함하도록 수정 2023-12-02 04:51:59 +09:00
b5e6176885 룰렛 돌리기 - 캔 사용처가 정확하지 않아 생기는 버그 수정 2023-12-02 04:31:03 +09:00
6b90f42a0e 라이브 방 정보 - 룰렛 사용 여부 추가 2023-12-01 17:29:11 +09:00
cae9f22e49 라이브 종료 - 라이브 종료 시 룰렛 비활성화 하도록 수정 2023-12-01 03:16:50 +09:00
aeed1dbd06 룰렛 저장 API, 룰렛 가져오기 API 수정 2023-12-01 02:36:29 +09:00
382364b5df 룰렛 옵션 저장
- percentage(퍼센트) -> weight(가중치)로 변경
2023-12-01 00:12:58 +09:00
0e8d3656b2 룰렛 가져 오기 API - isActive 추가 2023-11-29 19:46:26 +09:00
0d2e0a1af8 룰렛 돌리기 API 추가 2023-11-28 16:07:07 +09:00
516853b05f 룰렛 데이터 읽기 API 추가 2023-11-28 15:24:01 +09:00
e96b3d9bd4 룰렛 데이터 생성/수정 API 추가 2023-11-28 14:24:03 +09:00
858ce524f9 Merge pull request 'test' (#95) from test into main
Reviewed-on: #95
2023-11-27 12:48:24 +00:00
1a5b4b364a 콘텐츠 댓글의 답글 푸시
- 댓글을 지우면 답글 푸시가 가지 않도록 수정
2023-11-27 21:11:05 +09:00
f196b20024 콘텐츠 댓글의 답글 푸시
- AS-IS : 원 댓글의 글쓴이에게 알림(원 댓글 글쓴이가 답글을 달아도 알림)
- To-Be : 답글을 쓴 본인을 제외하고 원 댓글의 답글을 쓴 모든 유저에게 푸시 알림
2023-11-27 20:35:08 +09:00
3795fb4a40 Merge pull request 'test' (#94) from test into main
Reviewed-on: #94
2023-11-24 07:03:10 +00:00
11bb799bd5 관리자 - 푸시 발송
- 본인 인증 여부에 따라 푸시 메시지를 발송할 수 있도록 isAuth 플래그 추가
2023-11-24 15:52:47 +09:00
84102bb95a 관리자 - 푸시 발송
- 본인 인증 여부에 따라 푸시 메시지를 발송할 수 있도록 isAuth 플래그 추가
2023-11-24 15:03:15 +09:00
3ada2dea87 관리자 - 푸시 발송
- 본인 인증 여부에 따라 푸시 메시지를 발송할 수 있도록 isAuth 플래그 추가
2023-11-24 14:58:30 +09:00
82ac381936 관리자 - 푸시 발송
- 본인 인증 여부에 따라 푸시 메시지를 발송할 수 있도록 isAuth 플래그 추가
2023-11-24 14:38:33 +09:00
0c01aeec50 Merge pull request '관리자 - 이벤트 배너 등록' (#93) from test into main
Reviewed-on: #93
2023-11-21 16:21:19 +00:00
3c72ae048e 관리자 - 이벤트 배너 등록
- 본인 인증 여부에 따라 노출할 수 있도록 isAdult 컬럼 추가
2023-11-22 01:11:15 +09:00
892206744d Merge pull request '이벤트 배너, 팝업' (#92) from test into main
Reviewed-on: #92
2023-11-21 12:59:30 +00:00
61cf1577dc 이벤트 배너, 팝업
- 본인 인증 여부에 따라 노출할 수 있도록 isAdult 컬럼 추가
2023-11-21 21:41:45 +09:00
9e2c1474db Merge pull request '메시지 보내기 유저 검색' (#91) from test into main
Reviewed-on: #91
2023-11-20 05:41:57 +00:00
7fefc9b0a6 최근 방문한 방 유저검색
- 유저 권한 봇 제거
- 권한이 유저인 경우 유저에게 안보이도록 수정
2023-11-20 14:30:01 +09:00
5ce4078ba4 유저 검색
- 유저 권한 봇 제거
2023-11-20 12:41:08 +09:00
8718089483 유저 검색
- 권한이 유저인 경우 타 유저가 검색 되지 않도록 수정(크리에이터만 검색)
2023-11-20 12:24:15 +09:00
16328f73d9 Merge pull request '크리에이터 관리자, 관리자 - 일자별 콘텐츠 후원 정산 API' (#90) from test into main
Reviewed-on: #90
2023-11-14 13:14:19 +00:00
5a75972f26 크리에이터 관리자, 관리자 - 일자별 콘텐츠 후원 정산 API
- 유료콘텐츠 후원 정산 90%
- 무료콘텐츠 후원 정산 70%
2023-11-14 22:01:05 +09:00
e0d4f53cf4 Merge pull request 'test' (#89) from test into main
Reviewed-on: #89
2023-11-14 12:15:51 +00:00
dcd6933824 크리에이터 관리자 일자별 콘텐츠 후원 정산 API - 캐싱 추가 2023-11-14 21:03:21 +09:00
35f66e7e41 크리에이터 관리자 - 일자별 콘텐츠 후원 정산 API 추가 2023-11-14 20:50:54 +09:00
e09a59c5b4 Merge pull request 'test' (#88) from test into main
Reviewed-on: #88
2023-11-14 11:09:14 +00:00
8d32f1b3bd 관리자 콘텐츠 정산, 라이브 정산 - 캔을 한 번 사용했는데 여러개의 PG로 결제한 캔이 사용될 때 생기는 오차수정 2023-11-14 19:12:10 +09:00
04314c6256 관리자 콘텐츠 후원 정산 - 캔을 한 번 사용했는데 여러개의 PG로 결제한 캔이 사용될 때 생기는 오차수정 2023-11-14 19:09:49 +09:00
68c2b505bb 관리자 일자별 콘텐츠 후원 정산 - 캔을 한 번 사용했는데 여러개의 PG로 결제한 캔이 사용될 때 생기는 오차수정 2023-11-14 18:54:17 +09:00
049e654535 Merge pull request '관리자 일자별 콘텐츠 후원 정산 - 크리에이터 순으로 정렬' (#87) from test into main
Reviewed-on: #87
2023-11-14 09:22:38 +00:00
9448c6a488 관리자 일자별 콘텐츠 후원 정산 - 크리에이터 순으로 정렬 2023-11-14 18:17:42 +09:00
c927dc4ecd Merge pull request '관리자 - 일자별 콘텐츠 후원 정산 페이지 추가' (#86) from test into main
Reviewed-on: #86
2023-11-14 09:03:35 +00:00
481f2e1126 관리자 일자별 콘텐츠 후원 정산 페이지 - 캐시 추가 2023-11-14 17:56:49 +09:00
9751900193 관리자 일자별 콘텐츠 후원 정산 페이지 - 날짜 검색 추가 2023-11-14 17:47:16 +09:00
07455cd5b1 관리자 - 일자별 콘텐츠 후원 정산 페이지 추가 2023-11-14 17:29:46 +09:00
fe4ecd0ad8 Merge pull request '크리에이터 관리자 - 일자별 콘텐츠 정산 페이징 안되는 버그 수정' (#85) from test into main
Reviewed-on: #85
2023-11-14 05:17:58 +00:00
216f983d36 크리에이터 관리자 - 일자별 콘텐츠 정산 페이징 안되는 버그 수정 2023-11-14 14:11:26 +09:00
cb9ba824cf 크리에이터 관리자 - 일자별 콘텐츠 정산 페이징 안되는 버그 수정 2023-11-14 13:59:22 +09:00
a215d027ec 크리에이터 관리자 - 일자별 콘텐츠 정산 페이징 안되는 버그 수정 2023-11-14 13:41:32 +09:00
78d476fe80 Merge pull request '라이브 상세 - 시작 시간 dateformat yyyy.MM.dd E hh:mm a 로 복구' (#84) from test into main
Reviewed-on: #84
2023-11-14 03:31:54 +00:00
41a4802dc6 라이브 상세 - 시작 시간 dateformat yyyy.MM.dd E hh:mm a 로 복구 2023-11-14 12:07:42 +09:00
a11c8465d5 Merge pull request '크리에이터 관리자 - @JsonProperty 추가' (#83) from test into main
Reviewed-on: #83
2023-11-13 16:10:57 +00:00
06a890bc15 크리에이터 관리자 - @JsonProperty 추가 2023-11-14 01:02:51 +09:00
366304a9b7 Merge pull request '크리에이터 관리자 - 정산 API 캐시 추가' (#82) from test into main
Reviewed-on: #82
2023-11-13 15:56:58 +00:00
d382d85d26 크리에이터 관리자 - 정산 API 캐시 추가 2023-11-14 00:51:54 +09:00
4356663688 Merge pull request '크리에이터 관리자 - 콘텐츠 누적 매출 API' (#81) from test into main
Reviewed-on: #81
2023-11-13 15:23:53 +00:00
b464f7ae4c 크리에이터 관리자 - 콘텐츠 누적 매출 API 2023-11-14 00:16:04 +09:00
3164232f9d 콘텐츠 누적 매출 API - 캐시 키 이름 변경 2023-11-14 00:08:13 +09:00
8da7ee0bb3 라이브 매출 API - 30분 캐시 적용 2023-11-14 00:07:31 +09:00
9458a3976b 콘텐츠 누적 매출 API - 3시간 캐시 적용 2023-11-14 00:05:21 +09:00
26b55e6fcf Merge pull request '콘텐츠 누적 매출 API - orderType 추가' (#80) from test into main
Reviewed-on: #80
2023-11-13 14:48:47 +00:00
dbf795804c 콘텐츠 누적 매출 API - orderType 추가 2023-11-13 23:37:39 +09:00
0d743f7204 Merge pull request '콘텐츠 누적 매출 API 추가' (#79) from test into main
Reviewed-on: #79
2023-11-13 13:42:17 +00:00
e3a31f16bc 콘텐츠 누적 매출 API - 페이징 추가 2023-11-13 22:29:58 +09:00
234c9adcdd 콘텐츠 누적 매출 API - 정렬 1순위 회원번호, 2순위 콘텐츠 번호 추가 2023-11-13 22:24:01 +09:00
b7e4e6f43f 콘텐츠 누적 매출 API - 사용하지 않는 파라미터 제거 2023-11-13 22:16:34 +09:00
725b959117 콘텐츠 누적 매출 API 추가 2023-11-13 21:56:59 +09:00
6cbe113b3e Merge pull request '크리에이터 콘텐츠 정산 - API 추가' (#78) from test into main
Reviewed-on: #78
2023-11-13 10:03:02 +00:00
19875a841d 크리에이터 콘텐츠 정산 - totalCount query 수정 2023-11-13 18:46:49 +09:00
b3b7f739ff 크리에이터 콘텐츠 정산 - API 추가 2023-11-13 18:22:24 +09:00
6409b69d6c Merge pull request '콘텐츠 정산 - 결과값에 JsonProperty 를 추가하여 데이터 파싱이 진행 되도록 수정' (#77) from test into main
Reviewed-on: #77
2023-11-10 13:19:06 +00:00
9c367b400a 콘텐츠 정산 - 결과값에 JsonProperty 를 추가하여 데이터 파싱이 진행 되도록 수정 2023-11-10 22:14:38 +09:00
c5164c76fc Merge pull request '콘텐츠 정산 - group by 날짜 수정' (#76) from test into main
Reviewed-on: #76
2023-11-10 12:46:07 +00:00
0b0be28e1a 콘텐츠 정산 - group by 날짜 수정 2023-11-10 21:39:40 +09:00
baade8e138 Merge pull request 'test' (#75) from test into main
Reviewed-on: #75
2023-11-10 10:47:11 +00:00
fc17ba9aaa sql_mode=only_full_group_by 문제 쿼리를 수정하여 해결 2023-11-10 19:35:33 +09:00
31208b5e99 관리자 콘텐츠 정산 API - 페이징 제거 2023-11-10 19:15:29 +09:00
c590eff460 관리자 콘텐츠 정산 API - 총 개수 추가 2023-11-10 17:20:11 +09:00
472b51dd85 관리자 - 콘텐츠 정산 API 추가 2023-11-10 00:55:22 +09:00
b848d6b4e0 Merge pull request 'test' (#74) from test into main
Reviewed-on: #74
2023-11-09 11:18:20 +00:00
ad295564de 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 20:06:56 +09:00
bfedc448af 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 19:58:04 +09:00
1a6a833b93 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 19:49:31 +09:00
615164e488 Service 클래스 전체에 적용되어 있는 Transactional(readOnly=true) 삭제 2023-11-09 18:50:43 +09:00
8be8d15e4c 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 18:42:32 +09:00
60012a7aad 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 18:15:56 +09:00
75140a4055 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 17:32:10 +09:00
6bc5143471 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 17:17:58 +09:00
1eeea16792 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 16:38:21 +09:00
e0e24be688 본인인증 : block 된 사용자 정보로 본인인증을 시도하는 경우
- 본인인증 시도 계정 탈퇴 처리
2023-11-09 16:29:23 +09:00
d8139d2ab0 Merge pull request '라이브 리스트, 라이브 상세' (#73) from test into main
Reviewed-on: #73
2023-11-07 16:48:25 +00:00
db3ac923e1 라이브 리스트, 라이브 상세
- 라이브 시작 날짜/시간 포맷 24시간제에서 12시간제로 변경
2023-11-08 01:43:46 +09:00
e96d8f7469 Merge pull request 'test' (#72) from test into main
Reviewed-on: #72
2023-11-07 16:23:11 +00:00
24fc451574 라이브 리스트, 라이브 상세
- 라이브 시작 날짜/시간 포맷 yyyy년 MM월 dd일 (E) a HH시 mm분 로 변경
2023-11-08 00:55:35 +09:00
ee1c8d1f83 라이브 리스트, 라이브 상세
- 라이브 시작 날짜/시간 포맷 yyyy년 MM월 dd일 E요일 a HH:mm 로 변경
2023-11-08 00:49:11 +09:00
fc39b6c7a0 예약된 라이브 시작
- 예약된 라이브 시작시 라이브 시작시간을 현재시간으로 변경
- 예약한 시간 - 10분 보다 더 먼저 시작하는 경우 알림 메시지의 dateformat 변경
2023-11-08 00:38:46 +09:00
62797eb3f5 예약된 라이브 시작
- 예약된 라이브 시작시 라이브 시작시간을 현재시간으로 변경
- 예약한 시간 - 10분 보다 더 먼저 시작하는 경우 알림 메시지의 dateformat 변경
2023-11-08 00:16:41 +09:00
2acffd8afc Merge pull request '콘텐츠 메인 API - 캐싱을 적용하기 위해 AudioContentMainManageService 추가' (#71) from test into main
Reviewed-on: #71
2023-11-07 11:24:40 +00:00
9afc44b7b1 콘텐츠 메인 API - 캐싱을 적용하기 위해 AudioContentMainManageService 추가 2023-11-07 20:19:34 +09:00
3c8e72073c Merge pull request '콘텐츠 메인 API - @Transactional(readOnly = true) 추가' (#70) from test into main
Reviewed-on: #70
2023-11-07 08:47:47 +00:00
1b7fc14f00 콘텐츠 메인 API - @Transactional(readOnly = true) 추가 2023-11-07 17:38:43 +09:00
724d7a9d9b Merge pull request 'test' (#69) from test into main
Reviewed-on: #69
2023-11-07 08:21:56 +00:00
9a394b7dae 콘텐츠 메인 API - 코루틴 제거 2023-11-07 17:12:58 +09:00
ce15025c8d 콘텐츠 메인 API - 코루틴 적용 2023-11-07 16:47:25 +09:00
2da3b0db78 Merge pull request 'test' (#68) from test into main
Reviewed-on: #68
2023-11-06 09:26:36 +00:00
0e786f46cb 콘텐츠 메인 항목 - 사용하지 않는 isAdult 제거 2023-11-06 18:21:32 +09:00
5b218169c7 콘텐츠 메인 항목 - 사용하지 않는 isAdult 제거 2023-11-06 18:12:27 +09:00
5c228af14b 콘텐츠 메인 캐싱전략 수정
AS IS - 각 섹션별로 캐싱
TO BE - getMain 함수 전체 캐싱
2023-11-06 18:04:08 +09:00
685ad7afaf Merge pull request '콘텐츠 메인 캐싱전략 수정' (#67) from test into main
Reviewed-on: #67
2023-11-06 08:46:55 +00:00
976a504233 콘텐츠 메인 캐싱전략 수정
AS IS - 각 섹션별로 캐싱
TO BE - getMain 함수 전체 캐싱
2023-11-06 17:41:54 +09:00
264cf75964 Merge pull request '콘텐츠 메인 - 큐레이션 개수 15개만 노출' (#66) from test into main
Reviewed-on: #66
2023-11-06 08:11:00 +00:00
58a57c9a0e 콘텐츠 메인 - 큐레이션 개수 15개만 노출 2023-11-06 17:04:59 +09:00
c773dbc7b5 Merge pull request '콘텐츠 랭킹 - 후원 랭킹 조회 로직 수정' (#65) from test into main
Reviewed-on: #65
2023-11-04 14:24:46 +00:00
1d877255ac 콘텐츠 랭킹 - 후원 랭킹 조회 로직 수정 2023-11-04 23:18:29 +09:00
37cbc64f52 Merge pull request '본인인증' (#64) from test into main
Reviewed-on: #64
2023-11-03 07:48:02 +00:00
97e80a85e0 본인인증 - 3개 이상의 계정에 본인인증을 하려고 시도하는 경우 '이미 본인인증한 계정 N개 이용중입니다. 소다라이브의 본인인증은 최대 3개의 계정만 이용할 수 있습니다.' 메시지 반환 2023-11-03 16:05:49 +09:00
cf8d94776e 본인인증 - 이미 본인인증을 한 계정의 경우 '이미 인증된 계정입니다.' 문구 반환 2023-11-03 15:49:34 +09:00
cb1dde17bb Merge pull request 'test' (#63) from test into main
Reviewed-on: #63
2023-11-02 12:18:29 +00:00
bc1db79430 cache key 수정 - getActiveThemeOfContent -> activeThemeOfContent 2023-11-02 20:54:44 +09:00
42fbcdd1e8 콘텐츠 랭킹 정렬 조회 API 추가 2023-11-02 17:38:30 +09:00
dad31fffcb 콘텐츠 후원 랭킹 query 수정 2023-11-02 17:18:34 +09:00
ed3e55514a getAudioContentRanking - sort-type 추가 2023-11-02 03:41:54 +09:00
275f6049d1 getAudioContentRanking - sort-type 추가 2023-11-02 03:33:02 +09:00
d3b12eeef1 redis cache manager - serializeKeysWith 추가 2023-11-02 03:25:15 +09:00
b3d66151bc redis cache manager - serializeKeysWith 추가 2023-11-02 03:12:44 +09:00
1cf70c25a9 redis cache manager - serializeValuesWith 추가 2023-11-02 03:02:28 +09:00
695c8cbad8 redis cache manager - serializeValuesWith 추가 2023-11-02 02:58:12 +09:00
8fb61e7689 콘텐츠 순위 - GetAudioContentRanking class 에 JsonProperty 추가 2023-11-02 02:39:37 +09:00
d45a34525b 콘텐츠 메인 - 순위 정렬(매출, 후원, 댓글, 좋아요) 추가 2023-11-02 02:28:23 +09:00
c6c9073fa0 콘텐츠 메인 - 섹션별 캐시 키 수정 2023-11-01 15:59:43 +09:00
c29988acf4 Merge pull request '콘텐츠 주문 - 대여만 가능한 콘텐츠의 경우 소장으로 주문이 들어오더라도 대여로 처리되도록 로직 수정' (#62) from test into main
Reviewed-on: #62
2023-11-01 04:49:18 +00:00
64a63fa8fa 콘텐츠 주문 - 대여만 가능한 콘텐츠의 경우 소장으로 주문이 들어오더라도 대여로 처리되도록 로직 수정 2023-11-01 12:42:02 +09:00
eadbf56dae Merge pull request '정산테이블에 무료충전 코인도 반영되도록 수정' (#61) from test into main
Reviewed-on: #61
2023-10-28 08:42:29 +00:00
22b9dceea5 정산테이블에 무료충전 코인도 반영되도록 수정 2023-10-28 17:24:38 +09:00
4b3b455135 Merge pull request '캔 사용 시 제휴보상 캔도 사용할 수 있도록 수정' (#60) from test into main
Reviewed-on: #60
2023-10-27 13:48:33 +00:00
899bc076a3 캔 사용 시 제휴보상 캔도 사용할 수 있도록 수정 2023-10-27 22:41:38 +09:00
e6ac177396 Merge pull request '충전내역 - 결제수단에 "제휴보상" 표시' (#59) from test into main
Reviewed-on: #59
2023-10-26 18:48:46 +00:00
8017e837ef 충전내역 - 결제수단에 "제휴보상" 표시 2023-10-27 03:44:32 +09:00
3d0e29003f Merge pull request '충전내역 - 결제수단에 "제휴보상" 표시' (#58) from test into main
Reviewed-on: #58
2023-10-26 18:22:10 +00:00
4c8c6226a6 충전내역 - 결제수단에 "제휴보상" 표시 2023-10-27 03:12:56 +09:00
78b9b00f77 Merge pull request '충전내역 - 결제수단에 "제휴보상" 표시' (#57) from test into main
Reviewed-on: #57
2023-10-26 18:02:16 +00:00
fc99e6324a 충전내역 - 결제수단에 "제휴보상" 표시 2023-10-27 02:57:53 +09:00
0ee7faa551 Merge pull request '카울리를 이용한 무료충전 테이블 adProfit 과 point 타입 int -> float 로 변경' (#56) from test into main
Reviewed-on: #56
2023-10-26 16:55:39 +00:00
146205e4f7 카울리를 이용한 무료충전 테이블 adProfit 과 point 타입 int -> float 로 변경 2023-10-27 01:51:12 +09:00
e5fdced681 Merge pull request '콘텐츠 메인 캐싱 전략 변경' (#55) from test into main
Reviewed-on: #55
2023-10-26 16:14:37 +00:00
1c7fdfac69 콘텐츠 메인 캐싱 전략 변경 - repository 에 있던 @Cacheable 을 service 코드로 이동 2023-10-27 01:08:40 +09:00
3ec16b5045 현재 사용하지 않는 API 제거, 캐시 전략 변경을 위해 @Cacheable 제거 2023-10-26 18:42:22 +09:00
afb99fef64 Merge pull request 'GetAudioContentMainItem - adult를 isAdult로 변경, 캐시 제거' (#54) from test into main
Reviewed-on: #54
2023-10-24 11:39:56 +00:00
5b85c2e8a4 GetAudioContentMainItem - adult를 isAdult로 변경, 캐시 제거 2023-10-24 20:34:59 +09:00
7dfaa36024 Merge pull request 'test' (#53) from test into main
Reviewed-on: #53
2023-10-24 10:42:10 +00:00
063ee78a8c GetAudioContentMainItem JsonProperty - isAdult를 adult로 변경 2023-10-24 19:35:31 +09:00
5f3c7e7e90 GetAudioContentMainItem - JsonProperty 어노테이션 추가 2023-10-24 19:27:53 +09:00
1f055c2283 콘텐츠 1:1 관계 - FetchType EAGER로 변경 2023-10-24 19:18:49 +09:00
4eec062871 콘텐츠 랭킹 로직 - use_can 테이블 조인 제거 2023-10-24 19:14:15 +09:00
e7a318d6b9 콘텐츠 메인 - 큐레이션 아이템 가져오기 API 캐싱 2023-10-24 17:46:53 +09:00
9a7e76ea7a 콘텐츠 메인 - API 분리 2023-10-22 19:50:59 +09:00
f45c6c7938 콘텐츠 등록 - 대여만 가능하게 등록할 수 있도록 속성 추가 2023-10-20 23:55:50 +09:00
c43faef14e 콘텐츠 구매 - 대여만 가능한 경우 등록된 금액의 100%를 결제하도록 수정 2023-10-20 23:05:12 +09:00
487d3c9d6e 콘텐츠 상세 Response - isOnlyRental(대여만 가능) 추가 2023-10-20 22:10:18 +09:00
04c682fc9b 대여기간 - 15일로 변경 2023-10-20 18:25:42 +09:00
0496f665aa Merge pull request 'getAudioContentMainBannerList 부분 캐시 제거' (#52) from test into main
Reviewed-on: #52
2023-10-17 10:02:53 +00:00
ae6171e844 getAudioContentMainBannerList 부분 캐시 제거 2023-10-17 18:59:07 +09:00
0d19e1be74 Merge pull request 'audio content banner - lazy 옵션으로 인해 발생하는 com.fasterxml.jackson.databind.exc.InvalidDefinitionException 문제 수정' (#51) from test into main
Reviewed-on: #51
2023-10-17 09:47:44 +00:00
fb236a374d audio content banner - lazy 옵션으로 인해 발생하는 com.fasterxml.jackson.databind.exc.InvalidDefinitionException 문제 수정 2023-10-17 18:43:47 +09:00
4aff0111aa Merge pull request '로딩 속도를 위해 @Cacheable 적용' (#50) from test into main
Reviewed-on: #50
2023-10-17 09:31:08 +00:00
1be91d0de4 EventItem - Json 데이터에는 있지만 DTO에는 Key값이 없는 경우 발생하는 오류 수정 2023-10-17 18:25:45 +09:00
cda2f1ec36 기본 생성자 없는 data class에 @JsonProperty를 추가하여 Jackson에서 직렬화 할 수 있도록 수정 2023-10-17 18:11:56 +09:00
e5c85287bb 캐시 적용 - 추천라이브, 이벤트 리스트, 2주 이내 콘텐츠 업로드 한 크리에이터, 콘텐츠 상단 배너, 2023-10-17 17:58:24 +09:00
1fcb0ec5fd org.springframework.expression.spel.SpelParseException 문제를 제거하기 위해 key설정 제거 2023-10-17 17:39:22 +09:00
3537f22197 com.fasterxml.jackson.databind.exc.InvalidDefinitionException 에러를 처리하기 위해 - theme의 fetchType을 EAGER로 변경 2023-10-17 17:28:30 +09:00
99d7510c32 LocalDateTime serialize 에러를 처리 - JacksonConfig 제거, @JsonSerialize(using = LocalDateTimeSerializer::class) @JsonDeserialize(using = LocalDateTimeDeserializer::class)
추가
2023-10-17 17:15:55 +09:00
bcdd161205 LocalDateTime serialize 도중 발생하는 에러를 처리하기 위해 JacksonConfig 추가 2023-10-17 16:59:16 +09:00
0f6c3075bc 인기 콘텐츠 Cache 로직 - key 값에서 LocalDateTime 제거 2023-10-17 16:33:21 +09:00
cd4b165f90 인기 콘텐츠, 큐레이션 조회로직에 Cache 적용 2023-10-17 16:15:52 +09:00
63b3ba2bb2 Merge pull request '인기 콘텐츠 전체보기 집계날짜 수정' (#49) from test into main
Reviewed-on: #49
2023-10-16 13:59:28 +00:00
567c51f6a2 인기 콘텐츠 전체보기 집계날짜 수정 2023-10-16 22:55:26 +09:00
7444b41f60 Merge pull request '콘텐츠 메인 - 인기 콘텐츠 집계날짜 수정' (#48) from test into main
Reviewed-on: #48
2023-10-16 13:42:31 +00:00
0aecd36956 콘텐츠 메인 - 인기 콘텐츠 집계날짜 수정 2023-10-16 22:38:23 +09:00
8e90dbc8b6 Merge pull request '구매목록 - isActive 가 true 인 것만 조회되도록 수정' (#47) from test into main
Reviewed-on: #47
2023-10-16 03:30:10 +00:00
5068be1a4c 구매목록 - isActive 가 true 인 것만 조회되도록 수정 2023-10-16 12:26:01 +09:00
9f70722521 Merge pull request '탐색 인기 크리에이터 - 날짜 설명 글 수정' (#46) from test into main
Reviewed-on: #46
2023-10-14 21:43:48 +00:00
7a22e7d887 탐색 인기 크리에이터 - 날짜 설명 글 수정 2023-10-15 06:39:15 +09:00
52fae596fa Merge pull request '콘텐츠 랭킹 데이터 전체보기 API - 페이징 추가' (#45) from test into main
Reviewed-on: #45
2023-10-14 21:20:19 +00:00
6775d3d72d 콘텐츠 랭킹 데이터 전체보기 API - 페이징 추가 2023-10-15 05:57:26 +09:00
ccb67957bc Merge pull request '콘텐츠 랭킹 추가' (#44) from test into main
Reviewed-on: #44
2023-10-14 19:37:55 +00:00
149a3ad2f1 콘텐츠 랭킹 데이터 전체보기 API - 페이징 추가 2023-10-15 03:38:26 +09:00
9146e2e231 콘텐츠 랭킹 데이터 전체보기 API 추가 2023-10-15 03:23:03 +09:00
333458a184 콘텐츠 메인 콘텐츠 랭킹 데이터 - endDate 표시날짜 하루 전으로 변경 2023-10-15 01:20:54 +09:00
f889ae5232 콘텐츠 메인 콘텐츠 랭킹 데이터 - group by 추가 2023-10-15 01:03:20 +09:00
e507af8d5b 콘텐츠 메인 - 콘텐츠 랭킹 데이터 추가 2023-10-15 00:51:16 +09:00
fb82538d0d Merge pull request '캔 소비 - 콘텐츠 주문시 캔 소비내역에 콘텐츠 내용 추가' (#43) from test into main
Reviewed-on: #43
2023-10-14 11:33:59 +00:00
cccb76afc2 캔 소비 - 콘텐츠 주문시 캔 소비내역에 콘텐츠 내용 추가 2023-10-14 20:07:36 +09:00
72ee39612e Merge pull request '탐색 - 인기 급상승 제거, 인기 크리에이터 섹션 추가' (#42) from test into main
Reviewed-on: #42
2023-10-13 15:41:25 +00:00
d561ad6d41 탐색 인기 크리에이터 섹션 - 날짜 수정 2023-10-14 00:31:47 +09:00
a7fc89cf40 탐색 인기 크리에이터 섹션 - 날짜 수정 2023-10-14 00:24:27 +09:00
b59d7b5dca 탐색 - 인기 급상승 제거, 인기 크리에이터 섹션 추가 2023-10-13 23:59:38 +09:00
51fd5408dc Merge pull request 'test' (#41) from test into main
Reviewed-on: #41
2023-10-06 08:50:32 +00:00
4f0148d80e 채널, 라이브 정보 API - 후원랭킹 보기 스위치 on/off에 따라 후원랭킹이 조회되도록 수정 2023-10-05 23:05:37 +09:00
ed4f0a62a1 채널 후원랭킹 보기 스위칭 API 추가 2023-10-05 19:32:56 +09:00
3fae40fbef Merge pull request '콘텐츠 상세 - 댓글 수 로직 답글 포함하지 않도록 수정' (#40) from test into main
Reviewed-on: #40
2023-10-05 02:49:22 +00:00
51260311a0 콘텐츠 상세 - 댓글 수 로직 답글 포함하지 않도록 수정 2023-10-05 01:27:33 +09:00
0745890af0 Merge pull request 'test' (#39) from test into main
Reviewed-on: #39
2023-10-04 03:24:41 +00:00
75efb564fc 예약 후 시작하지 않은 라이브 조건 수정 - 채널 이름이 없는 라이브만 취소되도록 수정 2023-10-04 11:22:41 +09:00
f6e9c6d010 크리에이터 관리자 - 라이브 정산 API 추가 2023-10-03 23:58:16 +09:00
4abe1730a7 Merge pull request '관리자 라이브 정산 API - 인원 추가' (#38) from test into main
Reviewed-on: #38
2023-10-03 12:10:51 +00:00
236aeb0258 관리자 라이브 정산 API - 인원 추가 2023-10-03 19:57:28 +09:00
626f0e6989 Merge pull request '관리자 - 라이브 정산 API 추가' (#37) from test into main
Reviewed-on: #37
2023-10-03 09:28:04 +00:00
90732e137f 관리자 - 라이브 정산 API 추가 2023-10-03 18:00:38 +09:00
9f42d9d173 Merge pull request '라이브 시작 알림 - 알림 받을 유저 조회에서 에러가 발생하는 버그 수정' (#36) from test into main
Reviewed-on: #36
2023-10-02 13:00:11 +00:00
5b0be30c5b 라이브 시작 알림 - 불필요한 로그 제거 2023-10-02 21:46:51 +09:00
13aa9838cd 라이브 시작 알림 - 알림 받을 유저 조회에서 에러가 발생하는 버그 수정 2023-10-02 21:40:04 +09:00
24b8618306 라이브 시작 알림 - 알림 받을 유저 조회에서 에러가 발생하는 버그 수정 2023-10-02 21:27:03 +09:00
f90a93c4bc Merge pull request '후원순위 - 유료라이브 입장 캔 반영' (#35) from test into main
Reviewed-on: #35
2023-09-27 14:57:27 +00:00
75dbfad3a7 후원순위 - 유료라이브 입장 캔 반영 2023-09-27 23:22:16 +09:00
8000ad6c6a Merge pull request 'test' (#34) from test into main
Reviewed-on: #34
2023-09-27 06:49:21 +00:00
ffdcd61894 새로운 콘텐츠 - 최근 2주 데이터만 불러오도록 수정 2023-09-27 15:42:20 +09:00
ba491420f5 큐레이션 전체 보기 데이터 JSON 표현방식 수정 2023-09-27 15:37:14 +09:00
1f1f1bea1a Merge pull request 'test' (#33) from test into main
Reviewed-on: #33
2023-09-27 05:28:04 +00:00
f161d9a436 콘텐츠 테마 API 추가 2023-09-27 00:05:58 +09:00
fd460f2d3e 새로운 콘텐츠 전체보기 API 추가 2023-09-27 00:02:05 +09:00
468add0819 큐레이션 전체보기 - 총 개수, 정렬 추가 2023-09-26 19:15:42 +09:00
151cc10caf 새로운 콘텐츠 - 페이징 추가 2023-09-26 15:59:44 +09:00
22a79c0be4 콘텐츠 큐레이션 전체보기 API 추가 2023-09-26 11:54:51 +09:00
5970a9a5b6 오디오 콘텐츠 업로드 - 미리듣기 시간 설정 추가 2023-09-22 17:00:50 +09:00
d95460c7cd Merge pull request '닉네임 변경 가격 100 캔으로 변경' (#32) from test into main
Reviewed-on: #32
2023-09-22 07:01:27 +00:00
8896233d78 닉네임 변경 가격 100 캔으로 변경 2023-09-22 15:49:51 +09:00
a3d93d4b08 Merge pull request 'test' (#31) from test into main
Reviewed-on: #31
2023-09-19 06:32:22 +00:00
d0d6ef64df 푸시 발송 - 라이브 개설/시작, 콘텐츠 업로드 시 매형 계정에도 푸시 발송 2023-09-19 15:24:52 +09:00
ff10a9935b 유료라이브 - 10캔 이상 부터 설정 가능하도록 수정 2023-09-19 12:21:29 +09:00
d5cc28e50b point click postback api 추가 2023-09-19 12:15:57 +09:00
07a92af982 Merge pull request '라이브 시작시간 4시간이 지나도 라이브를 시작하지 않은 경우 자동취소로직 추가' (#30) from test into main
Reviewed-on: #30
2023-09-12 14:09:37 +00:00
615d4baef8 라이브 시작시간 4시간이 지나도 라이브를 시작하지 않은 경우 자동취소로직 추가 2023-09-12 22:50:17 +09:00
f4618877d4 Merge pull request '주문목록 - 크리에이터 닉네임 추가' (#29) from test into main
Reviewed-on: #29
2023-09-08 16:29:30 +00:00
9578e54ea7 라이브 예약 후 시작 시 푸시 2023-09-09 01:24:32 +09:00
5ff288f739 라이브 예약 후 시작 시 푸시 - 테스트 2023-09-09 01:17:05 +09:00
f081b9691f 라이브 예약 후 시작 시 푸시 - 테스트 2023-09-09 01:13:14 +09:00
06bb2ea228 라이브 예약 후 시작 시 푸시 - 테스트 2023-09-09 00:51:38 +09:00
81e2b43231 라이브 예약 후 시작 시 푸시 - 테스트 2023-09-07 22:44:35 +09:00
27c40da7b4 라이브 예약 후 시작 시 푸시 - 테스트 2023-09-07 22:35:08 +09:00
8b30c1c319 라이브 예약 후 시작 시 푸시 - join 제거 2023-09-07 22:18:41 +09:00
300e20dcd0 라이브 생성 푸시 - 개설했습니다. -> 예약했습니다. 2023-09-07 16:48:48 +09:00
0978549675 콘텐츠 댓글 삭제 기능 리팩토링 2023-09-07 16:35:46 +09:00
ab4a2d0e6b 응원 수정 기능 리팩토링 2023-09-07 16:14:15 +09:00
74f7c75012 콘텐츠 댓글 수정 기능 리팩토링 2023-09-06 16:01:14 +09:00
37f2f5e40b 콘텐츠 댓글 수정 기능 리팩토링 2023-09-06 15:54:27 +09:00
056e575e7c 응원글 - memberId 추가 2023-09-05 12:07:08 +09:00
febfa442fa 응원글 - 응원글 삭제로직 추가 2023-09-05 11:18:59 +09:00
27f4d78f0d 콘텐츠 댓글 - 댓글 수정 로직 리팩토링 2023-09-05 11:12:02 +09:00
858f1a9a32 주문목록 - 크리에이터 닉네임 추가 2023-09-04 19:54:58 +09:00
2b914fd222 Merge pull request 'test' (#28) from test into main
Reviewed-on: #28
2023-09-02 16:12:08 +00:00
d68a089235 충전코인 잘못 써지는 버그 수정 2023-09-03 01:03:24 +09:00
f3590dac7a 리워드코인 잘못 써지는 버그 수정 2023-09-03 01:01:13 +09:00
aa457cc2fb suda -> live 2023-09-03 00:45:46 +09:00
7ca71f90db 리워드코인 잘못 써지는 버그 수정 2023-09-03 00:42:26 +09:00
d2ae958847 라이브 예약현황 수정 2023-09-01 18:40:45 +09:00
a285e30108 라이브 취소 푸시 발송 로직 추가 2023-09-01 17:29:57 +09:00
896246d9ed 라이브 취소 푸시 발송 로직 추가 2023-09-01 17:13:32 +09:00
d6dfa63bea 라이브 취소 푸시 발송 로직 추가 2023-09-01 16:46:43 +09:00
8f50d05906 추천채널, 팔로잉 채널 API - 나를 차단한 유저는 표시되지 않도록 수정 2023-08-31 19:49:05 +09:00
88520dbc7f 사용자 차단 해제 기능 추가 2023-08-31 19:45:38 +09:00
109e42a5a3 Merge pull request 'test' (#27) from test into main
Reviewed-on: #27
2023-08-31 08:37:42 +00:00
6aec31711d 첫 충전 이벤트 - 20%에서 15%로 변경 2023-08-31 17:33:52 +09:00
1ba3cb6277 라이브 탭 상단 추천라이브 - 차단 당한 유저는 차단한 유저의 배너가 조회되지 않도록 수정 2023-08-30 22:41:40 +09:00
eece72bc40 라이브 탭 상단 추천라이브 - 차단 당한 유저는 차단한 유저의 배너가 조회되지 않도록 수정 2023-08-30 22:12:44 +09:00
41fe37bdb2 관리자 - 유저 비밀번호 초기화 기능 추가 2023-08-30 21:53:45 +09:00
fa515ad39c Merge pull request '관리자 캔 충전내역 - 애플 인 앱 결제에 PG결제가 같이 나오던 버그 수정' (#26) from test into main
Reviewed-on: #26
2023-08-30 10:02:32 +00:00
57426b5b5b 관리자 캔 충전내역 - 애플 인 앱 결제에 PG결제가 같이 나오던 버그 수정 2023-08-30 18:56:19 +09:00
f09673a795 Merge pull request 'test' (#25) from test into main
Reviewed-on: #25
2023-08-30 08:10:29 +00:00
42f5c49cbc 푸시 발송 - 우선순위와 소리/진동을 위해 ApnsConfig, AndroidConfig 추가 2023-08-30 17:00:48 +09:00
378e8f13af 푸시발송을 위해 @Transactional(readOnly = true) 추가 2023-08-30 16:59:07 +09:00
f71536c614 Merge pull request 'test' (#24) from test into main
Reviewed-on: #24
2023-08-30 07:24:14 +00:00
6cf401539f 유저 차단/차단해제 - 트랜젝션 추가 2023-08-30 12:39:05 +09:00
13761a4130 푸시 - 팔로잉 상태가 아니여도 라이브를 예약하면 푸시를 발송하도록 수정 2023-08-30 12:06:33 +09:00
7bdddc7ae8 Merge pull request '후원 전체보기 - 하단 랭킹에 콘텐츠 후원도 포함' (#23) from test into main
Reviewed-on: #23
2023-08-29 15:22:09 +00:00
fc2737921b 후원 전체보기 - 하단 랭킹에 콘텐츠 후원도 포함 2023-08-30 00:18:59 +09:00
aa8926a624 Merge pull request '후원 전체보기 - 상단 캔 현황 을 후원 캔만 반영하도록 수정' (#22) from test into main
Reviewed-on: #22
2023-08-29 14:53:44 +00:00
4603ea4f96 후원 전체보기 - 상단 캔 현황 을 후원 캔만 반영하도록 수정 2023-08-29 23:48:44 +09:00
be71e59be2 Merge pull request '무료 콘텐츠를 못올리는 버그 수정' (#21) from test into main
Reviewed-on: #21
2023-08-29 13:32:13 +00:00
bd09bfacd0 무료 콘텐츠를 못올리는 버그 수정 2023-08-29 22:27:04 +09:00
4d7753378f Merge pull request 'test' (#20) from test into main
Reviewed-on: #20
2023-08-29 07:33:57 +00:00
ed78959817 응원 전체보기 API 추가 2023-08-29 15:24:55 +09:00
ad66be3449 후원랭킹 전체보기 API 추가 2023-08-29 11:17:06 +09:00
00a6fa0c91 후원랭킹 전체보기 API 추가 2023-08-29 00:02:00 +09:00
f25888c88e 후원랭킹 전체보기 API 추가 2023-08-28 20:53:14 +09:00
1629bc8bf7 콘텐츠 가격 - 5캔(500원) 부터 등록할 수 있도록 수정 2023-08-28 18:22:16 +09:00
29c9460362 콘텐츠 대여 가격 - 소장 가격의 60%로 변경 2023-08-28 18:12:09 +09:00
60257c4ef4 Merge pull request 'test' (#19) from test into main
Reviewed-on: #19
2023-08-28 08:54:08 +00:00
7bb7e92137 콘텐츠 댓글 알림 추가 2023-08-28 16:12:32 +09:00
cd833dc21d 콘텐츠 댓글 알림 추가 2023-08-28 15:26:27 +09:00
1e0b79bf62 Merge pull request 'test' (#18) from test into main
Reviewed-on: #18
2023-08-27 12:28:42 +00:00
fb8309c7b4 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 21:22:42 +09:00
779c3f824e 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 21:09:57 +09:00
2abfb8ce58 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 20:58:03 +09:00
9046c10d10 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 20:50:18 +09:00
a9d3427b6f 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 20:32:30 +09:00
2e249db5f5 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 20:25:14 +09:00
f2ecf3a676 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 20:10:23 +09:00
53bc8ed05a 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 19:43:54 +09:00
c5fd55a6f8 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 19:35:04 +09:00
586f4d21d4 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 19:24:58 +09:00
0d56b61b2e 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 19:13:22 +09:00
3b9d8a4fcb 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 19:02:03 +09:00
72adae3f54 충전이벤트가 적용되지 않는 버그 수정 2023-08-27 18:43:22 +09:00
d1ea7b8704 크리에이터 채널 공지사항 변경 푸시 - 내용변경 2023-08-25 23:25:38 +09:00
41a6e05034 크리에이터 채널 공지사항 변경 시 푸시 발송 2023-08-25 22:42:23 +09:00
6883434d0d Merge pull request '유저 관심사, 라이브 관심사 - 연령제한 설정 추가' (#17) from test into main
Reviewed-on: #17
2023-08-25 08:38:51 +00:00
0679fdfb6d 유저 관심사, 라이브 관심사 - 연령제한 설정 추가 2023-08-25 17:31:55 +09:00
eda2193e64 Merge pull request 'test' (#16) from test into main
Reviewed-on: #16
2023-08-25 07:50:55 +00:00
118c5c2eed 유저 관심사, 라이브 관심사 - 연령제한 설정 추가 2023-08-25 16:39:40 +09:00
d865ec3ef8 유저 관심사, 라이브 관심사 - 연령제한 설정 추가 2023-08-25 16:33:03 +09:00
7af970ace4 유저 관심사, 라이브 관심사 - 연령제한 설정 추가 2023-08-25 16:15:59 +09:00
99bf829c88 Merge pull request '첫 충전 이벤트 - 본인인증한 전체 계정 중 첫 충전 시에만 첫충전 이벤트 적용' (#15) from test into main
Reviewed-on: #15
2023-08-24 16:26:54 +00:00
65e0b87d79 첫 충전 이벤트 - 본인인증한 전체 계정 중 첫 충전 시에만 첫충전 이벤트 적용 2023-08-25 01:17:06 +09:00
eab9ac5f05 첫 충전 이벤트 - 본인인증한 전체 계정 중 첫 충전 시에만 첫충전 이벤트 적용 2023-08-25 01:09:11 +09:00
5feafe1b48 Merge pull request 'test' (#14) from test into main
Reviewed-on: #14
2023-08-24 14:54:37 +00:00
d414e3b300 충전이벤트 적용 2023-08-24 22:13:11 +09:00
a47b40d922 주문 리포지토리 - 콘텐츠 메인 구매리스트 쿼리 수정 2023-08-24 18:30:44 +09:00
9126671581 콘텐츠 등록완료 - 콘텐츠 등록완료 시 콘텐츠를 올린 크리에이터에게 푸시메시지 발송 2023-08-24 18:03:06 +09:00
c9292b7d04 Merge pull request 'test' (#13) from test into main
Reviewed-on: #13
2023-08-23 14:05:00 +00:00
eaeca443cf 크리에이터 관리자 콘텐츠 조회 - 댓글 등록 가능 여부 추가 2023-08-23 22:38:57 +09:00
f49203cafc 크리에이터 관리자 - 콘텐츠 변경 요청 데이터 변경 2023-08-23 21:38:04 +09:00
307c09a4c1 크리에이터 관리자 - 패키지명 변경 2023-08-23 14:57:25 +09:00
c3cb8fe93b 크리에이터 관리자 - 콘텐츠 리스트, 로그인 API 추가 2023-08-23 00:06:09 +09:00
ae7e1a91c1 Merge pull request '캔 충전로직 수정' (#12) from test into main
Reviewed-on: #12
2023-08-21 15:12:44 +00:00
d0aebe906b 캔 충전로직 수정 2023-08-22 00:08:09 +09:00
3e1887e0d1 Merge pull request 'test' (#11) from test into main
Reviewed-on: #11
2023-08-21 13:19:03 +00:00
fa8d8c4ab1 음성메시지 - 로딩 버그 수정 2023-08-21 22:14:40 +09:00
ff81c6c5ca 요즘라이브 -> 소다라이브 2023-08-21 05:56:06 +09:00
474646db47 Merge pull request '스피커 최대 10 -> 5명으로 수정' (#10) from test into main
Reviewed-on: #10
2023-08-20 20:40:23 +00:00
4900b52d2b 스피커 최대 10 -> 5명으로 수정 2023-08-21 05:36:23 +09:00
56f7b6c449 Merge pull request 'test' (#9) from test into main
Reviewed-on: #9
2023-08-20 19:22:23 +00:00
82a6161abf 라이브 상세 - 응답값 19금 여부 추가 2023-08-21 04:04:56 +09:00
077af3a46d 푸시 발송 - token이 있는 경우에만 발송하도록 수정 2023-08-21 03:16:23 +09:00
498d9c4893 푸시 토큰 조회 - 푸시 토큰 != null 로직 추가 2023-08-21 03:15:04 +09:00
2410d77cb7 푸시 - 개별발송 로직 수정 2023-08-21 02:31:17 +09:00
40bdd5ba1c 푸시 - os 로그 추가 2023-08-21 01:11:15 +09:00
629528b6cf 푸시 - 보내기 성공/실패 로그 추가 2023-08-21 01:10:05 +09:00
76b2b5f7e3 Merge pull request '캔 사용내역 - 후원을 콘텐츠 후원, 라이브 후원으로 분리' (#8) from test into main
Reviewed-on: #8
2023-08-20 15:46:30 +00:00
a8c3f05ffa 캔 사용내역 - 후원을 콘텐츠 후원, 라이브 후원으로 분리 2023-08-21 00:40:06 +09:00
e918d809eb Merge pull request '충전내역 - 관리자 지급 추가' (#7) from test into main
Reviewed-on: #7
2023-08-20 15:08:35 +00:00
2a24c376b3 충전내역 - 관리자 지급 추가 2023-08-20 23:49:36 +09:00
7af059e543 Merge pull request 'test' (#6) from test into main
Reviewed-on: #6
2023-08-20 14:45:39 +00:00
73988d7b2c 라이브 정보 - 19금 여부 추가 2023-08-20 23:27:42 +09:00
1d217b9f75 fcm - ios 일 경우 notification 추가 2023-08-20 22:51:06 +09:00
3ad2256a66 마이페이지 - 라이브 예약 수, 구매내역 추가 2023-08-20 22:42:35 +09:00
897726e1ec Merge pull request 'test' (#5) from test into main
Reviewed-on: #5
2023-08-19 17:47:16 +00:00
91d25081c0 크리에이터 채널 - 콘텐츠 리스트 추가 2023-08-20 02:42:59 +09:00
dde4eb4a98 크리에이터 채널 - 콘텐츠 수, 라이브 리스트 나오도록 수정 2023-08-20 00:26:14 +09:00
8b98a2dd07 Merge pull request '비밀번호 찾기 API 추가' (#4) from test into main
Reviewed-on: #4
2023-08-19 07:05:17 +00:00
248e57b08c 비밀번호 찾기 API 추가 2023-08-19 03:21:39 +09:00
cca75420f0 Merge pull request '큐레이션 - 조건 추가' (#3) from test into main
Reviewed-on: #3
2023-08-18 14:07:34 +00:00
1884e5a5d9 큐레이션 - 조건 추가 2023-08-18 23:02:28 +09:00
86c627ed1d Merge pull request 'test' (#2) from test into main
Reviewed-on: #2
2023-08-18 12:54:09 +00:00
409af8b18c 큐레이션 - member join 추가 2023-08-18 21:49:26 +09:00
7b58335a42 닉네임 변경 로그 엔티티 - updated_at 제거 2023-08-18 19:25:58 +09:00
25be1a6adc 프로필 수정 API 2023-08-18 19:15:19 +09:00
d55514e3a7 Merge pull request 'test' (#1) from test into main
Reviewed-on: #1
2023-08-16 02:30:36 +00:00
9236aece18 manager -> creator 2023-08-15 00:12:27 +09:00
c84a9bc473 라이브 정보 - 필요없는 부분 제거 2023-08-14 22:49:41 +09:00
ba69f86295 라이브 상세 - dateformat 원상복구 2023-08-14 19:14:51 +09:00
6f40838d09 라이브 상세 - dateformat에 locale 추가 2023-08-14 18:58:16 +09:00
4bf8617102 coin -> can 으로 변경 2023-08-13 20:24:48 +09:00
c8764be69f coin -> can 으로 변경 2023-08-13 19:47:28 +09:00
c9970ce7ca 불필요한 logger 제거 2023-08-09 12:17:37 +09:00
c876378b21 푸시 발송 대상 유저 조회 로직 수정 2023-08-09 11:56:15 +09:00
0fa16762bd debug logger 2023-08-09 10:59:36 +09:00
e5531bbef7 debug logger 2023-08-09 10:43:39 +09:00
ccb32a73dd 푸시 발송 이벤트 - container가 필요한 부분에 container 추가 2023-08-09 10:36:40 +09:00
2548c92aa1 특정 유저 푸시 적용 2023-08-09 10:29:08 +09:00
b331e6e152 TransactionalEventListener -> EventListener 변경 2023-08-09 10:13:28 +09:00
11f1f781a0 EventListener -> TransactionalEventListener 변경 2023-08-09 10:06:06 +09:00
eb09251955 debug logger 추가 2023-08-09 09:16:27 +09:00
777f4755be 팔로잉 채널 전체보기 API 추가 2023-08-09 07:48:43 +09:00
ba3444fd26 라이브 메인 - 팔로잉 채널 API 추가 2023-08-09 07:14:54 +09:00
e9723d37ba 차단유저 필터링 로직 추가 2023-08-09 06:54:25 +09:00
705bf0b6b2 푸시메시지 기능 추가 - 전체, 개별, 라이브 생성, 라이브 시작, 메시지 전송, 콘텐츠 업로드 2023-08-08 16:46:30 +09:00
771dbeced0 레디스 JWT 토큰 저장소 - tokenList 가 계속 null 이 되면서 초기화가 되지 않아 set 으로 변경하고 default value 를 이용한 초기화로 변경 2023-08-08 12:47:49 +09:00
f7cdf40976 관리자 - 탐색 메뉴 API 추가 2023-08-07 18:20:00 +09:00
14220ff6dc 관리자 - 회원 타입 수정 기능 추가 2023-08-07 15:36:22 +09:00
b556219e36 라이브 입장 - 퇴장 횟수 체크, 차단된 유저 체크 2023-08-07 14:47:00 +09:00
6ff38decab account -> member 2023-08-07 14:46:31 +09:00
872e84baf1 토큰저장소 -> mutableList로 변경 2023-08-07 14:36:50 +09:00
581b6975a3 토큰저장소 -> mutableList로 변경 2023-08-07 14:30:49 +09:00
b99ab9103d ReportType - 사용하지 않는 Review 제거 2023-08-07 14:18:52 +09:00
34590347a6 관리자 - 추천 라이브 크리에이터 API 2023-08-07 03:09:44 +09:00
14b25bdfc3 관리자 - 콘텐츠 리스트, 콘텐츠 배너관리, 콘텐츠 큐레이션 관리 API 2023-08-07 01:23:42 +09:00
7696f06fbd 관리자 - 라이브 리스트 API 2023-08-06 22:50:57 +09:00
38f6e8d870 관리자 - 라이브 관심사 등록/수정/삭제 API 2023-08-06 22:42:50 +09:00
12c9b14168 관리자 - 충전 이벤트 API 2023-08-06 22:30:46 +09:00
dc299f7727 관리자 - 이벤트 API 2023-08-06 22:18:45 +09:00
a983595bad 관리자 - FAQ API 2023-08-06 22:10:33 +09:00
87a5ceee9c 관리자 - 개인정보처리방침, 이용약관 API 2023-08-06 22:03:25 +09:00
3d514e8ad4 관리자 - 크리에이터 리스트 API 2023-08-06 14:40:44 +09:00
94551b05ff 관리자 - 캔, 충전현황 API 2023-08-06 14:29:36 +09:00
841e32a50b 관리자 - 회원 태그 API 2023-08-06 14:01:09 +09:00
cbcc63dc71 관리자 - 회원리스트 API 2023-08-06 11:13:27 +09:00
9b4e2fd192 cloudfront - https 제거 2023-08-05 00:58:45 +09:00
c9a9c8c310 새로운 콘텐츠를 올린 크리에이터 - 프로필 이미지가 null인 버그 수정 2023-08-05 00:15:05 +09:00
737 changed files with 41426 additions and 1258 deletions

View File

@@ -7,5 +7,5 @@ indent_size = 4
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
max_line_length = 130
tab_width = 4

3
.gitignore vendored
View File

@@ -323,4 +323,7 @@ gradle-app.setting
### Gradle Patch ###
**/build/
.kiro/
.junie
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij,java,kotlin,macos,windows,eclipse,gradle

View File

@@ -26,11 +26,14 @@ repositories {
}
dependencies {
implementation("org.redisson:redisson-spring-data-27:3.19.2")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.retry:spring-retry")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// jwt
@@ -44,6 +47,7 @@ dependencies {
kapt("org.springframework.boot:spring-boot-configuration-processor")
// aws
implementation("com.amazonaws:aws-java-sdk-sqs:1.12.380")
implementation("com.amazonaws:aws-java-sdk-ses:1.12.380")
implementation("com.amazonaws:aws-java-sdk-s3:1.12.380")
implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380")
@@ -55,6 +59,20 @@ dependencies {
implementation("org.json:json:20230227")
implementation("com.google.code.findbugs:jsr305:3.0.2")
// firebase admin sdk
implementation("com.google.firebase:firebase-admin:9.2.0")
// android publisher
implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0")
implementation("com.google.api-client:google-api-client:1.32.1")
implementation("org.apache.poi:poi-ooxml:5.2.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
// file mimetype check
implementation("org.apache.tika:tika-core:3.2.0")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")

View File

@@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.retry.annotation.EnableRetry
import org.springframework.scheduling.annotation.EnableAsync
@SpringBootApplication
@EnableAsync
@EnableRetry
class SodaLiveApplication
fun main(args: Array<String>) {

View File

@@ -0,0 +1,43 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audition")
class AdminAuditionController(private val service: AdminAuditionService) {
@PostMapping
fun createAudition(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAudition(image, requestString), "등록되었습니다.")
@PutMapping
fun updateAudition(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAudition(image, requestString), "수정되었습니다.")
@GetMapping
fun getAuditionList(pageable: Pageable) = ApiResponse.ok(
service.getAuditionList(
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
@GetMapping("/{id}")
fun getAuditionDetail(@PathVariable id: Long) = ApiResponse.ok(
service.getAuditionDetail(auditionId = id)
)
}

View File

@@ -0,0 +1,69 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.audition.QAudition.audition
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminAuditionRepository : JpaRepository<Audition, Long>, AdminAuditionQueryRepository
interface AdminAuditionQueryRepository {
fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem>
fun getAuditionListCount(): Int
fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData
}
class AdminAuditionQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) : AdminAuditionQueryRepository {
override fun getAuditionList(offset: Long, limit: Long): List<GetAuditionListItem> {
return queryFactory
.select(
QGetAuditionListItem(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(coverImageHost),
audition.isAdult,
audition.information,
audition.status,
audition.originalWorkUrl.coalesce("")
)
)
.from(audition)
.where(audition.isActive.isTrue)
.offset(offset)
.limit(limit)
.orderBy(audition.isActive.desc(), audition.id.desc())
.fetch()
}
override fun getAuditionListCount(): Int {
return queryFactory
.select(audition.id)
.from(audition)
.fetch()
.size
}
override fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData {
return queryFactory
.select(
QGetAuditionDetailRawData(
audition.id,
audition.title,
audition.imagePath.prepend("/").prepend(coverImageHost),
audition.information,
audition.originalWorkUrl.coalesce("")
)
)
.from(audition)
.where(audition.id.eq(auditionId))
.fetchFirst()
}
}

View File

@@ -0,0 +1,122 @@
package kr.co.vividnext.sodalive.admin.audition
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.audition.role.AdminAuditionRoleRepository
import kr.co.vividnext.sodalive.audition.AuditionStatus
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminAuditionService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val repository: AdminAuditionRepository,
private val roleRepository: AdminAuditionRoleRepository,
private val applicationEventPublisher: ApplicationEventPublisher,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAudition(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateAuditionRequest::class.java)
val audition = repository.save(request.toAudition())
val fileName = generateFileName("audition")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/production/${audition.id}/$fileName"
)
audition.imagePath = imagePath
}
@Transactional
fun updateAudition(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRequest::class.java)
val audition = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
if (request.title != null) {
audition.title = request.title
}
if (request.information != null) {
audition.information = request.information
}
if (request.isAdult != null) {
audition.isAdult = request.isAdult
}
if (request.status != null) {
if (
(audition.status == AuditionStatus.COMPLETED || audition.status == AuditionStatus.IN_PROGRESS) &&
request.status == AuditionStatus.NOT_STARTED
) {
throw SodaException("모집전 상태로 변경할 수 없습니다.")
}
audition.status = request.status
}
if (request.originalWorkUrl != null) {
audition.originalWorkUrl = request.originalWorkUrl
}
if (image != null) {
val fileName = generateFileName("audition")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/production/${audition.id}/$fileName"
)
audition.imagePath = imagePath
}
if (request.isActive != null) {
audition.isActive = request.isActive
}
if (request.status != null && request.status == AuditionStatus.IN_PROGRESS && audition.isActive) {
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.IN_PROGRESS_AUDITION,
title = "새로운 오디션 등록!",
message = "'${audition.title}'이 등록되었습니다. 지금 바로 오리지널 오디오 드라마 오디션에 지원해보세요!",
isAuth = audition.isAdult,
auditionId = audition.id ?: -1
)
)
}
}
fun getAuditionList(offset: Long, limit: Long): GetAuditionListResponse {
val totalCount = repository.getAuditionListCount()
val items = repository.getAuditionList(offset = offset, limit = limit)
return GetAuditionListResponse(totalCount, items)
}
fun getAuditionDetail(auditionId: Long): GetAuditionDetailResponse {
val auditionDetail = repository.getAuditionDetail(auditionId = auditionId)
val roleList = roleRepository.getAuditionRoleListByAuditionId(auditionId = auditionId)
return GetAuditionDetailResponse(
id = auditionDetail.id,
title = auditionDetail.title,
imageUrl = auditionDetail.imageUrl,
information = auditionDetail.information,
originalWorkUrl = auditionDetail.originalWorkUrl,
roleList = roleList
)
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.audition.Audition
import kr.co.vividnext.sodalive.common.SodaException
data class CreateAuditionRequest(
val title: String,
val information: String,
val isAdult: Boolean = false,
val originalWorkUrl: String? = null
) {
init {
if (title.isBlank()) {
throw SodaException("오디션 제목을 입력하세요")
}
if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 정보는 최소 10글자 입니다")
}
}
fun toAudition(): Audition {
return Audition(
title = title,
information = information,
isAdult = isAdult,
originalWorkUrl = originalWorkUrl
)
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class GetAuditionDetailRawData @QueryProjection constructor(
val id: Long,
val title: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String
)
data class GetAuditionDetailResponse(
val id: Long,
val title: String,
val imageUrl: String,
val information: String,
val originalWorkUrl: String,
val roleList: List<GetAuditionRoleListData>
)
data class GetAuditionRoleListData @QueryProjection constructor(
val id: Long,
val name: String,
val imageUrl: String,
val information: String,
val auditionScriptUrl: String,
val status: AuditionStatus
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.audition
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class GetAuditionListResponse(
val totalCount: Int,
val items: List<GetAuditionListItem>
)
data class GetAuditionListItem @QueryProjection constructor(
val id: Long,
val title: String,
val imageUrl: String,
val isAdult: Boolean,
val information: String,
val status: AuditionStatus,
val originalWorkUrl: String
)

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.audition
import kr.co.vividnext.sodalive.audition.AuditionStatus
data class UpdateAuditionRequest(
val id: Long,
val title: String? = null,
val information: String? = null,
val isAdult: Boolean? = null,
val status: AuditionStatus? = null,
val originalWorkUrl: String? = null,
val isActive: Boolean? = null
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.audition.applicant
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audition/applicant")
class AdminAuditionApplicantController(private val service: AdminAuditionApplicantService) {
@DeleteMapping("/{id}")
fun deleteAuditionApplicant(@PathVariable id: Long) = ApiResponse.ok(
service.deleteAuditionApplicant(id),
"오디션 지원이 취소 되었습니다."
)
}

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.audition.applicant
import kr.co.vividnext.sodalive.audition.AuditionApplicant
import org.springframework.data.jpa.repository.JpaRepository
interface AdminAuditionApplicantRepository : JpaRepository<AuditionApplicant, Long>

View File

@@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.admin.audition.applicant
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminAuditionApplicantService(private val repository: AdminAuditionApplicantRepository) {
@Transactional
fun deleteAuditionApplicant(id: Long) {
val applicant = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
applicant.isActive = false
}
}

View File

@@ -0,0 +1,47 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audition/role")
class AdminAuditionRoleController(private val service: AdminAuditionRoleService) {
@PostMapping
fun createAuditionRole(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAuditionRole(image, requestString), "등록되었습니다.")
@PutMapping
fun updateAuditionRole(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAuditionRole(image, requestString), "수정되었습니다.")
@GetMapping("/{id}")
fun getAuditionRoleDetail(@PathVariable id: Long) = ApiResponse.ok(
service.getAuditionRoleDetail(auditionRoleId = id)
)
@GetMapping("/{id}/applicant")
fun getAuditionApplicantList(
@PathVariable id: Long,
pageable: Pageable
) = ApiResponse.ok(
service.getAuditionApplicantList(
auditionRoleId = id,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}

View File

@@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.audition.GetAuditionRoleListData
import kr.co.vividnext.sodalive.admin.audition.QGetAuditionRoleListData
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.audition.QAudition.audition
import kr.co.vividnext.sodalive.audition.QAuditionApplicant.auditionApplicant
import kr.co.vividnext.sodalive.audition.QAuditionRole.auditionRole
import kr.co.vividnext.sodalive.audition.QAuditionVote.auditionVote
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AdminAuditionRoleRepository : JpaRepository<AuditionRole, Long>, AdminAuditionRoleQueryRepository
interface AdminAuditionRoleQueryRepository {
fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData>
fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse
fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): List<GetAuditionRoleApplicantItem>
fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int
}
class AdminAuditionRoleQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudfrontHost: String
) : AdminAuditionRoleQueryRepository {
override fun getAuditionRoleListByAuditionId(auditionId: Long): List<GetAuditionRoleListData> {
return queryFactory
.select(
QGetAuditionRoleListData(
auditionRole.id,
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.information,
auditionRole.auditionScriptUrl,
auditionRole.status
)
)
.from(auditionRole)
.innerJoin(auditionRole.audition, audition)
.where(
auditionRole.audition.id.eq(auditionId),
auditionRole.isActive.isTrue
)
.fetch()
}
override fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse {
return queryFactory
.select(
QGetAuditionRoleDetailResponse(
auditionRole.name,
auditionRole.imagePath.prepend("/").prepend(cloudfrontHost),
auditionRole.information,
auditionRole.auditionScriptUrl
)
)
.from(auditionRole)
.where(auditionRole.id.eq(auditionRoleId))
.fetchFirst()
}
override fun getAuditionApplicantList(
auditionRoleId: Long,
offset: Long,
limit: Long
): List<GetAuditionRoleApplicantItem> {
return queryFactory
.select(
QGetAuditionRoleApplicantItem(
auditionApplicant.id,
member.nickname,
member.profileImage.prepend("/").prepend(cloudfrontHost),
auditionApplicant.phoneNumber,
auditionApplicant.voicePath.prepend("/").prepend(cloudfrontHost),
auditionVote.id.count()
)
)
.from(auditionApplicant)
.innerJoin(auditionApplicant.member, member)
.innerJoin(auditionApplicant.role, auditionRole)
.leftJoin(auditionVote).on(auditionApplicant.id.eq(auditionVote.applicant.id))
.where(
auditionRole.id.eq(auditionRoleId),
auditionApplicant.isActive.isTrue
)
.groupBy(auditionApplicant.id)
.orderBy(auditionVote.id.count().desc())
.offset(offset)
.limit(limit)
.fetch()
}
override fun getAuditionApplicantTotalCount(auditionRoleId: Long): Int {
return queryFactory
.select(auditionApplicant.id)
.from(auditionApplicant)
.innerJoin(auditionApplicant.role, auditionRole)
.where(auditionRole.id.eq(auditionRoleId))
.fetch()
.size
}
}

View File

@@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.audition.AdminAuditionRepository
import kr.co.vividnext.sodalive.audition.AuditionRole
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminAuditionRoleService(
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val repository: AdminAuditionRoleRepository,
private val auditionRepository: AdminAuditionRepository,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAuditionRole(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateAuditionRoleRequest::class.java)
val auditionRole = AuditionRole(
name = request.name,
information = request.information,
auditionScriptUrl = request.auditionScriptUrl
)
val audition = auditionRepository.findByIdOrNull(id = request.auditionId)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
auditionRole.audition = audition
repository.save(auditionRole)
val fileName = generateFileName("audition_role")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/role/${auditionRole.id}/$fileName"
)
auditionRole.imagePath = imagePath
}
@Transactional
fun updateAuditionRole(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateAuditionRoleRequest::class.java)
val auditionRole = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
if (!request.name.isNullOrBlank()) {
if (request.name.length < 2) throw SodaException("배역 이름은 최소 2글자 입니다")
auditionRole.name = request.name
}
if (!request.information.isNullOrBlank()) {
if (request.information.length < 10) throw SodaException("오디션 배역 정보는 최소 10글자 입니다")
auditionRole.information = request.information
}
if (!request.auditionScriptUrl.isNullOrBlank()) {
auditionRole.auditionScriptUrl = request.auditionScriptUrl
}
if (request.status != null) {
auditionRole.status = request.status
}
if (request.isActive != null) {
auditionRole.isActive = request.isActive
}
if (image != null) {
val fileName = generateFileName("audition_role")
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audition/role/${auditionRole.id}/$fileName"
)
auditionRole.imagePath = imagePath
}
}
fun getAuditionRoleDetail(auditionRoleId: Long): GetAuditionRoleDetailResponse {
return repository.getAuditionRoleDetail(auditionRoleId = auditionRoleId)
}
fun getAuditionApplicantList(auditionRoleId: Long, offset: Long, limit: Long): GetAuditionRoleApplicantResponse {
val totalCount = repository.getAuditionApplicantTotalCount(auditionRoleId = auditionRoleId)
val items = repository.getAuditionApplicantList(auditionRoleId = auditionRoleId, offset = offset, limit = limit)
return GetAuditionRoleApplicantResponse(totalCount, items)
}
}

View File

@@ -0,0 +1,28 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.common.SodaException
data class CreateAuditionRoleRequest(
val auditionId: Long,
val name: String,
val information: String,
val auditionScriptUrl: String
) {
init {
if (auditionId < 0) {
throw SodaException("캐릭터가 등록될 오디션을 선택하세요")
}
if (name.isBlank() || name.length < 2) {
throw SodaException("캐릭터명을 입력하세요")
}
if (auditionScriptUrl.isBlank() || auditionScriptUrl.length < 10) {
throw SodaException("오디션 대본 URL을 입력하세요")
}
if (information.isBlank() || information.length < 10) {
throw SodaException("오디션 캐릭터 정보는 최소 10글자 입니다")
}
}
}

View File

@@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionRoleApplicantResponse(
val totalCount: Int,
val items: List<GetAuditionRoleApplicantItem>
)
data class GetAuditionRoleApplicantItem @QueryProjection constructor(
val applicantId: Long,
val nickname: String,
val profileImageUrl: String,
val phoneNumber: String,
val voiceUrl: String,
val voteCount: Long
)

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.admin.audition.role
import com.querydsl.core.annotations.QueryProjection
data class GetAuditionRoleDetailResponse @QueryProjection constructor(
val name: String,
val imageUrl: String,
val information: String,
val auditionScriptUrl: String
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.audition.role
import kr.co.vividnext.sodalive.audition.AuditionStatus
import kr.co.vividnext.sodalive.common.SodaException
data class UpdateAuditionRoleRequest(
val id: Long,
val name: String? = null,
val information: String? = null,
val auditionScriptUrl: String? = null,
val status: AuditionStatus? = null,
val isActive: Boolean? = null
) {
init {
if (id < 0) {
throw SodaException("잘못된 요청입니다.")
}
}
}

View File

@@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.admin.calculate
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/calculate")
class AdminCalculateController(private val service: AdminCalculateService) {
@GetMapping("/live")
fun getCalculateLive(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
) = ApiResponse.ok(service.getCalculateLive(startDateStr, endDateStr))
@GetMapping("/content-list")
fun getCalculateContentList(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
) = ApiResponse.ok(service.getCalculateContentList(startDateStr, endDateStr))
@GetMapping("/cumulative-sales-by-content")
fun getCumulativeSalesByContent(pageable: Pageable) = ApiResponse.ok(
service.getCumulativeSalesByContent(pageable.offset, pageable.pageSize.toLong())
)
@GetMapping("/content-donation-list")
fun getCalculateContentDonationList(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
) = ApiResponse.ok(service.getCalculateContentDonationList(startDateStr, endDateStr))
@GetMapping("/community-post")
fun getCalculateCommunityPost(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateCommunityPost(
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/live-by-creator")
fun getCalculateLiveByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateLiveByCreator(
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/content-by-creator")
fun getCalculateContentByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateContentByCreator(
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
@GetMapping("/community-by-creator")
fun getCalculateCommunityByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
pageable: Pageable
) = ApiResponse.ok(
service.getCalculateCommunityByCreator(
startDateStr,
endDateStr,
pageable.offset,
pageable.pageSize.toLong()
)
)
}

View File

@@ -0,0 +1,398 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.DateTimePath
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.StringTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.calculate.ratio.QCreatorSettlementRatio.creatorSettlementRatio
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity.creatorCommunity
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getCalculateLive(startDate: LocalDateTime, endDate: LocalDateTime): List<GetCalculateLiveQueryData> {
val formattedDate = getFormattedDate(liveRoom.beginDateTime)
return queryFactory
.select(
QGetCalculateLiveQueryData(
member.email,
member.nickname,
formattedDate,
liveRoom.title,
liveRoom.price,
useCan.canUsage,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.liveSettlementRatio
)
)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(liveRoom.id, useCan.canUsage, creatorSettlementRatio.liveSettlementRatio)
.orderBy(member.nickname.desc(), liveRoom.id.desc(), useCan.canUsage.desc(), formattedDate.desc())
.fetch()
}
fun getCalculateContentList(startDate: LocalDateTime, endDate: LocalDateTime): List<GetCalculateContentQueryData> {
val orderFormattedDate = getFormattedDate(order.createdAt)
val pointGroup = CaseBuilder()
.`when`(order.point.loe(0)).then(0)
.otherwise(1)
return queryFactory
.select(
QGetCalculateContentQueryData(
member.nickname,
audioContent.title,
getFormattedDate(audioContent.createdAt),
orderFormattedDate,
order.type,
order.can,
order.id.count(),
order.can.sum(),
order.point.sum(),
creatorSettlementRatio.contentSettlementRatio
)
)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
)
.groupBy(
audioContent.id,
order.type,
orderFormattedDate,
order.can,
pointGroup,
creatorSettlementRatio.contentSettlementRatio
)
.orderBy(member.id.desc(), orderFormattedDate.desc(), audioContent.id.asc())
.fetch()
}
private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): StringTemplate {
return Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
dateTimePath,
"UTC",
"Asia/Seoul"
),
"%Y-%m-%d"
)
}
fun getCumulativeSalesByContentTotalCount(): Int {
return queryFactory
.select(audioContent.id)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.where(order.isActive.isTrue)
.groupBy(member.id, audioContent.id, order.can)
.fetch()
.size
}
fun getCumulativeSalesByContent(offset: Long, limit: Long): List<GetCumulativeSalesByContentQueryData> {
val pointGroup = CaseBuilder()
.`when`(order.point.loe(0)).then(0)
.otherwise(1)
return queryFactory
.select(
QGetCumulativeSalesByContentQueryData(
member.nickname,
audioContent.title,
getFormattedDate(audioContent.createdAt),
order.type,
order.can,
order.id.count(),
order.can.sum(),
order.point.sum(),
creatorSettlementRatio.contentSettlementRatio
)
)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(order.isActive.isTrue)
.groupBy(
member.id,
audioContent.id,
order.type,
order.can,
pointGroup,
creatorSettlementRatio.contentSettlementRatio
)
.offset(offset)
.limit(limit)
.orderBy(member.id.desc(), audioContent.id.desc())
.fetch()
}
fun getCalculateContentDonationList(
startDate: LocalDateTime,
endDate: LocalDateTime
): List<GetCalculateContentDonationQueryData> {
val donationFormattedDate = getFormattedDate(useCan.createdAt)
return queryFactory
.select(
QGetCalculateContentDonationQueryData(
member.nickname,
audioContent.title,
audioContent.price,
getFormattedDate(audioContent.createdAt),
donationFormattedDate,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum()
)
)
.from(useCan)
.innerJoin(useCan.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.DONATION))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(donationFormattedDate, audioContent.id)
.orderBy(member.id.asc(), donationFormattedDate.desc(), audioContent.id.desc())
.fetch()
}
fun getCalculateCommunityPostTotalCount(startDate: LocalDateTime?, endDate: LocalDateTime?): Int {
val formattedDate = getFormattedDate(useCan.createdAt)
return queryFactory
.select(creatorCommunity.id)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(formattedDate, creatorCommunity.id)
.fetch()
.size
}
fun getCalculateCommunityPostList(
startDate: LocalDateTime?,
endDate: LocalDateTime?,
offset: Long,
limit: Long
): List<GetCalculateCommunityPostQueryData> {
val formattedDate = getFormattedDate(useCan.createdAt)
return queryFactory
.select(
QGetCalculateCommunityPostQueryData(
member.nickname,
Expressions.stringTemplate("substring({0}, 1, 10)", creatorCommunity.content),
formattedDate,
creatorCommunity.price,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.communitySettlementRatio
)
)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(formattedDate, creatorCommunity.id, creatorSettlementRatio.communitySettlementRatio)
.orderBy(member.id.asc(), formattedDate.desc(), creatorCommunity.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getCalculateLiveByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.fetch()
.size
}
fun getCalculateLiveByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
limit: Long
): List<GetCalculateByCreatorQueryData> {
return queryFactory
.select(
QGetCalculateByCreatorQueryData(
member.email,
member.nickname,
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.liveSettlementRatio
)
)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id, creatorSettlementRatio.liveSettlementRatio)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getCalculateContentByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
)
.groupBy(member.id)
.fetch()
.size
}
fun getCalculateContentByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
limit: Long
): List<GetCalculateByCreatorQueryData> {
return queryFactory
.select(
QGetCalculateByCreatorQueryData(
member.email,
member.nickname,
order.can.sum(),
creatorSettlementRatio.contentSettlementRatio
)
)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getCalculateCommunityByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.fetch()
.size
}
fun getCalculateCommunityByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
limit: Long
): List<GetCalculateByCreatorQueryData> {
return queryFactory
.select(
QGetCalculateByCreatorQueryData(
member.email,
member.nickname,
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.communitySettlementRatio
)
)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.leftJoin(creatorSettlementRatio)
.on(member.id.eq(creatorSettlementRatio.member.id))
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id, creatorSettlementRatio.communitySettlementRatio)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
}

View File

@@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.admin.calculate
import kr.co.vividnext.sodalive.creator.admin.calculate.GetCreatorCalculateCommunityPostResponse
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminCalculateService(private val repository: AdminCalculateQueryRepository) {
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_hours"],
key = "'calculateLive:' + " + "#startDateStr + ':' + #endDateStr"
)
fun getCalculateLive(startDateStr: String, endDateStr: String): List<GetCalculateLiveResponse> {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
return repository
.getCalculateLive(startDate, endDate)
.map { it.toGetCalculateLiveResponse() }
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_hours"],
key = "'calculateContent:' + " + "#startDateStr + ':' + #endDateStr"
)
fun getCalculateContentList(startDateStr: String, endDateStr: String): List<GetCalculateContentResponse> {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
return repository
.getCalculateContentList(startDate, endDate)
.map { it.toGetCalculateContentResponse() }
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_hours"],
key = "'cumulativeSalesByContent:' + " + "#offset + ':' + #limit"
)
fun getCumulativeSalesByContent(offset: Long, limit: Long): GetCumulativeSalesByContentResponse {
val totalCount = repository.getCumulativeSalesByContentTotalCount()
val items = repository
.getCumulativeSalesByContent(offset, limit)
.map { it.toCumulativeSalesByContentItem() }
return GetCumulativeSalesByContentResponse(totalCount, items)
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_hours"],
key = "'calculateContentDonationList2:' + " + "#startDateStr + ':' + #endDateStr"
)
fun getCalculateContentDonationList(
startDateStr: String,
endDateStr: String
): List<GetCalculateContentDonationResponse> {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
return repository
.getCalculateContentDonationList(startDate, endDate)
.map { it.toGetCalculateContentDonationResponse() }
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_hours"],
key = "'calculateCommunityPost:' + " + "#startDateStr + ':' + #endDateStr + ':' + #offset"
)
fun getCalculateCommunityPost(
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
): GetCreatorCalculateCommunityPostResponse {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val totalCount = repository.getCalculateCommunityPostTotalCount(startDate, endDate)
val items = repository
.getCalculateCommunityPostList(startDate, endDate, offset, limit)
.map { it.toGetCalculateCommunityPostResponse() }
return GetCreatorCalculateCommunityPostResponse(totalCount, items)
}
fun getCalculateLiveByCreator(
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
) = run {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val totalCount = repository.getCalculateLiveByCreatorTotalCount(startDate, endDate)
val items = repository
.getCalculateLiveByCreator(startDate, endDate, offset, limit)
.map { it.toGetCalculateByCreator() }
GetCalculateByCreatorResponse(totalCount, items)
}
fun getCalculateContentByCreator(
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
) = run {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val totalCount = repository.getCalculateContentByCreatorTotalCount(startDate, endDate)
val items = repository
.getCalculateContentByCreator(startDate, endDate, offset, limit)
.map { it.toGetCalculateByCreator() }
GetCalculateByCreatorResponse(totalCount, items)
}
fun getCalculateCommunityByCreator(
startDateStr: String,
endDateStr: String,
offset: Long,
limit: Long
) = run {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val totalCount = repository.getCalculateCommunityByCreatorTotalCount(startDate, endDate)
val items = repository
.getCalculateCommunityByCreator(startDate, endDate, offset, limit)
.map { it.toGetCalculateByCreator() }
GetCalculateByCreatorResponse(totalCount, items)
}
}

View File

@@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateByCreatorItem(
@JsonProperty("email") val email: String,
@JsonProperty("nickname") val nickname: String,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateByCreatorQueryData @QueryProjection constructor(
val email: String,
val nickname: String,
val totalCan: Int,
val settlementRatio: Int?
) {
fun toGetCalculateByCreator(): GetCalculateByCreatorItem {
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalCan).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액 = (원화 - 결제수수료) 의 70%
val settlementAmount = if (settlementRatio != null) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(settlementRatio).divide(BigDecimal(100.0)))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
// 입금액
val depositAmount = settlementAmount.subtract(tax)
return GetCalculateByCreatorItem(
email = email,
nickname = nickname,
totalCan = totalCan,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.calculate
data class GetCalculateByCreatorResponse(
val totalCount: Int,
val items: List<GetCalculateByCreatorItem>
)

View File

@@ -0,0 +1,50 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateCommunityPostQueryData @QueryProjection constructor(
val nickname: String,
val title: String,
val date: String,
val can: Int,
val numberOfPurchase: Long,
val totalCan: Int,
val settlementRatio: Int?
) {
fun toGetCalculateCommunityPostResponse(): GetCalculateCommunityPostResponse {
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalCan).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액 = (원화 - 결제수수료) 의 70%
val settlementAmount = if (settlementRatio != null) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(settlementRatio).divide(BigDecimal(100.0)))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
// 입금액
val depositAmount = settlementAmount.subtract(tax)
return GetCalculateCommunityPostResponse(
nickname = nickname,
title = title,
date = date,
can = can,
numberOfPurchase = numberOfPurchase.toInt(),
totalCan = totalCan,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}

View File

@@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateCommunityPostResponse(
@JsonProperty("nickname") val nickname: String,
@JsonProperty("title") val title: String,
@JsonProperty("date") val date: String,
@JsonProperty("can") val can: Int,
@JsonProperty("numberOfPurchase") val numberOfPurchase: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateContentDonationQueryData @QueryProjection constructor(
// 등록 크리에이터 닉네임
val nickname: String,
// 콘텐츠 제목
val title: String,
// 콘텐츠 가격
val price: Int,
// 콘텐츠 등록 날짜
val registrationDate: String,
// 후원 날짜
val donationDate: String,
// 인원
val numberOfDonation: Long,
// 합계
val totalCan: Int
) {
fun toGetCalculateContentDonationResponse(): GetCalculateContentDonationResponse {
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalCan).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액
// 유료콘텐츠 (원화 - 결제수수료) 의 90%
// 무료콘텐츠 (원화 - 결제수수료) 의 70%
val settlementAmount = if (price > 0) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.9))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
// 입금액
val depositAmount = settlementAmount.subtract(tax)
val paidOrFree = if (price > 0) {
"유료"
} else {
"무료"
}
return GetCalculateContentDonationResponse(
nickname = nickname,
title = title,
paidOrFree = paidOrFree,
registrationDate = registrationDate,
donationDate = donationDate,
numberOfDonation = numberOfDonation.toInt(),
totalCan = totalCan,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}

View File

@@ -0,0 +1,18 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateContentDonationResponse(
@JsonProperty("nickname") val nickname: String,
@JsonProperty("title") val title: String,
@JsonProperty("paidOrFree") val paidOrFree: String,
@JsonProperty("registrationDate") val registrationDate: String,
@JsonProperty("donationDate") val donationDate: String,
@JsonProperty("numberOfDonation") val numberOfDonation: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,73 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.order.OrderType
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateContentQueryData @QueryProjection constructor(
// 등록 크리에이터 닉네임
val nickname: String,
// 콘텐츠 제목
val title: String,
// 콘텐츠 등록 날짜
val registrationDate: String,
// 콘텐츠 판매 날짜
val saleDate: String,
// 대여/소장 구분
val orderType: OrderType,
// 판매 금액(캔)
val orderPrice: Int,
// 인원
val numberOfPeople: Long,
// 합계
val totalCan: Int,
// 포인트
val totalPoint: Int,
// 정산비율
val settlementRatio: Int?
) {
fun toGetCalculateContentResponse(): GetCalculateContentResponse {
val orderTypeStr = if (totalPoint > 0) {
"포인트"
} else if (orderType == OrderType.RENTAL) {
"대여"
} else {
"소장"
}
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalCan).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액 = (원화 - 결제수수료) 의 70%
val settlementAmount = if (settlementRatio != null) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(settlementRatio).divide(BigDecimal(100.0)))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
val depositAmount = settlementAmount.subtract(tax)
return GetCalculateContentResponse(
nickname = nickname,
title = title,
registrationDate = registrationDate,
saleDate = saleDate,
orderType = orderTypeStr,
orderPrice = orderPrice,
numberOfPeople = numberOfPeople.toInt(),
totalCan = totalCan,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateContentResponse(
@JsonProperty("nickname") val nickname: String,
@JsonProperty("title") val title: String,
@JsonProperty("registrationDate") val registrationDate: String,
@JsonProperty("saleDate") val saleDate: String,
@JsonProperty("orderType") val orderType: String,
@JsonProperty("orderPrice") val orderPrice: Int,
@JsonProperty("numberOfPeople") val numberOfPeople: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,84 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.can.use.CanUsage
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateLiveQueryData @QueryProjection constructor(
val email: String,
val nickname: String,
val date: String,
val title: String,
// 유료방 입장 금액
val entranceFee: Int,
// 코인 사용 구분
val canUsage: CanUsage,
// 참여인원
val memberCount: Long,
// 합계
val totalAmount: Int,
// 정산비율
val settlementRatio: Int?
) {
fun toGetCalculateLiveResponse(): GetCalculateLiveResponse {
val canUsageStr = when (canUsage) {
CanUsage.LIVE -> {
"유료"
}
CanUsage.SPIN_ROULETTE -> {
"룰렛"
}
CanUsage.HEART -> {
"하트"
}
else -> {
"후원"
}
}
val numberOfPeople = if (canUsage == CanUsage.LIVE) {
memberCount.toInt()
} else {
0
}
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalAmount).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액 = (원화 - 결제수수료) 의 70%
val settlementAmount = if (settlementRatio != null) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(settlementRatio).divide(BigDecimal(100.0)))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
// 입금액
val depositAmount = settlementAmount.subtract(tax)
return GetCalculateLiveResponse(
email = email,
nickname = nickname,
date = date,
title = title,
entranceFee = entranceFee,
canUsageStr = canUsageStr,
numberOfPeople = numberOfPeople,
totalAmount = totalAmount,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateLiveResponse(
@JsonProperty("email") val email: String,
@JsonProperty("nickname") val nickname: String,
@JsonProperty("date") val date: String,
@JsonProperty("title") val title: String,
@JsonProperty("entranceFee") val entranceFee: Int,
@JsonProperty("canUsageStr") val canUsageStr: String,
@JsonProperty("numberOfPeople") val numberOfPeople: Int,
@JsonProperty("totalAmount") val totalAmount: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,92 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.order.OrderType
import java.math.BigDecimal
import java.math.RoundingMode
data class GetCumulativeSalesByContentQueryData @QueryProjection constructor(
// 등록 크리에이터 닉네임
val nickname: String,
// 콘텐츠 제목
val title: String,
// 콘텐츠 등록 날짜
val registrationDate: String,
// 대여/소장 구분
val orderType: OrderType,
// 판매 금액(캔)
val orderPrice: Int,
// 인원
val numberOfPeople: Long,
// 합계
val totalCan: Int,
// 포인트
val totalPoint: Int,
// 정산비율
val settlementRatio: Int?
) {
fun toCumulativeSalesByContentItem(): CumulativeSalesByContentItem {
val orderTypeStr = if (totalPoint > 0) {
"포인트"
} else if (orderType == OrderType.RENTAL) {
"대여"
} else {
"소장"
}
// 원화 = totalCoin * 100 ( 캔 1개 = 100원 )
val totalKrw = BigDecimal(totalCan).multiply(BigDecimal(100))
// 결제수수료 : 6.6%
val paymentFee = totalKrw.multiply(BigDecimal(0.066))
// 정산금액 = (원화 - 결제수수료) 의 70%
val settlementAmount = if (settlementRatio != null) {
totalKrw.subtract(paymentFee).multiply(BigDecimal(settlementRatio).divide(BigDecimal(100.0)))
} else {
totalKrw.subtract(paymentFee).multiply(BigDecimal(0.7))
}
// 원천세 = 정산금액의 3.3%
val tax = settlementAmount.multiply(BigDecimal(0.033))
// 입금액
val depositAmount = settlementAmount.subtract(tax)
return CumulativeSalesByContentItem(
nickname = nickname,
title = title,
registrationDate = registrationDate,
orderType = orderTypeStr,
orderPrice = orderPrice,
numberOfPeople = numberOfPeople.toInt(),
totalCan = totalCan,
totalKrw = totalKrw.toInt(),
paymentFee = paymentFee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt()
)
}
}
data class GetCumulativeSalesByContentResponse(
@JsonProperty("totalCount") val totalCount: Int,
@JsonProperty("items") val items: List<CumulativeSalesByContentItem>
)
data class CumulativeSalesByContentItem(
@JsonProperty("nickname") val nickname: String,
@JsonProperty("title") val title: String,
@JsonProperty("registrationDate") val registrationDate: String,
@JsonProperty("orderType") val orderType: String,
@JsonProperty("orderPrice") val orderPrice: Int,
@JsonProperty("numberOfPeople") val numberOfPeople: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("paymentFee") val paymentFee: Int,
@JsonProperty("settlementAmount") val settlementAmount: Int,
@JsonProperty("tax") val tax: Int,
@JsonProperty("depositAmount") val depositAmount: Int
)

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
data class CreateCreatorSettlementRatioRequest(
val memberId: Long,
val subsidy: Int,
val liveSettlementRatio: Int,
val contentSettlementRatio: Int,
val communitySettlementRatio: Int
) {
fun toEntity() = CreatorSettlementRatio(
subsidy = subsidy,
liveSettlementRatio = liveSettlementRatio,
contentSettlementRatio = contentSettlementRatio,
communitySettlementRatio = communitySettlementRatio
)
}

View File

@@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.OneToOne
@Entity
data class CreatorSettlementRatio(
val subsidy: Int,
val liveSettlementRatio: Int,
val contentSettlementRatio: Int,
val communitySettlementRatio: Int
) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member? = null
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
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.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/calculate/ratio")
class CreatorSettlementRatioController(private val service: CreatorSettlementRatioService) {
@PostMapping
fun createCreatorSettlementRatio(
@RequestBody request: CreateCreatorSettlementRatioRequest
) = ApiResponse.ok(service.createCreatorSettlementRatio(request))
@GetMapping
fun getCreatorSettlementRatio(
pageable: Pageable
) = ApiResponse.ok(
service.getCreatorSettlementRatio(
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}

View File

@@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.calculate.ratio.QCreatorSettlementRatio.creatorSettlementRatio
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.jpa.repository.JpaRepository
interface CreatorSettlementRatioRepository :
JpaRepository<CreatorSettlementRatio, Long>,
CreatorSettlementRatioQueryRepository
interface CreatorSettlementRatioQueryRepository {
fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem>
fun getCreatorSettlementRatioTotalCount(): Int
}
class CreatorSettlementRatioQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : CreatorSettlementRatioQueryRepository {
override fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem> {
return queryFactory
.select(
QGetCreatorSettlementRatioItem(
member.nickname,
creatorSettlementRatio.subsidy,
creatorSettlementRatio.liveSettlementRatio,
creatorSettlementRatio.contentSettlementRatio,
creatorSettlementRatio.communitySettlementRatio
)
)
.from(creatorSettlementRatio)
.innerJoin(creatorSettlementRatio.member, member)
.orderBy(creatorSettlementRatio.id.asc())
.offset(offset)
.limit(limit)
.fetch()
}
override fun getCreatorSettlementRatioTotalCount(): Int {
return queryFactory
.select(creatorSettlementRatio.id)
.from(creatorSettlementRatio)
.fetch()
.size
}
}

View File

@@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class CreatorSettlementRatioService(
private val repository: CreatorSettlementRatioRepository,
private val memberRepository: MemberRepository
) {
@Transactional
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
val creatorSettlementRatio = request.toEntity()
val creator = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 크리에이터 입니다.")
if (creator.role != MemberRole.CREATOR) {
throw SodaException("잘못된 크리에이터 입니다.")
}
creatorSettlementRatio.member = creator
repository.save(creatorSettlementRatio)
}
@Transactional(readOnly = true)
fun getCreatorSettlementRatio(offset: Long, limit: Long): GetCreatorSettlementRatioResponse {
val totalCount = repository.getCreatorSettlementRatioTotalCount()
val items = repository.getCreatorSettlementRatio(offset, limit)
return GetCreatorSettlementRatioResponse(totalCount, items)
}
}

View File

@@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.admin.calculate.ratio
import com.querydsl.core.annotations.QueryProjection
data class GetCreatorSettlementRatioResponse(
val totalCount: Int,
val items: List<GetCreatorSettlementRatioItem>
)
data class GetCreatorSettlementRatioItem @QueryProjection constructor(
val nickname: String,
val subsidy: Int,
val liveSettlementRatio: Int,
val contentSettlementRatio: Int,
val communitySettlementRatio: Int
)

View File

@@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.admin.can
data class AdminCanChargeRequest(
val memberId: Long,
val method: String,
val can: Int
)

View File

@@ -0,0 +1,24 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/admin/can")
@PreAuthorize("hasRole('ADMIN')")
class AdminCanController(private val service: AdminCanService) {
@PostMapping
fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request))
@DeleteMapping("/{id}")
fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.deleteCan(id))
@PostMapping("/charge")
fun charge(@RequestBody request: AdminCanChargeRequest) = ApiResponse.ok(service.charge(request))
}

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.can.Can
import org.springframework.data.jpa.repository.JpaRepository
interface AdminCanRepository : JpaRepository<Can, Long>

View File

@@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.can.Can
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.extensions.moneyFormat
data class AdminCanRequest(
val can: Int,
val rewardCan: Int,
val price: Int
) {
fun toEntity(): Can {
var title = "${can.moneyFormat()}"
if (rewardCan > 0) {
title = "$title + ${rewardCan.moneyFormat()}"
}
return Can(
title = title,
can = can,
rewardCan = rewardCan,
price = price,
status = CanStatus.SALE
)
}
}

View File

@@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.moneyFormat
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminCanService(
private val repository: AdminCanRepository,
private val chargeRepository: ChargeRepository,
private val memberRepository: AdminMemberRepository
) {
@Transactional
fun saveCan(request: AdminCanRequest) {
repository.save(request.toEntity())
}
@Transactional
fun deleteCan(id: Long) {
val can = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
can.status = CanStatus.END_OF_SALE
}
@Transactional
fun charge(request: AdminCanChargeRequest) {
val member = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 회원번호 입니다.")
if (request.can <= 0) throw SodaException("1 캔 이상 입력하세요.")
if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.")
val charge = Charge(0, request.can, status = ChargeStatus.ADMIN)
charge.title = "${request.can.moneyFormat()}"
charge.member = member
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
payment.method = request.method
charge.payment = payment
chargeRepository.save(charge)
member.pgRewardCan += charge.rewardCan
}
}

View File

@@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.admin.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/charge/status")
class AdminChargeStatusController(private val service: AdminChargeStatusService) {
@GetMapping
fun getChargeStatus(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
) = ApiResponse.ok(service.getChargeStatus(startDateStr, endDateStr))
@GetMapping("/detail")
fun getChargeStatusDetail(
@RequestParam startDateStr: String,
@RequestParam paymentGateway: PaymentGateway
) = ApiResponse.ok(service.getChargeStatusDetail(startDateStr, paymentGateway))
}

View File

@@ -0,0 +1,97 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.QCan.can1
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.charge.QCharge.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.payment.QPayment.payment
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class AdminChargeStatusQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getChargeStatus(startDate: LocalDateTime, endDate: LocalDateTime): List<GetChargeStatusQueryDto> {
val formattedDate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
charge.createdAt,
"UTC",
"Asia/Seoul"
),
"%Y-%m-%d"
)
return queryFactory
.select(
QGetChargeStatusQueryDto(
formattedDate,
payment.price.sum(),
can1.price.sum(),
payment.id.count(),
payment.paymentGateway
)
)
.from(payment)
.innerJoin(payment.charge, charge)
.leftJoin(charge.can, can1)
.where(
charge.createdAt.goe(startDate)
.and(charge.createdAt.loe(endDate))
.and(charge.status.eq(ChargeStatus.CHARGE))
.and(payment.status.eq(PaymentStatus.COMPLETE))
)
.groupBy(formattedDate, payment.paymentGateway)
.orderBy(formattedDate.desc())
.fetch()
}
fun getChargeStatusDetail(
startDate: LocalDateTime,
endDate: LocalDateTime,
paymentGateway: PaymentGateway
): List<GetChargeStatusDetailQueryDto> {
val formattedDate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
charge.createdAt,
"UTC",
"Asia/Seoul"
),
"%Y-%m-%d %H:%i:%s"
)
return queryFactory
.select(
QGetChargeStatusDetailQueryDto(
member.id,
member.nickname,
payment.method.coalesce(""),
payment.price,
can1.price,
payment.locale.coalesce(""),
formattedDate
)
)
.from(charge)
.innerJoin(charge.member, member)
.innerJoin(charge.payment, payment)
.leftJoin(charge.can, can1)
.where(
charge.createdAt.goe(startDate)
.and(charge.createdAt.loe(endDate))
.and(charge.status.eq(ChargeStatus.CHARGE))
.and(payment.status.eq(PaymentStatus.COMPLETE))
.and(payment.paymentGateway.eq(paymentGateway))
)
.orderBy(formattedDate.desc())
.fetch()
}
}

View File

@@ -0,0 +1,91 @@
package kr.co.vividnext.sodalive.admin.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository) {
fun getChargeStatus(startDateStr: String, endDateStr: String): List<GetChargeStatusResponse> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = LocalDate.parse(endDateStr, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
var totalChargeAmount = 0
var totalChargeCount = 0L
val chargeStatusList = repository.getChargeStatus(startDate, endDate)
.asSequence()
.map {
val chargeAmount = if (it.paymentGateWay == PaymentGateway.PG) {
it.pgChargeAmount
} else {
it.appleChargeAmount.toInt()
}
val chargeCount = it.chargeCount
totalChargeAmount += chargeAmount
totalChargeCount += chargeCount
GetChargeStatusResponse(
date = it.date,
chargeAmount = chargeAmount,
chargeCount = chargeCount,
pg = it.paymentGateWay.name
)
}
.toMutableList()
chargeStatusList.add(
0,
GetChargeStatusResponse(
date = "합계",
chargeAmount = totalChargeAmount,
chargeCount = totalChargeCount,
pg = ""
)
)
return chargeStatusList.toList()
}
fun getChargeStatusDetail(
startDateStr: String,
paymentGateway: PaymentGateway
): List<GetChargeStatusDetailResponse> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
return repository.getChargeStatusDetail(startDate, endDate, paymentGateway)
.asSequence()
.map {
GetChargeStatusDetailResponse(
memberId = it.memberId,
nickname = it.nickname,
method = it.method,
amount = it.appleChargeAmount.toInt(),
locale = it.locale,
datetime = it.datetime
)
}
.toList()
}
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
data class GetChargeStatusDetailQueryDto @QueryProjection constructor(
val memberId: Long,
val nickname: String,
val method: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val locale: String,
val datetime: String
)

View File

@@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.admin.charge
data class GetChargeStatusDetailResponse(
val memberId: Long,
val nickname: String,
val method: String,
val amount: Int,
val locale: String,
val datetime: String
)

View File

@@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
data class GetChargeStatusQueryDto @QueryProjection constructor(
val date: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val chargeCount: Long,
val paymentGateWay: PaymentGateway
)

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.admin.charge
data class GetChargeStatusResponse(
val date: String,
val chargeAmount: Int,
val chargeCount: Long,
val pg: String
)

View File

@@ -0,0 +1,229 @@
package kr.co.vividnext.sodalive.admin.chat
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerListPageResponse
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerRegisterRequest
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerResponse
import kr.co.vividnext.sodalive.admin.chat.dto.ChatCharacterBannerUpdateRequest
import kr.co.vividnext.sodalive.admin.chat.dto.UpdateBannerOrdersRequest
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerService
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
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.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/chat/banner")
@PreAuthorize("hasRole('ADMIN')")
class AdminChatBannerController(
private val bannerService: ChatCharacterBannerService,
private val adminCharacterService: AdminChatCharacterService,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
/**
* 활성화된 배너 목록 조회 API
*
* @param page 페이지 번호 (0부터 시작, 기본값 0)
* @param size 페이지 크기 (기본값 20)
* @return 페이징된 배너 목록
*/
@GetMapping("/list")
fun getBannerList(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageable = adminCharacterService.createDefaultPageRequest(page, size)
val banners = bannerService.getActiveBanners(pageable)
val response = ChatCharacterBannerListPageResponse(
totalCount = banners.totalElements,
content = banners.content.map { ChatCharacterBannerResponse.from(it, imageHost) }
)
ApiResponse.ok(response)
}
/**
* 배너 상세 조회 API
*
* @param bannerId 배너 ID
* @return 배너 상세 정보
*/
@GetMapping("/{bannerId}")
fun getBannerDetail(@PathVariable bannerId: Long) = run {
val banner = bannerService.getBannerById(bannerId)
val response = ChatCharacterBannerResponse.from(banner, imageHost)
ApiResponse.ok(response)
}
/**
* 캐릭터 검색 API (배너 등록을 위한)
*
* @param searchTerm 검색어 (이름, 설명, MBTI, 태그)
* @param page 페이지 번호 (0부터 시작, 기본값 0)
* @param size 페이지 크기 (기본값 20)
* @return 검색된 캐릭터 목록
*/
@GetMapping("/search-character")
fun searchCharacters(
@RequestParam searchTerm: String,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageable = adminCharacterService.createDefaultPageRequest(page, size)
val pageResult = adminCharacterService.searchCharacters(searchTerm, pageable, imageHost)
val response = ChatCharacterSearchListPageResponse(
totalCount = pageResult.totalElements,
content = pageResult.content
)
ApiResponse.ok(response)
}
/**
* 배너 등록 API
*
* @param image 배너 이미지
* @param requestString 배너 등록 요청 정보 (캐릭터 ID와 선택적으로 정렬 순서 포함)
* @return 등록된 배너 정보
*/
@PostMapping("/register")
fun registerBanner(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = run {
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(
requestString,
ChatCharacterBannerRegisterRequest::class.java
)
// 1. 먼저 빈 이미지 경로로 배너 등록 (정렬 순서 포함)
val banner = bannerService.registerBanner(
characterId = request.characterId,
imagePath = ""
)
// 2. 배너 ID를 사용하여 이미지 업로드
val imagePath = saveImage(banner.id!!, image)
// 3. 이미지 경로로 배너 업데이트
val updatedBanner = bannerService.updateBanner(banner.id!!, imagePath)
val response = ChatCharacterBannerResponse.from(updatedBanner, imageHost)
ApiResponse.ok(response)
}
/**
* 이미지를 S3에 업로드하고 경로를 반환
*
* @param bannerId 배너 ID (이미지 경로에 사용)
* @param image 업로드할 이미지 파일
* @return 업로드된 이미지 경로
*/
private fun saveImage(bannerId: Long, image: MultipartFile): String {
try {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
val fileName = generateFileName("character-banner")
// S3에 이미지 업로드 (배너 ID를 경로에 사용)
return s3Uploader.upload(
inputStream = image.inputStream,
bucket = s3Bucket,
filePath = "characters/banners/$bannerId/$fileName",
metadata = metadata
)
} catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
}
}
/**
* 배너 수정 API
*
* @param image 배너 이미지
* @param requestString 배너 수정 요청 정보 (배너 ID와 선택적으로 캐릭터 ID 포함)
* @return 수정된 배너 정보
*/
@PutMapping("/update")
fun updateBanner(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = run {
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(
requestString,
ChatCharacterBannerUpdateRequest::class.java
)
// 배너 정보 조회
bannerService.getBannerById(request.bannerId)
// 배너 ID를 사용하여 이미지 업로드
val imagePath = saveImage(request.bannerId, image)
// 배너 수정 (이미지와 캐릭터 모두 수정 가능)
val updatedBanner = bannerService.updateBanner(
bannerId = request.bannerId,
imagePath = imagePath,
characterId = request.characterId
)
val response = ChatCharacterBannerResponse.from(updatedBanner, imageHost)
ApiResponse.ok(response)
}
/**
* 배너 삭제 API (소프트 삭제)
*
* @param bannerId 배너 ID
* @return 성공 여부
*/
@DeleteMapping("/{bannerId}")
fun deleteBanner(@PathVariable bannerId: Long) = run {
bannerService.deleteBanner(bannerId)
ApiResponse.ok("배너가 성공적으로 삭제되었습니다.")
}
/**
* 배너 정렬 순서 일괄 변경 API
* ID 목록의 순서대로 정렬 순서를 1부터 순차적으로 설정합니다.
*
* @param request 정렬 순서 일괄 변경 요청 정보 (배너 ID 목록)
* @return 성공 메시지
*/
@PutMapping("/orders")
fun updateBannerOrders(
@RequestBody request: UpdateBannerOrdersRequest
) = run {
bannerService.updateBannerOrders(request.ids)
ApiResponse.ok(null, "배너 정렬 순서가 성공적으로 변경되었습니다.")
}
}

View File

@@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.admin.chat.calculate
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/chat/calculate")
class AdminChatCalculateController(
private val service: AdminChatCalculateService
) {
@GetMapping("/characters")
fun getCharacterCalculate(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@RequestParam(required = false, defaultValue = "TOTAL_SALES_DESC") sort: ChatCharacterCalculateSort,
pageable: Pageable
) = ApiResponse.ok(
service.getCharacterCalculate(
startDateStr,
endDateStr,
sort,
pageable.offset,
pageable.pageSize
)
)
}

View File

@@ -0,0 +1,139 @@
package kr.co.vividnext.sodalive.admin.chat.calculate
import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.CaseBuilder
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.chat.character.QChatCharacter
import kr.co.vividnext.sodalive.chat.character.image.QCharacterImage.characterImage
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class AdminChatCalculateQueryRepository(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
fun getCharacterCalculate(
startUtc: LocalDateTime,
endInclusiveUtc: LocalDateTime,
sort: ChatCharacterCalculateSort,
offset: Long,
limit: Long
): List<ChatCharacterCalculateQueryData> {
val imageCanExpr = CaseBuilder()
.`when`(useCan.canUsage.eq(CanUsage.CHARACTER_IMAGE_PURCHASE))
.then(useCan.can.add(useCan.rewardCan))
.otherwise(0)
val messageCanExpr = CaseBuilder()
.`when`(useCan.canUsage.eq(CanUsage.CHAT_MESSAGE_PURCHASE))
.then(useCan.can.add(useCan.rewardCan))
.otherwise(0)
val quotaCanExpr = CaseBuilder()
.`when`(useCan.canUsage.eq(CanUsage.CHAT_QUOTA_PURCHASE))
.then(useCan.can.add(useCan.rewardCan))
.otherwise(0)
val imageSum = imageCanExpr.sum()
val messageSum = messageCanExpr.sum()
val quotaSum = quotaCanExpr.sum()
val totalSum = imageSum.add(messageSum).add(quotaSum)
// 캐릭터 조인: 이미지 경로를 통한 캐릭터(c1) + characterId 직접 지정(c2)
val c1 = QChatCharacter("c1")
val c2 = QChatCharacter("c2")
val characterIdExpr = c1.id.coalesce(c2.id)
val characterNameAgg = Expressions.stringTemplate(
"coalesce(max({0}), max({1}), '')",
c1.name,
c2.name
)
val characterImagePathAgg = Expressions.stringTemplate(
"coalesce(max({0}), max({1}))",
c1.imagePath,
c2.imagePath
)
val query = queryFactory
.select(
Projections.constructor(
ChatCharacterCalculateQueryData::class.java,
characterIdExpr,
characterNameAgg,
characterImagePathAgg.prepend("/").prepend(imageHost),
imageSum,
messageSum,
quotaSum
)
)
.from(useCan)
.leftJoin(useCan.characterImage, characterImage)
.leftJoin(characterImage.chatCharacter, c1)
.leftJoin(c2).on(c2.id.eq(useCan.characterId))
.where(
useCan.isRefund.isFalse
.and(
useCan.canUsage.`in`(
CanUsage.CHARACTER_IMAGE_PURCHASE,
CanUsage.CHAT_MESSAGE_PURCHASE,
CanUsage.CHAT_QUOTA_PURCHASE
)
)
.and(useCan.createdAt.goe(startUtc))
.and(useCan.createdAt.loe(endInclusiveUtc))
)
.groupBy(characterIdExpr)
when (sort) {
ChatCharacterCalculateSort.TOTAL_SALES_DESC ->
query.orderBy(totalSum.desc(), characterIdExpr.desc())
ChatCharacterCalculateSort.LATEST_DESC ->
query.orderBy(characterIdExpr.desc(), totalSum.desc())
}
return query
.offset(offset)
.limit(limit)
.fetch()
}
fun getCharacterCalculateTotalCount(
startUtc: LocalDateTime,
endInclusiveUtc: LocalDateTime
): Int {
val c1 = QChatCharacter("c1")
val c2 = QChatCharacter("c2")
val characterIdExpr = c1.id.coalesce(c2.id)
return queryFactory
.select(characterIdExpr)
.from(useCan)
.leftJoin(useCan.characterImage, characterImage)
.leftJoin(characterImage.chatCharacter, c1)
.leftJoin(c2).on(c2.id.eq(useCan.characterId))
.where(
useCan.isRefund.isFalse
.and(
useCan.canUsage.`in`(
CanUsage.CHARACTER_IMAGE_PURCHASE,
CanUsage.CHAT_MESSAGE_PURCHASE,
CanUsage.CHAT_QUOTA_PURCHASE
)
)
.and(useCan.createdAt.goe(startUtc))
.and(useCan.createdAt.loe(endInclusiveUtc))
)
.groupBy(characterIdExpr)
.fetch()
.size
}
}

View File

@@ -0,0 +1,49 @@
package kr.co.vividnext.sodalive.admin.chat.calculate
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class AdminChatCalculateService(
private val repository: AdminChatCalculateQueryRepository
) {
private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
private val kstZone: ZoneId = ZoneId.of("Asia/Seoul")
@Transactional(readOnly = true)
fun getCharacterCalculate(
startDateStr: String,
endDateStr: String,
sort: ChatCharacterCalculateSort,
offset: Long,
pageSize: Int
): ChatCharacterCalculateResponse {
// 날짜 유효성 검증 (KST 기준)
val startDate = LocalDate.parse(startDateStr, dateFormatter)
val endDate = LocalDate.parse(endDateStr, dateFormatter)
val todayKst = LocalDate.now(kstZone)
if (endDate.isAfter(todayKst)) {
throw SodaException("끝 날짜는 오늘 날짜까지만 입력 가능합니다.")
}
if (startDate.isAfter(endDate)) {
throw SodaException("시작 날짜는 끝 날짜보다 이후일 수 없습니다.")
}
if (endDate.isAfter(startDate.plusMonths(6))) {
throw SodaException("조회 가능 기간은 최대 6개월입니다.")
}
val startUtc = startDateStr.convertLocalDateTime()
val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
val totalCount = repository.getCharacterCalculateTotalCount(startUtc, endInclusiveUtc)
val rows = repository.getCharacterCalculate(startUtc, endInclusiveUtc, sort, offset, pageSize.toLong())
val items = rows.map { it.toItem() }
return ChatCharacterCalculateResponse(totalCount, items)
}
}

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.admin.chat.calculate
import com.fasterxml.jackson.annotation.JsonProperty
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
import java.math.RoundingMode
// 정렬 옵션
enum class ChatCharacterCalculateSort {
TOTAL_SALES_DESC,
LATEST_DESC
}
// QueryDSL 프로젝션용 DTO
data class ChatCharacterCalculateQueryData @QueryProjection constructor(
val characterId: Long,
val characterName: String,
val characterImagePath: String?,
val imagePurchaseCan: Int?,
val messagePurchaseCan: Int?,
val quotaPurchaseCan: Int?
)
// 응답 DTO (아이템)
data class ChatCharacterCalculateItem(
@JsonProperty("characterId") val characterId: Long,
@JsonProperty("characterImage") val characterImage: String?,
@JsonProperty("name") val name: String,
@JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int,
@JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int,
@JsonProperty("quotaPurchaseCan") val quotaPurchaseCan: Int,
@JsonProperty("totalCan") val totalCan: Int,
@JsonProperty("totalKrw") val totalKrw: Int,
@JsonProperty("settlementKrw") val settlementKrw: Int
)
// 응답 DTO (전체)
data class ChatCharacterCalculateResponse(
@JsonProperty("totalCount") val totalCount: Int,
@JsonProperty("items") val items: List<ChatCharacterCalculateItem>
)
fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem {
val image = imagePurchaseCan ?: 0
val message = messagePurchaseCan ?: 0
val quota = quotaPurchaseCan ?: 0
val total = image + message + quota
val totalKrw = BigDecimal(total).multiply(BigDecimal(100))
val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP)
return ChatCharacterCalculateItem(
characterId = characterId,
characterImage = characterImagePath,
name = characterName,
imagePurchaseCan = image,
messagePurchaseCan = message,
quotaPurchaseCan = quota,
totalCan = total,
totalKrw = totalKrw.toInt(),
settlementKrw = settlement.toInt()
)
}

View File

@@ -0,0 +1,423 @@
package kr.co.vividnext.sodalive.admin.chat.character
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRegisterRequest
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest
import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
import kr.co.vividnext.sodalive.admin.chat.original.service.AdminOriginalWorkService
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.CharacterType
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.http.client.SimpleClientHttpRequestFactory
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/chat/character")
@PreAuthorize("hasRole('ADMIN')")
class AdminChatCharacterController(
private val service: ChatCharacterService,
private val adminService: AdminChatCharacterService,
private val s3Uploader: S3Uploader,
private val originalWorkService: AdminOriginalWorkService,
@Value("\${weraser.api-key}")
private val apiKey: String,
@Value("\${weraser.api-url}")
private val apiUrl: String,
@Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
/**
* 활성화된 캐릭터 목록 조회 API
*
* @param page 페이지 번호 (0부터 시작, 기본값 0)
* @param size 페이지 크기 (기본값 20)
* @return 페이징된 캐릭터 목록
*/
@GetMapping("/list")
fun getCharacterList(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageable = adminService.createDefaultPageRequest(page, size)
val response = adminService.getActiveChatCharacters(pageable, imageHost)
ApiResponse.ok(response)
}
/**
* 캐릭터 검색(관리자)
* - 이름/설명/MBTI/태그 기준 부분 검색, 활성 캐릭터만 대상
* - 페이징 지원: page, size 파라미터 사용
*/
@GetMapping("/search")
fun searchCharacters(
@RequestParam("searchTerm") searchTerm: String,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageable = adminService.createDefaultPageRequest(page, size)
val resultPage = adminService.searchCharacters(searchTerm, pageable, imageHost)
val response = ChatCharacterSearchListPageResponse(
totalCount = resultPage.totalElements,
content = resultPage.content
)
ApiResponse.ok(response)
}
/**
* 캐릭터 상세 정보 조회 API
*
* @param characterId 캐릭터 ID
* @return 캐릭터 상세 정보
*/
@GetMapping("/{characterId}")
fun getCharacterDetail(
@PathVariable characterId: Long
) = run {
val response = adminService.getChatCharacterDetail(characterId, imageHost)
ApiResponse.ok(response)
}
@PostMapping("/register")
fun registerCharacter(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = run {
// JSON 문자열을 ChatCharacterRegisterRequest 객체로 변환
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(requestString, ChatCharacterRegisterRequest::class.java)
// 외부 API 호출 전 DB에 동일한 이름이 있는지 조회
val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}")
}
// 1. 외부 API 호출
val characterUUID = callExternalApi(request)
// 2. ChatCharacter 저장
val chatCharacter = service.createChatCharacterWithDetails(
characterUUID = characterUUID,
name = request.name,
description = request.description,
systemPrompt = request.systemPrompt,
age = request.age?.toIntOrNull(),
gender = request.gender,
mbti = request.mbti,
speechPattern = request.speechPattern,
speechStyle = request.speechStyle,
appearance = request.appearance,
originalTitle = request.originalTitle,
originalLink = request.originalLink,
characterType = request.characterType?.let {
runCatching { CharacterType.valueOf(it) }
.getOrDefault(CharacterType.Character)
} ?: CharacterType.Character,
tags = request.tags,
values = request.values,
hobbies = request.hobbies,
goals = request.goals,
memories = request.memories.map { Triple(it.title, it.content, it.emotion) },
personalities = request.personalities.map { Pair(it.trait, it.description) },
backgrounds = request.backgrounds.map { Pair(it.topic, it.description) },
relationships = request.relationships
)
// 3. 이미지 저장 및 ChatCharacter에 이미지 path 설정
val imagePath = saveImage(
characterId = chatCharacter.id!!,
image = image
)
chatCharacter.imagePath = imagePath
service.saveChatCharacter(chatCharacter)
// 4. 원작 연결: originalWorkId가 있으면 서비스 계층을 통해 배정
if (request.originalWorkId != null) {
originalWorkService.assignOneCharacter(request.originalWorkId, chatCharacter.id!!)
}
ApiResponse.ok(null)
}
private fun callExternalApi(request: ChatCharacterRegisterRequest): String {
try {
val factory = SimpleClientHttpRequestFactory()
factory.setConnectTimeout(20000) // 20초
factory.setReadTimeout(20000) // 20초
val restTemplate = RestTemplate(factory)
val headers = HttpHeaders()
headers.set("x-api-key", apiKey) // 실제 API 키로 대체 필요
headers.contentType = MediaType.APPLICATION_JSON
// 외부 API에 전달하지 않을 필드(originalTitle, originalLink, characterType)를 제외하고 바디 구성
val body = mutableMapOf<String, Any>()
body["name"] = request.name
body["systemPrompt"] = request.systemPrompt
body["description"] = request.description
request.age?.let { body["age"] = it }
request.gender?.let { body["gender"] = it }
request.mbti?.let { body["mbti"] = it }
request.speechPattern?.let { body["speechPattern"] = it }
request.speechStyle?.let { body["speechStyle"] = it }
request.appearance?.let { body["appearance"] = it }
if (request.tags.isNotEmpty()) body["tags"] = request.tags
if (request.hobbies.isNotEmpty()) body["hobbies"] = request.hobbies
if (request.values.isNotEmpty()) body["values"] = request.values
if (request.goals.isNotEmpty()) body["goals"] = request.goals
if (request.relationships.isNotEmpty()) body["relationships"] = request.relationships
if (request.personalities.isNotEmpty()) body["personalities"] = request.personalities
if (request.backgrounds.isNotEmpty()) body["backgrounds"] = request.backgrounds
if (request.memories.isNotEmpty()) body["memories"] = request.memories
val httpEntity = HttpEntity(body, headers)
val response = restTemplate.exchange(
"$apiUrl/api/characters",
HttpMethod.POST,
httpEntity,
String::class.java
)
// 응답 파싱
val objectMapper = ObjectMapper()
val apiResponse = objectMapper.readValue(response.body, ExternalApiResponse::class.java)
// success가 false이면 throw
if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "등록에 실패했습니다. 다시 시도해 주세요.")
}
// success가 true이면 data.id 반환
return apiResponse.data?.id ?: throw SodaException("등록에 실패했습니다. 응답에 ID가 없습니다.")
} catch (e: Exception) {
e.printStackTrace()
throw SodaException("${e.message}, 등록에 실패했습니다. 다시 시도해 주세요.")
}
}
private fun saveImage(characterId: Long, image: MultipartFile): String {
try {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
// S3에 이미지 업로드
return s3Uploader.upload(
inputStream = image.inputStream,
bucket = s3Bucket,
filePath = "characters/$characterId/${generateFileName(prefix = "character")}",
metadata = metadata
)
} catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
}
}
/**
* 캐릭터 수정 API
* 1. JSON 문자열을 ChatCharacterUpdateRequest 객체로 변환
* 2. ChatCharacterUpdateRequest를 확인해서 변경한 데이터가 있는지 확인
* 3. 이미지 있는지 확인
* 4. 2, 3번 중 하나라도 해당 하면 계속 진행
* 5. 2, 3번에 데이터 없으면 throw SodaException('변경된 데이터가 없습니다.')
*
* @param image 캐릭터 이미지 (선택적)
* @param requestString ChatCharacterUpdateRequest 객체를 JSON 문자열로 변환한 값
* @return ApiResponse 객체
* @throws SodaException 변경된 데이터가 없거나 캐릭터를 찾을 수 없는 경우
*/
@PutMapping("/update")
fun updateCharacter(
@RequestPart(value = "image", required = false) image: MultipartFile?,
@RequestPart("request") requestString: String
) = run {
// 1. JSON 문자열을 ChatCharacterUpdateRequest 객체로 변환
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(requestString, ChatCharacterUpdateRequest::class.java)
// 2. ChatCharacterUpdateRequest를 확인해서 변경한 데이터가 있는지 확인
val hasChangedData = hasChanges(request) // 외부 API 대상으로의 변경 여부(3가지 필드 제외)
// 3. 이미지 있는지 확인
val hasImage = image != null && !image.isEmpty
// 3가지만 변경된 경우(외부 API 변경은 없지만 DB 변경은 있는 경우)를 허용하기 위해 별도 플래그 계산
val hasDbOnlyChanges =
request.originalTitle != null ||
request.originalLink != null ||
request.characterType != null ||
request.originalWorkId != null
if (!hasChangedData && !hasImage && !hasDbOnlyChanges) {
throw SodaException("변경된 데이터가 없습니다.")
}
// 외부 API로 전달할 변경이 있을 때만 외부 API 호출(3가지 필드만 변경된 경우는 호출하지 않음)
if (hasChangedData) {
val chatCharacter = service.findById(request.id)
?: throw SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: ${request.id}")
// 이름이 수정된 경우 DB에 동일한 이름이 있는지 확인
if (request.name != null && request.name != chatCharacter.name) {
val existingCharacter = service.findByName(request.name)
if (existingCharacter != null) {
throw SodaException("동일한 이름은 등록이 불가능합니다: ${request.name}")
}
}
callExternalApiForUpdate(chatCharacter.characterUUID, request)
}
// 이미지 경로 변수 초기화
// 이미지가 있으면 이미지 저장
val imagePath = if (hasImage) {
saveImage(
characterId = request.id,
image = image!!
)
} else {
null
}
// 엔티티 수정
service.updateChatCharacterWithDetails(
imagePath = imagePath,
request = request
)
// 원작 연결: originalWorkId가 있으면 서비스 계층을 통해 배정
if (request.originalWorkId != null) {
// 서비스에서 유효성 검증 및 저장까지 처리
originalWorkService.assignOneCharacter(request.originalWorkId, request.id)
}
ApiResponse.ok(null)
}
/**
* 요청에 변경된 데이터가 있는지 확인
* id를 제외한 모든 필드가 null이면 변경된 데이터가 없는 것으로 판단
*
* @param request 수정 요청 데이터
* @return 변경된 데이터가 있으면 true, 없으면 false
*/
private fun hasChanges(request: ChatCharacterUpdateRequest): Boolean {
return request.systemPrompt != null ||
request.description != null ||
request.age != null ||
request.gender != null ||
request.mbti != null ||
request.speechPattern != null ||
request.speechStyle != null ||
request.appearance != null ||
request.isActive != null ||
request.tags != null ||
request.hobbies != null ||
request.values != null ||
request.goals != null ||
request.relationships != null ||
request.personalities != null ||
request.backgrounds != null ||
request.memories != null ||
request.name != null
}
/**
* 외부 API 호출 - 수정 API
* 변경된 데이터만 요청에 포함
*
* @param characterUUID 캐릭터 UUID
* @param request 수정 요청 데이터
*/
private fun callExternalApiForUpdate(characterUUID: String, request: ChatCharacterUpdateRequest) {
try {
val factory = SimpleClientHttpRequestFactory()
factory.setConnectTimeout(20000) // 20초
factory.setReadTimeout(20000) // 20초
val restTemplate = RestTemplate(factory)
val headers = HttpHeaders()
headers.set("x-api-key", apiKey)
headers.contentType = MediaType.APPLICATION_JSON
// 변경된 데이터만 포함하는 맵 생성
val updateData = mutableMapOf<String, Any>()
// isActive = false인 경우 처리
if (request.isActive != null && !request.isActive) {
val inactiveName = "inactive_${request.name}"
val randomSuffix = "_" + java.util.UUID.randomUUID().toString().replace("-", "")
updateData["name"] = inactiveName + randomSuffix
} else {
request.name?.let { updateData["name"] = it }
request.systemPrompt?.let { updateData["systemPrompt"] = it }
request.description?.let { updateData["description"] = it }
request.age?.let { updateData["age"] = it }
request.gender?.let { updateData["gender"] = it }
request.mbti?.let { updateData["mbti"] = it }
request.speechPattern?.let { updateData["speechPattern"] = it }
request.speechStyle?.let { updateData["speechStyle"] = it }
request.appearance?.let { updateData["appearance"] = it }
request.tags?.let { updateData["tags"] = it }
request.hobbies?.let { updateData["hobbies"] = it }
request.values?.let { updateData["values"] = it }
request.goals?.let { updateData["goals"] = it }
request.relationships?.let { updateData["relationships"] = it }
request.personalities?.let { updateData["personalities"] = it }
request.backgrounds?.let { updateData["backgrounds"] = it }
request.memories?.let { updateData["memories"] = it }
}
val httpEntity = HttpEntity(updateData, headers)
val response = restTemplate.exchange(
"$apiUrl/api/characters/$characterUUID",
HttpMethod.PUT,
httpEntity,
String::class.java
)
// 응답 파싱
val objectMapper = ObjectMapper()
val apiResponse = objectMapper.readValue(response.body, ExternalApiResponse::class.java)
// success가 false이면 throw
if (!apiResponse.success) {
throw SodaException(apiResponse.message ?: "수정에 실패했습니다. 다시 시도해 주세요.")
}
} catch (e: Exception) {
e.printStackTrace()
throw SodaException("${e.message} 수정에 실패했습니다. 다시 시도해 주세요.")
}
}
}

View File

@@ -0,0 +1,82 @@
package kr.co.vividnext.sodalive.admin.chat.character.curation
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/admin/chat/character/curation")
@PreAuthorize("hasRole('ADMIN')")
class CharacterCurationAdminController(
private val service: CharacterCurationAdminService,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
@GetMapping("/list")
fun listAll(): ApiResponse<List<CharacterCurationListItemResponse>> =
ApiResponse.ok(service.listAll())
@GetMapping("/{curationId}/characters")
fun listCharacters(
@PathVariable curationId: Long
): ApiResponse<List<CharacterCurationCharacterItemResponse>> {
val characters = service.listCharacters(curationId)
val items = characters.map {
CharacterCurationCharacterItemResponse(
id = it.id!!,
name = it.name,
description = it.description,
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
)
}
return ApiResponse.ok(items)
}
@PostMapping("/register")
fun register(@RequestBody request: CharacterCurationRegisterRequest) =
ApiResponse.ok(service.register(request).id)
@PutMapping("/update")
fun update(@RequestBody request: CharacterCurationUpdateRequest) =
ApiResponse.ok(service.update(request).id)
@DeleteMapping("/{curationId}")
fun delete(@PathVariable curationId: Long) =
ApiResponse.ok(service.softDelete(curationId))
@PutMapping("/reorder")
fun reorder(@RequestBody request: CharacterCurationOrderUpdateRequest) =
ApiResponse.ok(service.reorder(request.ids))
@PostMapping("/{curationId}/characters")
fun addCharacter(
@PathVariable curationId: Long,
@RequestBody request: CharacterCurationAddCharacterRequest
): ApiResponse<Boolean> {
val ids = request.characterIds.filter { it > 0 }.distinct()
if (ids.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다")
service.addCharacters(curationId, ids)
return ApiResponse.ok(true)
}
@DeleteMapping("/{curationId}/characters/{characterId}")
fun removeCharacter(
@PathVariable curationId: Long,
@PathVariable characterId: Long
) = ApiResponse.ok(service.removeCharacter(curationId, characterId))
@PutMapping("/{curationId}/characters/reorder")
fun reorderCharacters(
@PathVariable curationId: Long,
@RequestBody request: CharacterCurationReorderCharactersRequest
) = ApiResponse.ok(service.reorderCharacters(curationId, request.characterIds))
}

View File

@@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.admin.chat.character.curation
data class CharacterCurationRegisterRequest(
val title: String,
val isAdult: Boolean = false,
val isActive: Boolean = true
)
data class CharacterCurationUpdateRequest(
val id: Long,
val title: String? = null,
val isAdult: Boolean? = null,
val isActive: Boolean? = null
)
data class CharacterCurationOrderUpdateRequest(
val ids: List<Long>
)
data class CharacterCurationAddCharacterRequest(
val characterIds: List<Long>
)
data class CharacterCurationReorderCharactersRequest(
val characterIds: List<Long>
)
data class CharacterCurationListItemResponse(
val id: Long,
val title: String,
val isAdult: Boolean,
val isActive: Boolean,
val characterCount: Int
)
// 관리자 큐레이션 상세 - 캐릭터 리스트 항목 응답 DTO
// id, name, description, 이미지 URL
// 이미지 URL은 컨트롤러에서 cloud-front host + imagePath로 구성
data class CharacterCurationCharacterItemResponse(
val id: Long,
val name: String,
val description: String,
val imageUrl: String
)

View File

@@ -0,0 +1,153 @@
package kr.co.vividnext.sodalive.admin.chat.character.curation
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.chat.character.curation.CharacterCuration
import kr.co.vividnext.sodalive.chat.character.curation.CharacterCurationMapping
import kr.co.vividnext.sodalive.chat.character.curation.repository.CharacterCurationMappingRepository
import kr.co.vividnext.sodalive.chat.character.curation.repository.CharacterCurationRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class CharacterCurationAdminService(
private val curationRepository: CharacterCurationRepository,
private val mappingRepository: CharacterCurationMappingRepository,
private val characterRepository: ChatCharacterRepository
) {
@Transactional
fun register(request: CharacterCurationRegisterRequest): CharacterCuration {
val sortOrder = (curationRepository.findMaxSortOrder() ?: 0) + 1
val curation = CharacterCuration(
title = request.title,
isAdult = request.isAdult,
isActive = request.isActive,
sortOrder = sortOrder
)
return curationRepository.save(curation)
}
@Transactional
fun update(request: CharacterCurationUpdateRequest): CharacterCuration {
val curation = curationRepository.findById(request.id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: ${request.id}") }
request.title?.let { curation.title = it }
request.isAdult?.let { curation.isAdult = it }
request.isActive?.let { curation.isActive = it }
return curationRepository.save(curation)
}
@Transactional
fun softDelete(curationId: Long) {
val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
curation.isActive = false
curationRepository.save(curation)
}
@Transactional
fun reorder(ids: List<Long>) {
ids.forEachIndexed { index, id ->
val curation = curationRepository.findById(id)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $id") }
curation.sortOrder = index + 1
curationRepository.save(curation)
}
}
@Transactional
fun addCharacters(curationId: Long, characterIds: List<Long>) {
if (characterIds.isEmpty()) throw SodaException("등록할 캐릭터 ID 리스트가 비어있습니다")
val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
if (!curation.isActive) throw SodaException("비활성화된 큐레이션입니다: $curationId")
val uniqueIds = characterIds.filter { it > 0 }.distinct()
if (uniqueIds.isEmpty()) throw SodaException("유효한 캐릭터 ID가 없습니다")
// 활성 캐릭터만 조회 (조회 단계에서 검증 포함)
val characters = characterRepository.findByIdInAndIsActiveTrue(uniqueIds)
val characterMap = characters.associateBy { it.id!! }
// 조회 결과에 존재하는 캐릭터만 유효
val validIds = uniqueIds.filter { id -> characterMap.containsKey(id) }
val existingMappings = mappingRepository.findByCuration(curation)
val existingCharacterIds = existingMappings.mapNotNull { it.chatCharacter.id }.toSet()
var nextOrder = (existingMappings.maxOfOrNull { it.sortOrder } ?: 0) + 1
val toSave = mutableListOf<CharacterCurationMapping>()
validIds.forEach { id ->
if (!existingCharacterIds.contains(id)) {
val character = characterMap[id] ?: return@forEach
toSave += CharacterCurationMapping(
curation = curation,
chatCharacter = character,
sortOrder = nextOrder++
)
}
}
if (toSave.isNotEmpty()) {
mappingRepository.saveAll(toSave)
}
}
@Transactional
fun removeCharacter(curationId: Long, characterId: Long) {
val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
val mappings = mappingRepository.findByCuration(curation)
val target = mappings.firstOrNull { it.chatCharacter.id == characterId }
?: throw SodaException("매핑을 찾을 수 없습니다: curation=$curationId, character=$characterId")
mappingRepository.delete(target)
}
@Transactional
fun reorderCharacters(curationId: Long, characterIds: List<Long>) {
val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
val mappings = mappingRepository.findByCuration(curation)
val mappingByCharacterId = mappings.associateBy { it.chatCharacter.id }
characterIds.forEachIndexed { index, cid ->
val mapping = mappingByCharacterId[cid]
?: throw SodaException("큐레이션에 포함되지 않은 캐릭터입니다: $cid")
mapping.sortOrder = index + 1
mappingRepository.save(mapping)
}
}
@Transactional(readOnly = true)
fun listAll(): List<CharacterCurationListItemResponse> {
val curations = curationRepository.findByIsActiveTrueOrderBySortOrderAsc()
if (curations.isEmpty()) return emptyList()
// DB 집계로 활성 캐릭터 수 카운트
val counts = mappingRepository.countActiveCharactersByCurations(curations)
val countByCurationId: Map<Long, Int> = counts.associate { it.curationId to it.count.toInt() }
return curations.map { curation ->
CharacterCurationListItemResponse(
id = curation.id!!,
title = curation.title,
isAdult = curation.isAdult,
isActive = curation.isActive,
characterCount = countByCurationId[curation.id!!] ?: 0
)
}
}
@Transactional(readOnly = true)
fun listCharacters(curationId: Long): List<ChatCharacter> {
val curation = curationRepository.findById(curationId)
.orElseThrow { SodaException("큐레이션을 찾을 수 없습니다: $curationId") }
val mappings = mappingRepository.findByCurationWithCharacterOrderBySortOrderAsc(curation)
return mappings.map { it.chatCharacter }
}
}

View File

@@ -0,0 +1,132 @@
package kr.co.vividnext.sodalive.admin.chat.character.dto
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
/**
* 관리자 캐릭터 상세 응답 DTO
* - 원작이 연결되어 있으면 원작 요약 정보(originalWork)를 함께 반환한다.
*/
data class ChatCharacterDetailResponse(
val id: Long,
val characterUUID: String,
val name: String,
val imageUrl: String?,
val description: String,
val systemPrompt: String,
val characterType: String,
val age: Int?,
val gender: String?,
val mbti: String?,
val speechPattern: String?,
val speechStyle: String?,
val appearance: String?,
val isActive: Boolean,
val tags: List<String>,
val hobbies: List<String>,
val values: List<String>,
val goals: List<String>,
val relationships: List<RelationshipResponse>,
val personalities: List<PersonalityResponse>,
val backgrounds: List<BackgroundResponse>,
val memories: List<MemoryResponse>,
val originalWork: OriginalWorkBriefResponse? // 추가: 원작 요약 정보
) {
companion object {
fun from(chatCharacter: ChatCharacter, imageHost: String = ""): ChatCharacterDetailResponse {
val fullImagePath = if (chatCharacter.imagePath != null && imageHost.isNotEmpty()) {
"$imageHost/${chatCharacter.imagePath}"
} else {
chatCharacter.imagePath ?: ""
}
val ow = chatCharacter.originalWork
val originalWorkBrief = ow?.let {
val owImage = if (it.imagePath != null && imageHost.isNotEmpty()) {
"$imageHost/${it.imagePath}"
} else {
it.imagePath
}
OriginalWorkBriefResponse(
id = it.id!!,
imageUrl = owImage,
title = it.title
)
}
return ChatCharacterDetailResponse(
id = chatCharacter.id!!,
characterUUID = chatCharacter.characterUUID,
name = chatCharacter.name,
imageUrl = fullImagePath,
description = chatCharacter.description,
systemPrompt = chatCharacter.systemPrompt,
characterType = chatCharacter.characterType.name,
age = chatCharacter.age,
gender = chatCharacter.gender,
mbti = chatCharacter.mbti,
speechPattern = chatCharacter.speechPattern,
speechStyle = chatCharacter.speechStyle,
appearance = chatCharacter.appearance,
isActive = chatCharacter.isActive,
tags = chatCharacter.tagMappings.map { it.tag.tag },
hobbies = chatCharacter.hobbyMappings.map { it.hobby.hobby },
values = chatCharacter.valueMappings.map { it.value.value },
goals = chatCharacter.goalMappings.map { it.goal.goal },
relationships = chatCharacter.relationships.map {
RelationshipResponse(
personName = it.personName,
relationshipName = it.relationshipName,
description = it.description,
importance = it.importance,
relationshipType = it.relationshipType,
currentStatus = it.currentStatus
)
},
personalities = chatCharacter.personalities.map {
PersonalityResponse(it.trait, it.description)
},
backgrounds = chatCharacter.backgrounds.map {
BackgroundResponse(it.topic, it.description)
},
memories = chatCharacter.memories.map {
MemoryResponse(it.title, it.content, it.emotion)
},
originalWork = originalWorkBrief
)
}
}
}
data class PersonalityResponse(
val trait: String,
val description: String
)
data class BackgroundResponse(
val topic: String,
val description: String
)
data class MemoryResponse(
val title: String,
val content: String,
val emotion: String
)
data class RelationshipResponse(
val personName: String,
val relationshipName: String,
val description: String,
val importance: Int,
val relationshipType: String,
val currentStatus: String
)
/**
* 원작 요약 응답 DTO(관리자 캐릭터 상세용)
*/
data class OriginalWorkBriefResponse(
val id: Long,
val imageUrl: String?,
val title: String
)

View File

@@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.admin.chat.character.dto
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
data class ChatCharacterPersonalityRequest(
@JsonProperty("trait") val trait: String,
@JsonProperty("description") val description: String
)
data class ChatCharacterBackgroundRequest(
@JsonProperty("topic") val topic: String,
@JsonProperty("description") val description: String
)
data class ChatCharacterMemoryRequest(
@JsonProperty("title") val title: String,
@JsonProperty("content") val content: String,
@JsonProperty("emotion") val emotion: String
)
data class ChatCharacterRelationshipRequest(
@JsonProperty("personName") val personName: String,
@JsonProperty("relationshipName") val relationshipName: String,
@JsonProperty("description") val description: String,
@JsonProperty("importance") val importance: Int,
@JsonProperty("relationshipType") val relationshipType: String,
@JsonProperty("currentStatus") val currentStatus: String
)
data class ChatCharacterRegisterRequest(
@JsonProperty("name") val name: String,
@JsonProperty("systemPrompt") val systemPrompt: String,
@JsonProperty("description") val description: String,
@JsonProperty("age") val age: String?,
@JsonProperty("gender") val gender: String?,
@JsonProperty("mbti") val mbti: String?,
@JsonProperty("speechPattern") val speechPattern: String?,
@JsonProperty("speechStyle") val speechStyle: String?,
@JsonProperty("appearance") val appearance: String?,
@JsonProperty("originalTitle") val originalTitle: String? = null,
@JsonProperty("originalLink") val originalLink: String? = null,
@JsonProperty("originalWorkId") val originalWorkId: Long? = null,
@JsonProperty("characterType") val characterType: String? = null,
@JsonProperty("tags") val tags: List<String> = emptyList(),
@JsonProperty("hobbies") val hobbies: List<String> = emptyList(),
@JsonProperty("values") val values: List<String> = emptyList(),
@JsonProperty("goals") val goals: List<String> = emptyList(),
@JsonProperty("relationships") val relationships: List<ChatCharacterRelationshipRequest> = emptyList(),
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest> = emptyList(),
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest> = emptyList(),
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest> = emptyList()
)
data class ExternalApiResponse(
@JsonProperty("success") val success: Boolean,
@JsonProperty("data") val data: ExternalApiData? = null,
@JsonProperty("message") val message: String? = null
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class ExternalApiData(
@JsonProperty("id") val id: String
)
data class ChatCharacterUpdateRequest(
@JsonProperty("id") val id: Long,
@JsonProperty("name") val name: String? = null,
@JsonProperty("systemPrompt") val systemPrompt: String? = null,
@JsonProperty("description") val description: String? = null,
@JsonProperty("age") val age: String? = null,
@JsonProperty("gender") val gender: String? = null,
@JsonProperty("mbti") val mbti: String? = null,
@JsonProperty("speechPattern") val speechPattern: String? = null,
@JsonProperty("speechStyle") val speechStyle: String? = null,
@JsonProperty("appearance") val appearance: String? = null,
@JsonProperty("originalTitle") val originalTitle: String? = null,
@JsonProperty("originalLink") val originalLink: String? = null,
@JsonProperty("originalWorkId") val originalWorkId: Long? = null,
@JsonProperty("characterType") val characterType: String? = null,
@JsonProperty("isActive") val isActive: Boolean? = null,
@JsonProperty("tags") val tags: List<String>? = null,
@JsonProperty("hobbies") val hobbies: List<String>? = null,
@JsonProperty("values") val values: List<String>? = null,
@JsonProperty("goals") val goals: List<String>? = null,
@JsonProperty("relationships") val relationships: List<ChatCharacterRelationshipRequest>? = null,
@JsonProperty("personalities") val personalities: List<ChatCharacterPersonalityRequest>? = null,
@JsonProperty("backgrounds") val backgrounds: List<ChatCharacterBackgroundRequest>? = null,
@JsonProperty("memories") val memories: List<ChatCharacterMemoryRequest>? = null
)

View File

@@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.admin.chat.character.dto
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import java.time.ZoneId
import java.time.format.DateTimeFormatter
data class ChatCharacterListResponse(
val id: Long,
val name: String,
val imageUrl: String?,
val description: String,
val gender: String?,
val age: Int?,
val mbti: String?,
val speechStyle: String?,
val speechPattern: String?,
val tags: List<String>,
val createdAt: String?,
val updatedAt: String?
) {
companion object {
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private val seoulZoneId = ZoneId.of("Asia/Seoul")
fun from(chatCharacter: ChatCharacter, imageHost: String = ""): ChatCharacterListResponse {
val fullImagePath = if (chatCharacter.imagePath != null && imageHost.isNotEmpty()) {
"$imageHost/${chatCharacter.imagePath}"
} else {
chatCharacter.imagePath
}
// UTC에서 Asia/Seoul로 시간대 변환 및 문자열 포맷팅
val createdAtStr = chatCharacter.createdAt?.atZone(ZoneId.of("UTC"))
?.withZoneSameInstant(seoulZoneId)
?.format(formatter)
val updatedAtStr = chatCharacter.updatedAt?.atZone(ZoneId.of("UTC"))
?.withZoneSameInstant(seoulZoneId)
?.format(formatter)
return ChatCharacterListResponse(
id = chatCharacter.id!!,
name = chatCharacter.name,
imageUrl = fullImagePath,
description = chatCharacter.description,
gender = chatCharacter.gender,
age = chatCharacter.age,
mbti = chatCharacter.mbti,
speechStyle = chatCharacter.speechStyle,
speechPattern = chatCharacter.speechPattern,
tags = chatCharacter.tagMappings.map { it.tag.tag },
createdAt = createdAtStr,
updatedAt = updatedAtStr
)
}
}
}
data class ChatCharacterListPageResponse(
val totalCount: Long,
val content: List<ChatCharacterListResponse>
)

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.admin.chat.character.dto
/**
* 캐릭터 검색 결과 페이지 응답 DTO
*/
data class ChatCharacterSearchListPageResponse(
val totalCount: Long,
val content: List<ChatCharacterListResponse>
)

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.chat.character.dto
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
/**
* 원작 연결된 캐릭터 결과 응답 DTO
*/
data class OriginalWorkChatCharacterResponse(
val id: Long,
val name: String,
val imagePath: String?
) {
companion object {
fun from(character: ChatCharacter, imageHost: String): OriginalWorkChatCharacterResponse {
return OriginalWorkChatCharacterResponse(
id = character.id!!,
name = character.name,
imagePath = character.imagePath?.let { "$imageHost/$it" }
)
}
}
}
/**
* 원작 연결된 캐릭터 결과 페이지 응답 DTO
*/
data class OriginalWorkChatCharacterListPageResponse(
val totalCount: Long,
val content: List<OriginalWorkChatCharacterResponse>
)

View File

@@ -0,0 +1,170 @@
package kr.co.vividnext.sodalive.admin.chat.character.image
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.chat.character.image.dto.AdminCharacterImageResponse
import kr.co.vividnext.sodalive.admin.chat.character.image.dto.RegisterCharacterImageRequest
import kr.co.vividnext.sodalive.admin.chat.character.image.dto.UpdateCharacterImageOrdersRequest
import kr.co.vividnext.sodalive.admin.chat.character.image.dto.UpdateCharacterImageTriggersRequest
import kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.ImageBlurUtil
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
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.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/chat/character/image")
@PreAuthorize("hasRole('ADMIN')")
class AdminCharacterImageController(
private val imageService: CharacterImageService,
private val s3Uploader: S3Uploader,
private val imageCloudFront: ImageContentCloudFront,
@Value("\${cloud.aws.s3.content-bucket}")
private val s3Bucket: String,
@Value("\${cloud.aws.s3.bucket}")
private val freeBucket: String
) {
@GetMapping("/list")
fun list(@RequestParam characterId: Long) = run {
val expiration = 5L * 60L * 1000L // 5분
val list = imageService.listActiveByCharacter(characterId)
.map { img ->
val signedUrl = imageCloudFront.generateSignedURL(img.imagePath, expiration)
AdminCharacterImageResponse.fromWithUrl(img, signedUrl)
}
ApiResponse.ok(list)
}
@GetMapping("/{imageId}")
fun detail(@PathVariable imageId: Long) = run {
val img = imageService.getById(imageId)
val expiration = 5L * 60L * 1000L // 5분
val signedUrl = imageCloudFront.generateSignedURL(img.imagePath, expiration)
ApiResponse.ok(AdminCharacterImageResponse.fromWithUrl(img, signedUrl))
}
@PostMapping("/register")
fun register(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = run {
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(requestString, RegisterCharacterImageRequest::class.java)
// 업로드 키 생성
val s3Key = buildS3Key(characterId = request.characterId)
// 원본 저장 (content-bucket)
val imagePath = saveImageToBucket(s3Key, image, s3Bucket)
// 블러 생성 및 저장 (무료 이미지 버킷)
val blurImagePath = saveBlurImageToBucket(s3Key, image, freeBucket)
imageService.registerImage(
characterId = request.characterId,
imagePath = imagePath,
blurImagePath = blurImagePath,
imagePriceCan = request.imagePriceCan,
messagePriceCan = request.messagePriceCan,
isAdult = request.isAdult,
triggers = request.triggers ?: emptyList()
)
ApiResponse.ok(null)
}
@PutMapping("/{imageId}/triggers")
fun updateTriggers(
@PathVariable imageId: Long,
@RequestBody request: UpdateCharacterImageTriggersRequest
) = run {
if (!request.triggers.isNullOrEmpty()) {
imageService.updateTriggers(imageId, request.triggers)
}
ApiResponse.ok(null)
}
@DeleteMapping("/{imageId}")
fun delete(@PathVariable imageId: Long) = run {
imageService.deleteImage(imageId)
ApiResponse.ok(null, "이미지가 삭제되었습니다.")
}
@PutMapping("/orders")
fun updateOrders(@RequestBody request: UpdateCharacterImageOrdersRequest) = run {
if (request.characterId == null) throw SodaException("characterId는 필수입니다")
imageService.updateOrders(request.characterId, request.ids)
ApiResponse.ok(null, "정렬 순서가 변경되었습니다.")
}
private fun buildS3Key(characterId: Long): String {
val fileName = generateFileName("character-image")
return "characters/$characterId/images/$fileName"
}
private fun saveImageToBucket(filePath: String, image: MultipartFile, bucket: String): String {
try {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
return s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = filePath,
metadata = metadata
)
} catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
}
}
private fun saveBlurImageToBucket(filePath: String, image: MultipartFile, bucket: String): String {
try {
// 멀티파트를 BufferedImage로 읽기
val bytes = image.bytes
val bimg = javax.imageio.ImageIO.read(java.io.ByteArrayInputStream(bytes))
?: throw SodaException("이미지 포맷을 인식할 수 없습니다.")
val blurred = ImageBlurUtil.blurFast(bimg)
// PNG로 저장(알파 유지), JPEG 업로드가 필요하면 포맷 변경 가능
val baos = java.io.ByteArrayOutputStream()
val format = when (image.contentType?.lowercase()) {
"image/png" -> "png"
else -> "jpg"
}
javax.imageio.ImageIO.write(blurred, format, baos)
val inputStream = java.io.ByteArrayInputStream(baos.toByteArray())
val metadata = ObjectMetadata()
metadata.contentLength = baos.size().toLong()
metadata.contentType = image.contentType ?: if (format == "png") "image/png" else "image/jpeg"
return s3Uploader.upload(
inputStream = inputStream,
bucket = bucket,
filePath = filePath,
metadata = metadata
)
} catch (e: Exception) {
throw SodaException("블러 이미지 저장에 실패했습니다: ${e.message}")
}
}
}

View File

@@ -0,0 +1,53 @@
package kr.co.vividnext.sodalive.admin.chat.character.image.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.chat.character.image.CharacterImage
// 요청 DTOs
data class RegisterCharacterImageRequest(
@JsonProperty("characterId") val characterId: Long,
@JsonProperty("imagePriceCan") val imagePriceCan: Long,
@JsonProperty("messagePriceCan") val messagePriceCan: Long,
@JsonProperty("isAdult") val isAdult: Boolean = false,
@JsonProperty("triggers") val triggers: List<String>? = null
)
data class UpdateCharacterImageTriggersRequest(
@JsonProperty("triggers") val triggers: List<String>? = null
)
data class UpdateCharacterImageOrdersRequest(
@JsonProperty("characterId") val characterId: Long?,
@JsonProperty("ids") val ids: List<Long>
)
// 응답 DTOs
data class AdminCharacterImageResponse(
val id: Long,
val characterId: Long,
val imagePriceCan: Long,
val messagePriceCan: Long,
val imageUrl: String,
val triggers: List<String>,
val isAdult: Boolean
) {
companion object {
fun fromWithUrl(entity: CharacterImage, signedUrl: String): AdminCharacterImageResponse {
return base(entity, signedUrl)
}
private fun base(entity: CharacterImage, url: String): AdminCharacterImageResponse {
return AdminCharacterImageResponse(
id = entity.id!!,
characterId = entity.chatCharacter.id!!,
imagePriceCan = entity.imagePriceCan,
messagePriceCan = entity.messagePriceCan,
imageUrl = url,
triggers = entity.triggerMappings.map { it.tag.word },
isAdult = entity.isAdult
)
}
}
}

View File

@@ -0,0 +1,78 @@
package kr.co.vividnext.sodalive.admin.chat.character.service
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterDetailResponse
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminChatCharacterService(
private val chatCharacterRepository: ChatCharacterRepository
) {
/**
* 활성화된 캐릭터 목록을 페이징하여 조회
*
* @param pageable 페이징 정보
* @return 페이징된 캐릭터 목록
*/
@Transactional(readOnly = true)
fun getActiveChatCharacters(pageable: Pageable, imageHost: String = ""): ChatCharacterListPageResponse {
// isActive가 true인 캐릭터만 조회
val page = chatCharacterRepository.findByIsActiveTrue(pageable)
// 페이지 정보 생성
val content = page.content.map { ChatCharacterListResponse.from(it, imageHost) }
return ChatCharacterListPageResponse(
totalCount = page.totalElements,
content = content
)
}
/**
* 기본 페이지 요청 생성
*
* @param page 페이지 번호 (0부터 시작)
* @param size 페이지 크기
* @return 페이지 요청 객체
*/
fun createDefaultPageRequest(page: Int = 0, size: Int = 20): PageRequest {
return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
}
/**
* 캐릭터 상세 정보 조회
*
* @param characterId 캐릭터 ID
* @param imageHost 이미지 호스트 URL
* @return 캐릭터 상세 정보
* @throws SodaException 캐릭터를 찾을 수 없는 경우
*/
@Transactional(readOnly = true)
fun getChatCharacterDetail(characterId: Long, imageHost: String = ""): ChatCharacterDetailResponse {
val chatCharacter = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 ID의 캐릭터를 찾을 수 없습니다: $characterId") }
return ChatCharacterDetailResponse.from(chatCharacter, imageHost)
}
/**
* 캐릭터 검색 (이름, 설명, MBTI, 태그 기반) - 페이징 (기존 사용처 호환용)
*/
@Transactional(readOnly = true)
fun searchCharacters(
searchTerm: String,
pageable: Pageable,
imageHost: String = ""
): Page<ChatCharacterListResponse> {
val characters = chatCharacterRepository.searchCharacters(searchTerm, pageable)
return characters.map { ChatCharacterListResponse.from(it, imageHost) }
}
}

View File

@@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.chat.dto
import com.fasterxml.jackson.annotation.JsonProperty
/**
* 캐릭터 배너 등록 요청 DTO
*/
data class ChatCharacterBannerRegisterRequest(
// 캐릭터 ID
@JsonProperty("characterId") val characterId: Long
)
/**
* 캐릭터 배너 수정 요청 DTO
*/
data class ChatCharacterBannerUpdateRequest(
// 배너 ID
@JsonProperty("bannerId") val bannerId: Long,
// 캐릭터 ID (변경할 캐릭터)
@JsonProperty("characterId") val characterId: Long? = null
)
/**
* 캐릭터 배너 정렬 순서 일괄 변경 요청 DTO
*/
data class UpdateBannerOrdersRequest(
// 배너 ID 목록 (순서대로 정렬됨)
@JsonProperty("ids") val ids: List<Long>
)

View File

@@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.admin.chat.dto
import kr.co.vividnext.sodalive.chat.character.ChatCharacterBanner
/**
* 캐릭터 배너 응답 DTO
*/
data class ChatCharacterBannerResponse(
val id: Long,
val imagePath: String,
val characterId: Long,
val characterName: String
) {
companion object {
fun from(banner: ChatCharacterBanner, imageHost: String): ChatCharacterBannerResponse {
return ChatCharacterBannerResponse(
id = banner.id!!,
imagePath = "$imageHost/${banner.imagePath}",
characterId = banner.chatCharacter.id!!,
characterName = banner.chatCharacter.name
)
}
}
}
/**
* 캐릭터 배너 목록 페이지 응답 DTO
*/
data class ChatCharacterBannerListPageResponse(
val totalCount: Long,
val content: List<ChatCharacterBannerResponse>
)

View File

@@ -0,0 +1,199 @@
package kr.co.vividnext.sodalive.admin.chat.original
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterListPageResponse
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterResponse
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkAssignCharactersRequest
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkPageResponse
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkRegisterRequest
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkResponse
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkUpdateRequest
import kr.co.vividnext.sodalive.admin.chat.original.service.AdminOriginalWorkService
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
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.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
/**
* 원작(오리지널 작품) 관리자 API
* - 원작 등록/수정/삭제
* - 원작과 캐릭터 연결(배정) 및 해제
*/
@RestController
@RequestMapping("/admin/chat/original")
@PreAuthorize("hasRole('ADMIN')")
class AdminOriginalWorkController(
private val originalWorkService: AdminOriginalWorkService,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val s3Bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) {
/**
* 원작 등록
* - 이미지 파일과 JSON 요청을 멀티파트로 받는다.
*/
@PostMapping("/register")
fun register(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = run {
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(requestString, OriginalWorkRegisterRequest::class.java)
// 서비스 계층을 통해 원작을 생성
val saved = originalWorkService.createOriginalWork(request)
// 이미지 업로드 후 이미지 경로 업데이트
val imagePath = uploadImage(saved.id!!, image)
originalWorkService.updateOriginalWorkImage(saved.id!!, imagePath)
ApiResponse.ok(null)
}
/**
* 원작 수정
* - 이미지가 있으면 교체, 없으면 유지
*/
@PutMapping("/update")
fun update(
@RequestPart(value = "image", required = false) image: MultipartFile?,
@RequestPart("request") requestString: String
) = run {
val objectMapper = ObjectMapper()
val request = objectMapper.readValue(requestString, OriginalWorkUpdateRequest::class.java)
// 이미지가 전달된 경우 먼저 업로드하여 경로를 생성
val imagePath = if (image != null && !image.isEmpty) {
uploadImage(request.id, image)
} else {
null
}
originalWorkService.updateOriginalWork(request, imagePath)
ApiResponse.ok(null)
}
/**
* 원작 삭제
*/
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long) = run {
originalWorkService.deleteOriginalWork(id)
ApiResponse.ok(null)
}
/**
* 원작 목록(페이징)
*/
@GetMapping("/list")
fun list(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageRes = originalWorkService.getOriginalWorkPage(page, size)
val content = pageRes.content.map { OriginalWorkResponse.from(it, imageHost) }
ApiResponse.ok(OriginalWorkPageResponse(totalCount = pageRes.totalElements, content = content))
}
/**
* 원작 검색(관리자)
* - 제목/콘텐츠타입/카테고리 기준 부분 검색, 소프트 삭제 제외
* - 페이징 제거: 전체 목록 반환
*/
@GetMapping("/search")
fun search(
@RequestParam("searchTerm") searchTerm: String
) = run {
val list = originalWorkService.searchOriginalWorksAll(searchTerm)
val content = list.map { OriginalWorkResponse.from(it, imageHost) }
ApiResponse.ok(content)
}
/**
* 원작 상세
*/
@GetMapping("/{id}")
fun detail(@PathVariable id: Long) = run {
ApiResponse.ok(OriginalWorkResponse.from(originalWorkService.getOriginalWork(id), imageHost))
}
/**
* 원작에 기존 캐릭터들을 배정
* - 캐릭터는 하나의 원작에만 속하므로, 해당 캐릭터들의 originalWork를 이 원작으로 설정
*/
@PostMapping("/{id}/assign-characters")
fun assignCharacters(
@PathVariable id: Long,
@RequestBody body: OriginalWorkAssignCharactersRequest
) = run {
originalWorkService.assignCharacters(id, body.characterIds)
ApiResponse.ok(null)
}
/**
* 원작에서 캐릭터들 해제
* - 캐릭터들의 originalWork를 null로 설정
*/
@PostMapping("/{id}/unassign-characters")
fun unassignCharacters(
@PathVariable id: Long,
@RequestBody body: OriginalWorkAssignCharactersRequest
) = run {
originalWorkService.unassignCharacters(id, body.characterIds)
ApiResponse.ok(null)
}
/**
* 관리자용: 지정 원작에 속한 캐릭터 목록 페이징 조회
* - 활성 캐릭터만 포함
* - 응답 항목: 캐릭터 이미지(URL), 이름
*/
@GetMapping("/{id}/characters")
fun listCharactersOfOriginal(
@PathVariable id: Long,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int
) = run {
val pageRes = originalWorkService.getCharactersOfOriginalWorkPage(id, page, size)
val content = pageRes.content.map { OriginalWorkChatCharacterResponse.from(it, imageHost) }
ApiResponse.ok(
OriginalWorkChatCharacterListPageResponse(
totalCount = pageRes.totalElements,
content = content
)
)
}
/** 이미지 업로드 공통 처리 */
private fun uploadImage(originalWorkId: Long, image: MultipartFile): String {
try {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
return s3Uploader.upload(
inputStream = image.inputStream,
bucket = s3Bucket,
filePath = "originals/$originalWorkId/${generateFileName(prefix = "original")}",
metadata = metadata
)
} catch (e: Exception) {
throw SodaException("이미지 저장에 실패했습니다: ${e.message}")
}
}
}

View File

@@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.admin.chat.original.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.chat.original.OriginalWork
/**
* 원작 등록 요청 DTO
*/
data class OriginalWorkRegisterRequest(
@JsonProperty("title") val title: String,
@JsonProperty("contentType") val contentType: String,
@JsonProperty("category") val category: String,
@JsonProperty("isAdult") val isAdult: Boolean = false,
@JsonProperty("description") val description: String = "",
@JsonProperty("originalWork") val originalWork: String? = null,
@JsonProperty("originalLink") val originalLink: String? = null,
@JsonProperty("writer") val writer: String? = null,
@JsonProperty("studio") val studio: String? = null,
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
@JsonProperty("tags") val tags: List<String>? = null
)
/**
* 원작 수정 요청 DTO (부분 수정 가능)
*/
data class OriginalWorkUpdateRequest(
@JsonProperty("id") val id: Long,
@JsonProperty("title") val title: String? = null,
@JsonProperty("contentType") val contentType: String? = null,
@JsonProperty("category") val category: String? = null,
@JsonProperty("isAdult") val isAdult: Boolean? = null,
@JsonProperty("description") val description: String? = null,
@JsonProperty("originalWork") val originalWork: String? = null,
@JsonProperty("originalLink") val originalLink: String? = null,
@JsonProperty("writer") val writer: String? = null,
@JsonProperty("studio") val studio: String? = null,
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
@JsonProperty("tags") val tags: List<String>? = null
)
/**
* 원작 상세/목록 응답 DTO
*/
data class OriginalWorkResponse(
val id: Long,
val title: String,
val contentType: String,
val category: String,
val isAdult: Boolean,
val description: String,
val originalWork: String?,
val originalLink: String?,
val writer: String?,
val studio: String?,
val originalLinks: List<String>,
val tags: List<String>,
val imageUrl: String?
) {
companion object {
fun from(entity: OriginalWork, imageHost: String = ""): OriginalWorkResponse {
val fullImagePath = if (entity.imagePath != null && imageHost.isNotEmpty()) {
"$imageHost/${entity.imagePath}"
} else {
entity.imagePath
}
return OriginalWorkResponse(
id = entity.id!!,
title = entity.title,
contentType = entity.contentType,
category = entity.category,
isAdult = entity.isAdult,
description = entity.description,
originalWork = entity.originalWork,
originalLink = entity.originalLink,
writer = entity.writer,
studio = entity.studio,
originalLinks = entity.originalLinks.map { it.url },
tags = entity.tagMappings.map { it.tag.tag },
imageUrl = fullImagePath
)
}
}
}
data class OriginalWorkPageResponse(
val totalCount: Long,
val content: List<OriginalWorkResponse>
)
/**
* 원작-캐릭터 연결/해제 요청 DTO
*/
data class OriginalWorkAssignCharactersRequest(
@JsonProperty("characterIds") val characterIds: List<Long>
)

View File

@@ -0,0 +1,213 @@
package kr.co.vividnext.sodalive.admin.chat.original.service
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkRegisterRequest
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkUpdateRequest
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.chat.original.OriginalWork
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTagMapping
import kr.co.vividnext.sodalive.chat.original.repository.OriginalWorkTagRepository
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
/**
* 원작(오리지널 작품) 관련 관리자 서비스
* - 컨트롤러와 레포지토리 사이의 서비스 계층으로 DB 접근을 캡슐화한다.
*/
@Service
class AdminOriginalWorkService(
private val originalWorkRepository: OriginalWorkRepository,
private val chatCharacterRepository: ChatCharacterRepository,
private val originalWorkTagRepository: OriginalWorkTagRepository
) {
/** 원작 등록 (중복 제목 방지 포함) */
@Transactional
fun createOriginalWork(request: OriginalWorkRegisterRequest): OriginalWork {
originalWorkRepository.findByTitleAndIsDeletedFalse(request.title)?.let {
throw SodaException("동일한 제목의 원작이 이미 존재합니다: ${request.title}")
}
val entity = OriginalWork(
title = request.title,
contentType = request.contentType,
category = request.category,
isAdult = request.isAdult,
description = request.description,
originalWork = request.originalWork,
originalLink = request.originalLink,
writer = request.writer,
studio = request.studio
)
// 링크 리스트 생성
request.originalLinks?.filter { it.isNotBlank() }?.forEach { link ->
entity.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = entity))
}
// 태그 매핑 생성 (기존 태그 재사용)
request.tags?.let { tags ->
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
normalized.forEach { t ->
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
entity.tagMappings.add(OriginalWorkTagMapping(originalWork = entity, tag = tagEntity))
}
}
return originalWorkRepository.save(entity)
}
/** 원작 수정 (이미지 경로 포함 선택적 변경) */
@Transactional
fun updateOriginalWork(request: OriginalWorkUpdateRequest, imagePath: String? = null): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(request.id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
request.title?.let { ow.title = it }
request.contentType?.let { ow.contentType = it }
request.category?.let { ow.category = it }
request.isAdult?.let { ow.isAdult = it }
request.description?.let { ow.description = it }
request.originalWork?.let { ow.originalWork = it }
request.originalLink?.let { ow.originalLink = it }
request.writer?.let { ow.writer = it }
request.studio?.let { ow.studio = it }
// 링크 리스트가 전달되면 기존 것을 교체
request.originalLinks?.let { links ->
ow.originalLinks.clear()
links.filter { it.isNotBlank() }.forEach { link ->
ow.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = ow))
}
}
// 태그 변경사항만 반영 (요청이 null이면 변경 없음)
request.tags?.let { tags ->
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
val current = ow.tagMappings.map { it.tag.tag }.toSet()
val toAdd = normalized.minus(current)
val toRemove = current.minus(normalized)
if (toRemove.isNotEmpty()) {
val itr = ow.tagMappings.iterator()
while (itr.hasNext()) {
val m = itr.next()
if (toRemove.contains(m.tag.tag)) {
itr.remove() // orphanRemoval=true로 매핑 삭제
}
}
}
if (toAdd.isNotEmpty()) {
toAdd.forEach { t ->
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
ow.tagMappings.add(OriginalWorkTagMapping(originalWork = ow, tag = tagEntity))
}
}
}
if (imagePath != null) {
ow.imagePath = imagePath
}
return originalWorkRepository.save(ow)
}
/** 원작 이미지 경로만 별도 갱신 */
@Transactional
fun updateOriginalWorkImage(originalWorkId: Long, imagePath: String): OriginalWork {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
ow.imagePath = imagePath
return originalWorkRepository.save(ow)
}
/** 원작 삭제 (소프트 삭제) */
@Transactional
fun deleteOriginalWork(id: Long) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다: $id") }
ow.isDeleted = true
originalWorkRepository.save(ow)
}
/** 원작 상세 조회 (소프트 삭제 제외) */
@Transactional(readOnly = true)
fun getOriginalWork(id: Long): OriginalWork {
return originalWorkRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
}
/** 원작 페이징 조회 */
@Transactional(readOnly = true)
fun getOriginalWorkPage(page: Int, size: Int): Page<OriginalWork> {
val safePage = if (page < 0) 0 else page
val safeSize = when {
size <= 0 -> 20
size > 100 -> 100
else -> size
}
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
return originalWorkRepository.findByIsDeletedFalse(pageable)
}
/** 지정 원작에 속한 활성 캐릭터 페이징 조회 (최신순) */
@Transactional(readOnly = true)
fun getCharactersOfOriginalWorkPage(originalWorkId: Long, page: Int, size: Int): Page<ChatCharacter> {
// 원작 존재 및 소프트 삭제 여부 확인
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
val safePage = if (page < 0) 0 else page
val safeSize = when {
size <= 0 -> 20
size > 100 -> 100
else -> size
}
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
return chatCharacterRepository.findByOriginalWorkIdAndIsActiveTrue(originalWorkId, pageable)
}
/** 원작 검색 (제목/콘텐츠타입/카테고리, 소프트 삭제 제외) - 무페이징 */
@Transactional(readOnly = true)
fun searchOriginalWorksAll(searchTerm: String): List<OriginalWork> {
return originalWorkRepository.searchNoPaging(searchTerm)
}
/** 원작에 기존 캐릭터들을 배정 */
@Transactional
fun assignCharacters(originalWorkId: Long, characterIds: List<Long>) {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = ow }
chatCharacterRepository.saveAll(characters)
}
/** 원작에서 캐릭터들 해제 */
@Transactional
fun unassignCharacters(originalWorkId: Long, characterIds: List<Long>) {
// 원작 존재 확인 (소프트 삭제 제외)
originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
if (characterIds.isEmpty()) return
val characters = chatCharacterRepository.findByIdInAndIsActiveTrue(characterIds)
characters.forEach { it.originalWork = null }
chatCharacterRepository.saveAll(characters)
}
/** 단일 캐릭터를 지정 원작에 배정 */
@Transactional
fun assignOneCharacter(originalWorkId: Long, characterId: Long) {
val character = chatCharacterRepository.findById(characterId)
.orElseThrow { SodaException("해당 캐릭터를 찾을 수 없습니다") }
if (originalWorkId == 0L) {
character.originalWork = null
} else {
val ow = originalWorkRepository.findByIdAndIsDeletedFalse(originalWorkId)
.orElseThrow { SodaException("해당 원작을 찾을 수 없습니다") }
character.originalWork = ow
}
chatCharacterRepository.save(character)
}
}

View File

@@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.admin.content
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
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
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audio-content")
class AdminContentController(private val service: AdminContentService) {
@GetMapping("/list")
fun getAudioContentList(
@RequestParam(value = "status", required = false) status: ContentReleaseStatus?,
pageable: Pageable
) = ApiResponse.ok(
service.getAudioContentList(
status = status ?: ContentReleaseStatus.OPEN,
pageable
)
)
@GetMapping("/search")
fun searchAudioContent(
@RequestParam(value = "status", required = false) status: ContentReleaseStatus?,
@RequestParam(value = "search_word") searchWord: String,
pageable: Pageable
) = ApiResponse.ok(
service.searchAudioContent(
status = status ?: ContentReleaseStatus.OPEN,
searchWord,
pageable
)
)
@PutMapping
fun modifyAudioContent(
@RequestBody request: UpdateAdminContentRequest
) = ApiResponse.ok(service.updateAudioContent(request))
@GetMapping("/main/tab")
fun getContentMainTabList() = ApiResponse.ok(service.getContentMainTabList())
}
enum class ContentReleaseStatus {
// 콘텐츠가 공개된 상태
OPEN,
// 예약된 콘텐츠, 아직 공개되지 않은 상태
SCHEDULED
}

View File

@@ -0,0 +1,174 @@
package kr.co.vividnext.sodalive.admin.content
import com.querydsl.core.types.dsl.DateTimePath
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.StringTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.hashtag.QAudioContentHashTag.audioContentHashTag
import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
interface AdminContentRepository : JpaRepository<AudioContent, Long>, AdminAudioContentQueryRepository
interface AdminAudioContentQueryRepository {
fun getAudioContentTotalCount(
searchWord: String = "",
status: ContentReleaseStatus = ContentReleaseStatus.OPEN
): Int
fun getAudioContentList(
status: ContentReleaseStatus = ContentReleaseStatus.OPEN,
offset: Long,
limit: Long,
searchWord: String = ""
): List<GetAdminContentListItem>
fun getHashTagList(audioContentId: Long): List<String>
fun findByIdAndActiveTrue(audioContentId: Long): AudioContent?
}
class AdminAudioContentQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) : AdminAudioContentQueryRepository {
override fun getAudioContentTotalCount(
searchWord: String,
status: ContentReleaseStatus
): Int {
val now = LocalDateTime.now()
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull))
if (searchWord.trim().length > 1) {
where = where.and(
audioContent.title.contains(searchWord)
.or(audioContent.member.nickname.contains(searchWord))
)
}
where = if (status == ContentReleaseStatus.SCHEDULED) {
where.and(audioContent.releaseDate.after(now))
} else {
where.and(audioContent.releaseDate.before(now))
}
return queryFactory
.select(audioContent.id)
.from(audioContent)
.where(where)
.fetch()
.size
}
override fun getAudioContentList(
status: ContentReleaseStatus,
offset: Long,
limit: Long,
searchWord: String
): List<GetAdminContentListItem> {
val now = LocalDateTime.now()
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull))
if (searchWord.trim().length > 1) {
where = where.and(
audioContent.title.contains(searchWord)
.or(audioContent.member.nickname.contains(searchWord))
)
}
where = if (status == ContentReleaseStatus.SCHEDULED) {
where.and(audioContent.releaseDate.after(now))
} else {
where.and(audioContent.releaseDate.before(now))
}
return queryFactory
.select(
QGetAdminContentListItem(
audioContent.id,
audioContent.title,
audioContent.detail,
audioContentCuration.title,
audioContentCuration.id.nullif(0),
audioContent.coverImage.prepend("/").prepend(imageHost),
audioContent.member!!.nickname,
audioContentTheme.theme,
audioContentTheme.id,
audioContent.price,
audioContent.limited,
audioContent.remaining,
audioContent.isAdult,
audioContent.duration,
audioContent.content,
audioContent.isCommentAvailable,
formattedDateExpression(audioContent.createdAt),
formattedDateExpression(audioContent.releaseDate, "%Y-%m-%d %H:%i")
)
)
.from(audioContent)
.leftJoin(audioContent.curation, audioContentCuration)
.innerJoin(audioContent.theme, audioContentTheme)
.where(where)
.offset(offset)
.limit(limit)
.orderBy(audioContent.releaseDate.desc())
.fetch()
}
override fun getHashTagList(audioContentId: Long): List<String> {
return queryFactory
.select(hashTag.tag)
.from(audioContentHashTag)
.innerJoin(audioContentHashTag.hashTag, hashTag)
.innerJoin(audioContentHashTag.audioContent, audioContent)
.where(
audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContentHashTag.audioContent.id.eq(audioContentId))
.and(audioContentHashTag.isActive.isTrue)
)
.fetch()
}
override fun findByIdAndActiveTrue(audioContentId: Long): AudioContent? {
return queryFactory
.selectFrom(audioContent)
.where(
audioContent.id.eq(audioContentId),
audioContent.isActive.isTrue
)
.fetchFirst()
}
private fun formattedDateExpression(
dateTime: DateTimePath<LocalDateTime>,
format: String = "%Y-%m-%d"
): StringTemplate {
return Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
dateTime,
"UTC",
"Asia/Seoul"
),
format
)
}
}

View File

@@ -0,0 +1,128 @@
package kr.co.vividnext.sodalive.admin.content
import kr.co.vividnext.sodalive.admin.content.curation.AdminContentCurationRepository
import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository
import kr.co.vividnext.sodalive.admin.content.theme.AdminContentThemeRepository
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.tab.GetContentMainTabItem
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminContentService(
private val repository: AdminContentRepository,
private val themeRepository: AdminContentThemeRepository,
private val audioContentCloudFront: AudioContentCloudFront,
private val curationRepository: AdminContentCurationRepository,
private val contentMainTabRepository: AdminContentMainTabRepository
) {
fun getAudioContentList(status: ContentReleaseStatus, pageable: Pageable): GetAdminContentListResponse {
val totalCount = repository.getAudioContentTotalCount(status = status)
val audioContentAndThemeList = repository.getAudioContentList(
status = status,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
val audioContentList = audioContentAndThemeList
.map {
val tags = repository
.getHashTagList(audioContentId = it.audioContentId)
.joinToString(" ") { tag -> tag }
it.tags = tags
it
}
.map {
it.contentUrl = audioContentCloudFront.generateSignedURL(
resourcePath = it.contentUrl,
expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2)
)
it
}
return GetAdminContentListResponse(totalCount, audioContentList)
}
fun searchAudioContent(
status: ContentReleaseStatus,
searchWord: String,
pageable: Pageable
): GetAdminContentListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.")
val totalCount = repository.getAudioContentTotalCount(searchWord, status = status)
val audioContentAndThemeList = repository.getAudioContentList(
status = status,
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
searchWord = searchWord
)
val audioContentList = audioContentAndThemeList
.map {
val tags = repository
.getHashTagList(audioContentId = it.audioContentId)
.joinToString(" ") { tag -> tag }
it.tags = tags
it
}
.map {
it.contentUrl = audioContentCloudFront.generateSignedURL(
resourcePath = it.contentUrl,
expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2)
)
it
}
return GetAdminContentListResponse(totalCount, audioContentList)
}
@Transactional
fun updateAudioContent(request: UpdateAdminContentRequest) {
val audioContent = repository.findByIdOrNull(id = request.id)
?: throw SodaException("없는 콘텐츠 입니다.")
if (request.isDefaultCoverImage) {
audioContent.coverImage = "`profile/default_profile.png`"
}
if (request.isActive != null) {
if (!request.isActive) {
audioContent.releaseDate = null
}
audioContent.isActive = request.isActive
}
if (request.isAdult != null) {
audioContent.isAdult = request.isAdult
}
if (request.isCommentAvailable != null) {
audioContent.isCommentAvailable = request.isCommentAvailable
}
if (request.title != null) {
audioContent.title = request.title
}
if (request.detail != null) {
audioContent.detail = request.detail
}
if (request.curationId != null) {
val curation = curationRepository.findByIdAndActive(id = request.curationId)
audioContent.curation = curation
}
if (request.themeId != null) {
val theme = themeRepository.findByIdAndActive(id = request.themeId)
audioContent.theme = theme
}
}
fun getContentMainTabList(): List<GetContentMainTabItem> {
return contentMainTabRepository.findAllByActiveIsTrue()
}
}

View File

@@ -0,0 +1,31 @@
package kr.co.vividnext.sodalive.admin.content
import com.querydsl.core.annotations.QueryProjection
data class GetAdminContentListResponse(
val totalCount: Int,
val items: List<GetAdminContentListItem>
)
data class GetAdminContentListItem @QueryProjection constructor(
val audioContentId: Long,
val title: String,
val detail: String,
val curationTitle: String?,
val curationId: Long,
var coverImageUrl: String,
val creatorNickname: String,
val theme: String,
val themeId: Long,
val price: Int,
val totalContentCount: Int?,
val remainingContentCount: Int?,
val isAdult: Boolean,
val remainingTime: String,
var contentUrl: String,
val isCommentAvailable: Boolean,
val date: String,
val releaseDate: String?
) {
var tags: String = ""
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.content
data class UpdateAdminContentRequest(
val id: Long,
val isDefaultCoverImage: Boolean,
val title: String?,
val detail: String?,
val curationId: Long?,
val themeId: Long?,
val isAdult: Boolean?,
val isActive: Boolean?,
val isCommentAvailable: Boolean?
)

View File

@@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
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.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/audio-content/banner")
@PreAuthorize("hasRole('ADMIN')")
class AdminContentBannerController(private val service: AdminContentBannerService) {
@PostMapping
fun createAudioContentMainBanner(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAudioContentMainBanner(image, requestString))
@PutMapping
fun modifyAudioContentMainBanner(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAudioContentMainBanner(image, requestString))
@PutMapping("/orders")
fun updateBannerOrders(
@RequestBody request: UpdateBannerOrdersRequest
) = ApiResponse.ok(service.updateBannerOrders(request.ids), "수정되었습니다.")
@GetMapping
fun getAudioContentMainBannerList(
@RequestParam(value = "tabId", required = false) tabId: Long? = null
) = ApiResponse.ok(service.getAudioContentMainBannerList(tabId = tabId))
}

View File

@@ -0,0 +1,61 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner
import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
import kr.co.vividnext.sodalive.event.QEvent.event
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminContentBannerRepository : JpaRepository<AudioContentBanner, Long>, AdminContentBannerQueryRepository
interface AdminContentBannerQueryRepository {
fun getAudioContentMainBannerList(tabId: Long = 1): List<GetAdminContentBannerResponse>
}
class AdminContentBannerQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : AdminContentBannerQueryRepository {
override fun getAudioContentMainBannerList(tabId: Long): List<GetAdminContentBannerResponse> {
var where = audioContentBanner.isActive.isTrue
where = if (tabId <= 1L) {
where.and(audioContentMainTab.id.isNull)
} else {
where.and(audioContentMainTab.id.eq(tabId))
}
return queryFactory
.select(
QGetAdminContentBannerResponse(
audioContentBanner.id,
audioContentBanner.tab.id.coalesce(1),
audioContentBanner.type,
audioContentBanner.thumbnailImage.prepend("/").prepend(cloudFrontHost),
audioContentBanner.event.id,
audioContentBanner.event.thumbnailImage,
audioContentBanner.creator.id,
audioContentBanner.creator.nickname,
audioContentBanner.series.id,
audioContentBanner.series.title,
audioContentBanner.link,
audioContentBanner.isAdult
)
)
.from(audioContentBanner)
.leftJoin(audioContentBanner.event, event)
.leftJoin(audioContentBanner.creator, member)
.leftJoin(audioContentBanner.series, series)
.leftJoin(audioContentBanner.tab, audioContentMainTab)
.where(where)
.orderBy(audioContentBanner.orders.asc())
.fetch()
}
}

View File

@@ -0,0 +1,188 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
import kr.co.vividnext.sodalive.event.EventRepository
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminContentBannerService(
private val s3Uploader: S3Uploader,
private val repository: AdminContentBannerRepository,
private val memberRepository: MemberRepository,
private val seriesRepository: AdminContentSeriesRepository,
private val eventRepository: EventRepository,
private val contentMainTabRepository: AdminContentMainTabRepository,
private val objectMapper: ObjectMapper,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAudioContentMainBanner(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java)
if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) {
throw SodaException("크리에이터를 선택하세요.")
}
if (request.type == AudioContentBannerType.SERIES && request.seriesId == null) {
throw SodaException("시리즈를 선택하세요.")
}
if (request.type == AudioContentBannerType.LINK && request.link == null) {
throw SodaException("링크 url을 입력하세요.")
}
if (request.type == AudioContentBannerType.EVENT && request.eventId == null) {
throw SodaException("이벤트를 선택하세요.")
}
val event = if (request.eventId != null && request.eventId > 0) {
eventRepository.findByIdOrNull(request.eventId)
} else {
null
}
val creator = if (request.creatorId != null && request.creatorId > 0) {
memberRepository.findByIdOrNull(request.creatorId)
} else {
null
}
val series = if (request.seriesId != null && request.seriesId > 0) {
seriesRepository.findByIdOrNull(request.seriesId)
} else {
null
}
val tab = if (request.tabId !== null) {
contentMainTabRepository.findByIdOrNull(request.tabId)
} else {
null
}
val audioContentBanner = AudioContentBanner(type = request.type)
audioContentBanner.link = request.link
audioContentBanner.isAdult = request.isAdult
audioContentBanner.event = event
audioContentBanner.creator = creator
audioContentBanner.series = series
audioContentBanner.tab = tab
repository.save(audioContentBanner)
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audio_content_banner/${audioContentBanner.id}/$fileName"
)
audioContentBanner.thumbnailImage = imagePath
}
@Transactional
fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java)
val audioContentBanner = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 요청입니다.")
if (image != null) {
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audio_content_banner/${audioContentBanner.id}/$fileName"
)
audioContentBanner.thumbnailImage = imagePath
}
if (request.isAdult != null) {
audioContentBanner.isAdult = request.isAdult
}
if (request.isActive != null) {
audioContentBanner.isActive = request.isActive
}
if (request.type != null) {
audioContentBanner.creator = null
audioContentBanner.event = null
audioContentBanner.link = null
audioContentBanner.series = null
when (request.type) {
AudioContentBannerType.EVENT -> {
if (request.eventId != null) {
val event = eventRepository.findByIdOrNull(request.eventId)
?: throw SodaException("이벤트를 선택하세요.")
audioContentBanner.event = event
} else {
throw SodaException("이벤트를 선택하세요.")
}
}
AudioContentBannerType.CREATOR -> {
if (request.creatorId != null) {
val creator = memberRepository.findByIdOrNull(request.creatorId)
?: throw SodaException("크리에이터를 선택하세요.")
audioContentBanner.creator = creator
} else {
throw SodaException("크리에이터를 선택하세요.")
}
}
AudioContentBannerType.LINK -> {
if (request.link != null) {
audioContentBanner.link = request.link
} else {
throw SodaException("링크 url을 입력하세요.")
}
}
AudioContentBannerType.SERIES -> {
if (request.seriesId != null) {
val series = seriesRepository.findByIdOrNull(request.seriesId)
?: throw SodaException("시리즈를 선택하세요.")
audioContentBanner.series = series
} else {
throw SodaException("시리즈를 선택하세요.")
}
}
}
audioContentBanner.type = request.type
}
if (request.tabId !== null) {
audioContentBanner.tab = contentMainTabRepository.findByIdOrNull(request.tabId)
}
}
@Transactional
fun updateBannerOrders(ids: List<Long>) {
for (index in ids.indices) {
val tag = repository.findByIdOrNull(ids[index])
if (tag != null) {
tag.orders = index + 1
}
}
}
fun getAudioContentMainBannerList(tabId: Long?): List<GetAdminContentBannerResponse> {
return repository.getAudioContentMainBannerList(tabId = tabId ?: 1)
}
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class CreateContentBannerRequest(
val type: AudioContentBannerType,
val tabId: Long?,
val eventId: Long?,
val creatorId: Long?,
val seriesId: Long?,
val link: String?,
val isAdult: Boolean
)

View File

@@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class GetAdminContentBannerResponse @QueryProjection constructor(
val id: Long,
val tabId: Long?,
val type: AudioContentBannerType,
val thumbnailImageUrl: String,
val eventId: Long?,
val eventThumbnailImage: String?,
val creatorId: Long?,
val creatorNickname: String?,
val seriesId: Long?,
val seriesTitle: String?,
val link: String?,
val isAdult: Boolean
)

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.admin.content.banner
data class UpdateBannerOrdersRequest(
val ids: List<Long>
)

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class UpdateContentBannerRequest(
val id: Long,
val type: AudioContentBannerType?,
val tabId: Long?,
val eventId: Long?,
val creatorId: Long?,
val seriesId: Long?,
val link: String?,
val isAdult: Boolean?,
val isActive: Boolean?
)

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class AddItemToCurationRequest(
val curationId: Long,
val itemIdList: List<Long>
)

View File

@@ -0,0 +1,68 @@
package kr.co.vividnext.sodalive.admin.content.curation
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
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("/admin/audio-content/curation")
@PreAuthorize("hasRole('ADMIN')")
class AdminContentCurationController(private val service: AdminContentCurationService) {
@PostMapping
fun createContentCuration(
@RequestBody request: CreateContentCurationRequest
) = ApiResponse.ok(service.createContentCuration(request))
@PutMapping
fun updateContentCuration(
@RequestBody request: UpdateContentCurationRequest
) = ApiResponse.ok(service.updateContentCuration(request))
@PutMapping("/orders")
fun updateContentCurationOrders(
@RequestBody request: UpdateContentCurationOrdersRequest
) = ApiResponse.ok(service.updateContentCurationOrders(request.ids), "수정되었습니다.")
@GetMapping
fun getContentCurationList(
@RequestParam tabId: Long
) = ApiResponse.ok(service.getContentCurationList(tabId = tabId))
@GetMapping("/items")
fun getCurationItems(
@RequestParam curationId: Long
) = ApiResponse.ok(service.getCurationItem(curationId = curationId))
@GetMapping("/search/content")
fun searchCurationContentItem(
@RequestParam curationId: Long,
@RequestParam searchWord: String
) = ApiResponse.ok(service.searchCurationContentItem(curationId, searchWord))
@GetMapping("/search/series")
fun searchCurationSeriesItem(
@RequestParam curationId: Long,
@RequestParam searchWord: String
) = ApiResponse.ok(service.searchCurationSeriesItem(curationId, searchWord))
@PostMapping("/add/item")
fun addItemToCuration(
@RequestBody request: AddItemToCurationRequest
) = ApiResponse.ok(service.addItemToCuration(request), "큐레이션 아이템을 등록했습니다.")
@PutMapping("/remove/item")
fun removeItemInCuration(
@RequestBody request: RemoveItemInCurationRequest
) = ApiResponse.ok(service.removeItemInCuration(request), "큐레이션 아이템을 제거했습니다.")
@PutMapping("/orders/item")
fun updateItemInCurationOrders(
@RequestBody request: UpdateCurationItemOrdersRequest
) = ApiResponse.ok(service.updateItemInCurationOrders(request), "수정되었습니다.")
}

View File

@@ -0,0 +1,106 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCurationItem.audioContentCurationItem
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AdminContentCurationItemRepository :
JpaRepository<AudioContentCurationItem, Long>,
AdminContentCurationItemQueryRepository
interface AdminContentCurationItemQueryRepository {
fun findByCurationIdAndSeriesId(curationId: Long, seriesId: Long?): AudioContentCurationItem?
fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): AudioContentCurationItem?
fun findByCurationIdAndItemId(curationId: Long, itemId: Long): AudioContentCurationItem?
fun getAudioContentCurationItemList(curationId: Long): List<GetCurationItemResponse>
fun getAudioContentCurationSeriesItemList(curationId: Long): List<GetCurationItemResponse>
}
class AdminContentCurationItemQueryRepositoryImpl(
val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) : AdminContentCurationItemQueryRepository {
override fun findByCurationIdAndSeriesId(curationId: Long, seriesId: Long?): AudioContentCurationItem? {
return queryFactory
.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.series, series)
.where(
audioContentCurationItem.curation.id.eq(curationId),
audioContentCurationItem.series.id.eq(seriesId)
)
.fetchFirst()
}
override fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): AudioContentCurationItem? {
return queryFactory
.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.content, audioContent)
.where(
audioContentCurationItem.curation.id.eq(curationId),
audioContentCurationItem.content.id.eq(contentId)
)
.fetchFirst()
}
override fun findByCurationIdAndItemId(curationId: Long, itemId: Long): AudioContentCurationItem? {
return queryFactory.selectFrom(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.where(audioContentCuration.id.eq(curationId), audioContentCurationItem.id.eq(itemId))
.fetchFirst()
}
override fun getAudioContentCurationItemList(curationId: Long): List<GetCurationItemResponse> {
return queryFactory
.select(
QGetCurationItemResponse(
audioContentCurationItem.id,
audioContent.title,
audioContent.detail,
audioContent.coverImage.prepend("/").prepend(imageHost),
audioContent.member.nickname.coalesce(""),
audioContent.isAdult
)
)
.from(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.content, audioContent)
.where(
audioContentCuration.id.eq(curationId),
audioContentCurationItem.isActive.isTrue
)
.orderBy(audioContentCurationItem.orders.asc())
.fetch()
}
override fun getAudioContentCurationSeriesItemList(curationId: Long): List<GetCurationItemResponse> {
return queryFactory
.select(
QGetCurationItemResponse(
audioContentCurationItem.id,
series.title,
series.introduction,
series.coverImage.prepend("/").prepend(imageHost),
series.member.nickname.coalesce(""),
series.isAdult
)
)
.from(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.series, series)
.where(
audioContentCuration.id.eq(curationId),
audioContentCurationItem.isActive.isTrue
)
.orderBy(audioContentCurationItem.orders.asc())
.fetch()
}
}

View File

@@ -0,0 +1,122 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCurationItem.audioContentCurationItem
import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminContentCurationRepository :
JpaRepository<AudioContentCuration, Long>,
AdminContentCurationQueryRepository
interface AdminContentCurationQueryRepository {
fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse>
fun findByIdAndActive(id: Long): AudioContentCuration?
fun searchCurationContentItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse>
fun searchCurationSeriesItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse>
}
@Repository
class AdminContentCurationQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) : AdminContentCurationQueryRepository {
override fun getAudioContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> {
return queryFactory
.select(
QGetAdminContentCurationResponse(
audioContentCuration.id,
audioContentMainTab.id,
audioContentCuration.title,
audioContentCuration.description,
audioContentCuration.isAdult,
audioContentCuration.isSeries
)
)
.from(audioContentCuration)
.innerJoin(audioContentCuration.tab, audioContentMainTab)
.where(
audioContentCuration.isActive.isTrue,
audioContentMainTab.id.eq(tabId)
)
.orderBy(audioContentCuration.orders.asc())
.fetch()
}
override fun findByIdAndActive(id: Long): AudioContentCuration? {
return queryFactory
.selectFrom(audioContentCuration)
.where(
audioContentCuration.id.eq(id)
.and(audioContentCuration.isActive.isTrue)
)
.fetchFirst()
}
override fun searchCurationContentItem(
curationId: Long,
searchWord: String
): List<SearchCurationItemResponse> {
return queryFactory
.select(
QSearchCurationItemResponse(
audioContent.id,
audioContent.title,
audioContent.coverImage.prepend("/").prepend(imageHost)
)
)
.from(audioContent)
.leftJoin(audioContentCurationItem)
.on(
audioContent.id.eq(audioContentCurationItem.content.id)
.and(audioContentCurationItem.curation.id.eq(curationId))
)
.where(
audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
.and(audioContent.title.contains(searchWord))
.and(audioContentCurationItem.id.isNull)
)
.fetch()
}
override fun searchCurationSeriesItem(
curationId: Long,
searchWord: String
): List<SearchCurationItemResponse> {
return queryFactory
.select(
QSearchCurationItemResponse(
series.id,
series.title,
series.coverImage.prepend("/").prepend(imageHost)
)
)
.from(series)
.leftJoin(audioContentCurationItem)
.on(
series.id.eq(audioContentCurationItem.series.id)
.and(audioContentCurationItem.curation.id.eq(curationId))
)
.where(
series.isActive.isTrue
.and(series.member.isNotNull)
.and(series.title.contains(searchWord))
.and(
audioContentCurationItem.id.isNull
.or(audioContentCurationItem.isActive.isFalse)
)
)
.fetch()
}
}

View File

@@ -0,0 +1,168 @@
package kr.co.vividnext.sodalive.admin.content.curation
import kr.co.vividnext.sodalive.admin.content.AdminContentRepository
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminContentCurationService(
private val repository: AdminContentCurationRepository,
private val contentMainTabRepository: AdminContentMainTabRepository,
private val seriesRepository: AdminContentSeriesRepository,
private val contentRepository: AdminContentRepository,
private val contentCurationItemRepository: AdminContentCurationItemRepository
) {
@Transactional
fun createContentCuration(request: CreateContentCurationRequest) {
val tab = contentMainTabRepository.findByIdOrNull(request.tabId)
?: throw SodaException("잘못된 요청입니다.")
val curation = AudioContentCuration(
title = request.title,
description = request.description,
isAdult = request.isAdult,
isSeries = request.isSeries
)
curation.tab = tab
repository.save(curation)
}
@Transactional
fun updateContentCuration(request: UpdateContentCurationRequest) {
val audioContentCuration = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.")
if (request.title != null) {
audioContentCuration.title = request.title
}
if (request.description != null) {
audioContentCuration.description = request.description
}
if (request.isAdult != null) {
audioContentCuration.isAdult = request.isAdult
}
if (request.isActive != null) {
audioContentCuration.isActive = request.isActive
}
if (request.isSeries != null) {
audioContentCuration.isSeries = request.isSeries
}
if (request.tabId != null) {
val tab = contentMainTabRepository.findByIdOrNull(request.tabId)
if (tab != null) {
audioContentCuration.tab = tab
}
}
}
@Transactional
fun updateContentCurationOrders(ids: List<Long>) {
for (index in ids.indices) {
val audioContentCuration = repository.findByIdOrNull(ids[index])
if (audioContentCuration != null) {
audioContentCuration.orders = index + 1
}
}
}
fun getContentCurationList(tabId: Long): List<GetAdminContentCurationResponse> {
return repository.getAudioContentCurationList(tabId = tabId)
}
fun getCurationItem(curationId: Long): List<GetCurationItemResponse> {
val curation = repository.findByIdOrNull(curationId)
?: throw SodaException("잘못된 요청입니다.")
return if (curation.isSeries) {
contentCurationItemRepository.getAudioContentCurationSeriesItemList(curationId)
} else {
contentCurationItemRepository.getAudioContentCurationItemList(curationId)
}
}
fun searchCurationContentItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse> {
return repository.searchCurationContentItem(curationId, searchWord)
}
fun searchCurationSeriesItem(curationId: Long, searchWord: String): List<SearchCurationItemResponse> {
return repository.searchCurationSeriesItem(curationId, searchWord)
}
@Transactional
fun addItemToCuration(request: AddItemToCurationRequest) {
// 큐레이션 조회
val audioContentCuration = repository.findByIdOrNull(id = request.curationId)
?: throw SodaException("잘못된 요청입니다.")
if (audioContentCuration.isSeries) {
request.itemIdList.forEach { seriesId ->
val series = seriesRepository.findByIdAndActiveTrue(seriesId)
if (series != null) {
val item = contentCurationItemRepository.findByCurationIdAndSeriesId(
curationId = request.curationId,
seriesId = series.id
) ?: AudioContentCurationItem()
item.curation = audioContentCuration
item.series = series
item.isActive = true
contentCurationItemRepository.save(item)
}
}
} else {
request.itemIdList.forEach { contentId ->
val audioContent = contentRepository.findByIdAndActiveTrue(contentId)
if (audioContent != null) {
val item = contentCurationItemRepository.findByCurationIdAndContentId(
curationId = request.curationId,
contentId = audioContent.id
) ?: AudioContentCurationItem()
item.curation = audioContentCuration
item.content = audioContent
item.isActive = true
contentCurationItemRepository.save(item)
}
}
}
}
@Transactional
fun removeItemInCuration(request: RemoveItemInCurationRequest) {
val audioContentCurationItem = contentCurationItemRepository.findByCurationIdAndItemId(
curationId = request.curationId,
itemId = request.itemId
)
audioContentCurationItem?.isActive = false
}
@Transactional
fun updateItemInCurationOrders(request: UpdateCurationItemOrdersRequest) {
val ids = request.itemIds
for (index in ids.indices) {
val item = contentCurationItemRepository.findByCurationIdAndItemId(
curationId = request.curationId,
itemId = ids[index]
)
if (item != null) {
item.orders = index + 1
}
}
}
}

View File

@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class CreateContentCurationRequest(
val tabId: Long,
val title: String,
val description: String,
val isAdult: Boolean,
val isSeries: Boolean
)
data class UpdateContentCurationRequest(
val id: Long,
val tabId: Long?,
val title: String?,
val description: String?,
val isAdult: Boolean?,
val isSeries: Boolean?,
val isActive: Boolean?
)
data class UpdateContentCurationOrdersRequest(
val ids: List<Long>
)

View File

@@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class GetAdminContentCurationResponse @QueryProjection constructor(
val id: Long,
val tabId: Long,
val title: String,
val description: String,
val isAdult: Boolean,
val isSeries: Boolean
)

View File

@@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class GetCurationItemResponse @QueryProjection constructor(
val id: Long,
val title: String,
val desc: String,
val coverImageUrl: String,
val creatorNickname: String,
val isAdult: Boolean
)

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class RemoveItemInCurationRequest(
val curationId: Long,
val itemId: Long
)

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class SearchCurationItemResponse @QueryProjection constructor(
val id: Long,
val title: String,
val coverImageUrl: String
)

View File

@@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class UpdateCurationItemOrdersRequest(
val curationId: Long,
val itemIds: List<Long>
)

View File

@@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.admin.content.curation.tag
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.main.curation.tag.ContentHashTagCurationItem
import kr.co.vividnext.sodalive.content.main.curation.tag.QContentHashTagCuration.contentHashTagCuration
import kr.co.vividnext.sodalive.content.main.curation.tag.QContentHashTagCurationItem.contentHashTagCurationItem
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface AdminContentHashTagCurationItemRepository :
JpaRepository<ContentHashTagCurationItem, Long>,
AdminContentHashTagCurationItemQueryRepository
interface AdminContentHashTagCurationItemQueryRepository {
fun getContentHashTagCurationItemList(curationId: Long): List<GetAdminHashTagCurationItemResponse>
fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): ContentHashTagCurationItem?
fun findByCurationIdAndItemId(curationId: Long, itemId: Long): ContentHashTagCurationItem?
}
class AdminContentHashTagCurationItemQueryRepositoryImpl(
val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String
) : AdminContentHashTagCurationItemQueryRepository {
override fun getContentHashTagCurationItemList(curationId: Long): List<GetAdminHashTagCurationItemResponse> {
return queryFactory
.select(
QGetAdminHashTagCurationItemResponse(
contentHashTagCurationItem.id,
audioContent.title,
audioContent.detail,
audioContent.coverImage.prepend("/").prepend(imageHost),
audioContent.member.nickname.coalesce(""),
audioContent.isAdult
)
)
.from(contentHashTagCurationItem)
.innerJoin(contentHashTagCurationItem.curation, contentHashTagCuration)
.innerJoin(contentHashTagCurationItem.content, audioContent)
.where(
contentHashTagCuration.id.eq(curationId),
contentHashTagCurationItem.isActive.isTrue,
audioContent.isActive.isTrue
)
.orderBy(contentHashTagCurationItem.orders.asc())
.fetch()
}
override fun findByCurationIdAndContentId(curationId: Long, contentId: Long?): ContentHashTagCurationItem? {
return queryFactory
.selectFrom(contentHashTagCurationItem)
.innerJoin(contentHashTagCurationItem.curation, contentHashTagCuration)
.innerJoin(contentHashTagCurationItem.content, audioContent)
.where(
contentHashTagCuration.id.eq(curationId),
audioContent.id.eq(contentId)
)
.fetchFirst()
}
override fun findByCurationIdAndItemId(curationId: Long, itemId: Long): ContentHashTagCurationItem? {
return queryFactory.selectFrom(contentHashTagCurationItem)
.innerJoin(contentHashTagCurationItem.curation, contentHashTagCuration)
.where(
contentHashTagCuration.id.eq(curationId),
contentHashTagCurationItem.id.eq(itemId)
)
.fetchFirst()
}
}

Some files were not shown because too many files have changed in this diff Show More